checkpoint

master
voussoir 2023-03-21 21:25:57 -07:00
parent 5b68b33f1d
commit 06ca0fd2b3
12 changed files with 324 additions and 260 deletions

View File

@ -53,6 +53,14 @@ class Database(val trackbook: Trackbook)
this.connection.endTransaction()
}
fun delete_trkpt(device_id: String, time: Long)
{
Log.i("VOUSSOIR", "Database.delete_trkpt")
begin_transaction()
connection.delete("trkpt", "device_id = ? AND time = ?", arrayOf(device_id, time.toString()))
commit()
}
fun insert_trkpt(trkpt: Trkpt)
{
Log.i("VOUSSOIR", "Database.insert_trkpt")
@ -60,7 +68,7 @@ class Database(val trackbook: Trackbook)
put("device_id", trkpt.device_id)
put("lat", trkpt.latitude)
put("lon", trkpt.longitude)
put("time", GregorianCalendar.getInstance().time.time)
put("time", trkpt.time)
put("accuracy", trkpt.accuracy)
put("sat", trkpt.numberSatellites)
put("ele", trkpt.altitude)
@ -108,8 +116,9 @@ class Database(val trackbook: Trackbook)
{
begin_transaction()
this.connection.execSQL("CREATE TABLE IF NOT EXISTS meta(name TEXT PRIMARY KEY, value TEXT)")
this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time INTEGER NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, PRIMARY KEY(lat, lon, time, device_id))")
this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time INTEGER NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, PRIMARY KEY(device_id, time))")
this.connection.execSQL("CREATE TABLE IF NOT EXISTS homepoints(id INTEGER PRIMARY KEY, lat REAL NOT NULL, lon REAL NOT NULL, radius REAL NOT NULL, name TEXT)")
this.connection.execSQL("CREATE INDEX IF NOT EXISTS index_trkpt_device_id_time on trkpt(device_id, time)")
// The pragmas don't seem to execute unless you call moveToNext.
var cursor: Cursor
cursor = this.connection.rawQuery("PRAGMA journal_mode = DELETE", null)

View File

@ -16,6 +16,7 @@
package org.y20k.trackbook
import android.graphics.Color
import java.util.*
/*
@ -109,4 +110,7 @@ object Keys {
// notification
const val TRACKER_SERVICE_NOTIFICATION_ID: Int = 1
const val NOTIFICATION_CHANNEL_RECORDING: String = "notificationChannelIdRecordingChannel"
const val POLYLINE_THICKNESS = 4F
val POLYLINE_COLOR = Color.argb(255, 255, 0, 255)
}

View File

@ -40,8 +40,6 @@ import androidx.fragment.app.Fragment
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import org.osmdroid.api.IGeoPoint
import org.osmdroid.api.IMapController
import org.osmdroid.events.MapEventsReceiver
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
@ -51,46 +49,46 @@ import org.osmdroid.views.overlay.MapEventsOverlay
import org.osmdroid.views.overlay.Overlay
import org.osmdroid.views.overlay.OverlayItem
import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.compass.CompassOverlay
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
import org.y20k.trackbook.helpers.*
/*
* MapFragment class
*/
class MapFragment : Fragment()
{
/* Main class variables */
private lateinit var trackbook: Trackbook
private var bound: Boolean = false
private val handler: Handler = Handler(Looper.getMainLooper())
val handler: Handler = Handler(Looper.getMainLooper())
private var trackingState: Int = Keys.STATE_TRACKING_STOPPED
private var gpsProviderActive: Boolean = false
private var networkProviderActive: Boolean = false
private lateinit var currentBestLocation: Location
private lateinit var trackerService: TrackerService
private lateinit var trackbook: Trackbook
lateinit var rootView: View
var continuous_auto_center: Boolean = true
lateinit var currentLocationButton: FloatingActionButton
private lateinit var trackerService: TrackerService
private lateinit var database_changed_listener: DatabaseChangedListener
var thismapfragment: MapFragment? = null
lateinit var rootView: View
private lateinit var mapView: MapView
lateinit var mainButton: ExtendedFloatingActionButton
lateinit var zoom_in_button: FloatingActionButton
lateinit var zoom_out_button: FloatingActionButton
lateinit var mainButton: ExtendedFloatingActionButton
private lateinit var mapView: MapView
lateinit var currentLocationButton: FloatingActionButton
private var current_track_overlay: Polyline? = null
private var current_position_overlays = ArrayList<Overlay>()
private var currentTrackOverlay: SimpleFastPointOverlay? = null
private lateinit var locationErrorBar: Snackbar
private var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL
private var homepoints_overlays = ArrayList<Overlay>()
private lateinit var database_changed_listener: DatabaseChangedListener
private lateinit var locationErrorBar: Snackbar
/* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?)
{
Log.i("VOUSSOIR", "MapFragment.onCreate")
super.onCreate(savedInstanceState)
thismapfragment = this
this.trackbook = (requireContext().applicationContext as Trackbook)
database_changed_listener = object: DatabaseChangedListener
{
@ -99,7 +97,7 @@ class MapFragment : Fragment()
Log.i("VOUSSOIR", "MapFragment database_ready_changed to ${trackbook.database.ready}")
if (trackbook.database.ready)
{
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
create_homepoint_overlays()
}
else
{
@ -116,7 +114,7 @@ class MapFragment : Fragment()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
{
Log.i("VOUSSOIR", "MapFragment.onCreateView")
// find views
rootView = inflater.inflate(R.layout.fragment_map, container, false)
mapView = rootView.findViewById(R.id.map)
currentLocationButton = rootView.findViewById(R.id.location_button)
@ -130,8 +128,6 @@ class MapFragment : Fragment()
true
}
mapView.isLongClickable = true
// basic map setup
mapView.isTilesScaledToDpi = true
mapView.isVerticalMapRepetitionEnabled = false
mapView.setTileSource(TileSourceFactory.MAPNIK)
@ -145,10 +141,8 @@ class MapFragment : Fragment()
}
val densityScalingFactor: Float = UiHelper.getDensityScalingFactor(requireContext())
val compassOverlay = CompassOverlay(requireContext(), InternalCompassOrientationProvider(requireContext()), mapView)
compassOverlay.enableCompass()
// compassOverlay.setCompassCenter(36f, 36f + (statusBarHeight / densityScalingFactor)) // TODO uncomment when transparent status bar is re-implemented
val screen_width = Resources.getSystem().displayMetrics.widthPixels
compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f)
mapView.overlays.add(compassOverlay)
@ -187,7 +181,7 @@ class MapFragment : Fragment()
radius=radius,
)
trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
create_homepoint_overlays()
dialog.dismiss()
}
@ -198,28 +192,34 @@ class MapFragment : Fragment()
mapView.overlays.add(MapEventsOverlay(receiver))
trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
create_homepoint_overlays()
if (database_changed_listener !in trackbook.database_changed_listeners)
{
trackbook.database_changed_listeners.add(database_changed_listener)
}
create_current_position_overlays(currentBestLocation, trackingState)
centerMap(currentBestLocation)
// initialize track overlays
currentTrackOverlay = null
// initialize main button state
update_main_button()
current_track_overlay = null
mapView.setOnTouchListener { v, event ->
continuous_auto_center = false
false
}
// set up buttons
update_main_button()
mainButton.setOnClickListener {
handleTrackingManagementMenu()
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{
trackerService.stopTracking()
}
else
{
startTracking()
}
handler.post(location_update_redraw)
}
currentLocationButton.setOnClickListener {
centerMap(currentBestLocation, animated=true)
@ -238,6 +238,7 @@ class MapFragment : Fragment()
/* Overrides onStart from Fragment */
override fun onStart()
{
Log.i("VOUSSOIR", "MapFragment.onStart")
super.onStart()
// request location permission if denied
if (ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)
@ -265,6 +266,10 @@ class MapFragment : Fragment()
{
Log.i("VOUSSOIR", "MapFragment.onPause")
super.onPause()
if (::trackerService.isInitialized)
{
trackerService.mapfragment = null
}
saveBestLocationState(currentBestLocation)
if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) {
trackerService.removeGpsLocationListener()
@ -278,6 +283,10 @@ class MapFragment : Fragment()
override fun onStop()
{
super.onStop()
if (::trackerService.isInitialized)
{
trackerService.mapfragment = null
}
// unbind from TrackerService
if (bound)
{
@ -290,6 +299,7 @@ class MapFragment : Fragment()
{
Log.i("VOUSSOIR", "MapFragment.onDestroy")
super.onDestroy()
trackerService.mapfragment = null
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (database_changed_listener in trackbook.database_changed_listeners)
@ -314,14 +324,17 @@ class MapFragment : Fragment()
toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
}
/* Start recording waypoints */
private fun startTracking() {
private fun startTracking()
{
// start service via intent so that it keeps running after unbind
val intent = Intent(activity, TrackerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
// ... start service in foreground to prevent it being killed on Oreo
activity?.startForegroundService(intent)
} else {
}
else
{
activity?.startService(intent)
}
trackerService.startTracking()
@ -334,16 +347,9 @@ class MapFragment : Fragment()
bound = false
// unregister listener for changes in shared preferences
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
// stop receiving location updates
handler.removeCallbacks(periodicLocationRequestRunnable)
}
/* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */
private fun handleTrackingManagementMenu()
{
when (trackingState) {
Keys.STATE_TRACKING_ACTIVE -> trackerService.stopTracking()
Keys.STATE_TRACKING_STOPPED -> startTracking()
if (::trackerService.isInitialized)
{
trackerService.mapfragment = null
}
}
@ -374,7 +380,8 @@ class MapFragment : Fragment()
continuous_auto_center = true
}
fun saveBestLocationState(currentBestLocation: Location) {
fun saveBestLocationState(currentBestLocation: Location)
{
PreferencesHelper.saveCurrentBestLocation(currentBestLocation)
PreferencesHelper.saveZoomLevel(mapView.zoomLevelDouble)
continuous_auto_center = true
@ -395,38 +402,26 @@ class MapFragment : Fragment()
/* Mark current position on map */
fun create_current_position_overlays(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED)
{
// Log.i("VOUSSOIR", "MapFragmentLayoutHolder.markCurrentPosition")
clear_current_position_overlays()
val locationIsOld: Boolean = !(isRecentEnough(location))
// create marker
val newMarker: Drawable
val fillcolor: Int
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
if (locationIsOld)
{
fillcolor = Color.argb(64, 0, 0, 0)
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!!
}
else if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{
fillcolor = Color.argb(64, 220, 61, 51)
if (locationIsOld)
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!!
}
else
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_red_24dp)!!
}
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_red_24dp)!!
}
else
{
fillcolor = Color.argb(64, 60, 152, 219)
if(locationIsOld)
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!!
}
else
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_blue_24dp)!!
}
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_blue_24dp)!!
}
val current_location_radius = Polygon()
@ -447,6 +442,21 @@ class MapFragment : Fragment()
}
}
fun clear_track_overlay()
{
mapView.overlays.remove(current_track_overlay)
}
fun create_track_overlay()
{
clear_track_overlay()
val pl = Polyline(mapView)
pl.outlinePaint.strokeWidth = Keys.POLYLINE_THICKNESS
pl.outlinePaint.color = Keys.POLYLINE_COLOR
mapView.overlays.add(pl)
current_track_overlay = pl
}
fun clear_homepoint_overlays()
{
for (ov in homepoints_overlays)
@ -459,15 +469,16 @@ class MapFragment : Fragment()
homepoints_overlays.clear()
}
fun create_homepoint_overlays(context: Context, map_view: MapView, homepoints: List<Homepoint>)
fun create_homepoint_overlays()
{
Log.i("VOUSSOIR", "MapFragmentLayoutHolder.createHomepointOverlays")
val context = requireContext()
val newMarker: Drawable = ContextCompat.getDrawable(context, R.drawable.ic_homepoint_24dp)!!
clear_homepoint_overlays()
for (homepoint in homepoints)
for (homepoint in trackbook.homepoints)
{
val p = Polygon()
p.points = Polygon.pointsAsCircle(GeoPoint(homepoint.location.latitude, homepoint.location.longitude), homepoint.location.accuracy.toDouble())
@ -476,7 +487,14 @@ class MapFragment : Fragment()
homepoints_overlays.add(p)
val overlayItems: java.util.ArrayList<OverlayItem> = java.util.ArrayList<OverlayItem>()
val overlayItem: OverlayItem = createOverlayItem(context, homepoint.location.latitude, homepoint.location.longitude, homepoint.location.accuracy, homepoint.location.provider.toString(), homepoint.location.time)
val overlayItem: OverlayItem = createOverlayItem(
context,
homepoint.location.latitude,
homepoint.location.longitude,
homepoint.location.accuracy,
homepoint.location.provider.toString(),
homepoint.location.time
)
overlayItem.setMarker(newMarker)
overlayItems.add(overlayItem)
val homepoint_overlay = ItemizedIconOverlay<OverlayItem>(context, overlayItems,
@ -504,14 +522,14 @@ class MapFragment : Fragment()
delete_button.setOnClickListener {
trackbook.database.delete_homepoint(homepoint.id)
trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
create_homepoint_overlays()
dialog.dismiss()
}
save_button.setOnClickListener {
val radius = radius_input.text.toString().toDoubleOrNull() ?: 25.0
trackbook.database.update_homepoint(homepoint.id, name=name_input.text.toString(), radius=radius)
trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
create_homepoint_overlays()
dialog.dismiss()
}
@ -529,19 +547,6 @@ class MapFragment : Fragment()
}
}
/* Overlay current track on map */
fun create_current_track_overlay(geopoints: MutableList<IGeoPoint>, trackingState: Int)
{
if (currentTrackOverlay != null)
{
mapView.overlays.remove(currentTrackOverlay)
}
if (geopoints.isNotEmpty())
{
currentTrackOverlay = createTrackOverlay(requireContext(), mapView, geopoints, trackingState)
}
}
fun update_main_button()
{
mainButton.isEnabled = trackbook.database.ready
@ -592,14 +597,13 @@ class MapFragment : Fragment()
// get reference to tracker service
val binder = service as TrackerService.LocalBinder
trackerService = binder.service
trackerService.mapfragment = thismapfragment
// get state of tracking and update button if necessary
trackingState = trackerService.trackingState
update_main_button()
// register listener for changes in shared preferences
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
// start listening for location updates
handler.removeCallbacks(periodicLocationRequestRunnable)
handler.postDelayed(periodicLocationRequestRunnable, 0)
}
override fun onServiceDisconnected(arg0: ComponentName)
{
@ -608,22 +612,27 @@ class MapFragment : Fragment()
}
}
private val periodicLocationRequestRunnable: Runnable = object : Runnable {
val location_update_redraw: Runnable = object : Runnable
{
override fun run()
{
Log.i("VOUSSOIR", "MapFragment.location_update_redraw")
currentBestLocation = trackerService.currentBestLocation
gpsProviderActive = trackerService.gpsProviderActive
networkProviderActive = trackerService.networkProviderActive
trackingState = trackerService.trackingState
// update location and track
create_current_position_overlays(currentBestLocation, trackingState)
create_current_track_overlay(trackerService.recent_trackpoints_for_mapview, trackingState)
// center map, if it had not been dragged/zoomed before
if (current_track_overlay == null)
{
create_track_overlay()
}
current_track_overlay!!.setPoints(trackerService.recent_trackpoints_for_mapview)
if (continuous_auto_center)
{
centerMap(currentBestLocation, animated=false)
}
handler.postDelayed(this, Keys.REQUEST_CURRENT_LOCATION_INTERVAL)
}
}
}

View File

@ -18,12 +18,13 @@ package org.y20k.trackbook
import android.content.Context
import android.database.Cursor
import android.database.DatabaseUtils.dumpCursorToString
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import org.y20k.trackbook.helpers.iso8601_format
import org.y20k.trackbook.helpers.iso8601
import java.text.SimpleDateFormat
import java.util.*
@ -33,7 +34,7 @@ data class Track (
var start_time: Date,
var end_time: Date,
var name: String = "",
val trkpts: ArrayDeque<Trkpt> = ArrayDeque<Trkpt>(),
val trkpts: ArrayList<Trkpt> = ArrayList<Trkpt>(),
var view_latitude: Double = Keys.DEFAULT_LATITUDE,
var view_longitude: Double = Keys.DEFAULT_LONGITUDE,
var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
@ -73,7 +74,7 @@ data class Track (
>
""".trimIndent())
write("\t<metadata>")
write("\t\t<name>Trackbook Recording: ${this.name}</name>")
write("\t\t<name>${this.name}</name>")
write("\t\t<device>${this.device_id}</device>")
write("\t</metadata>")
@ -81,7 +82,6 @@ data class Track (
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
write("\t<trk>")
write("\t\t<name>${this.name}</name>")
write("\t\t<trkseg>")
var previous: Trkpt? = null
@ -94,7 +94,8 @@ data class Track (
}
write("\t\t\t<trkpt lat=\"${trkpt.latitude}\" lon=\"${trkpt.longitude}\">")
write("\t\t\t\t<ele>${trkpt.altitude}</ele>")
write("\t\t\t\t<time>${iso8601_format.format(trkpt.time)}</time>")
write("\t\t\t\t<time>${iso8601(trkpt.time)}</time>")
write("\t\t\t\t<unix>${trkpt.time}</unix>")
write("\t\t\t\t<sat>${trkpt.numberSatellites}</sat>")
write("\t\t\t</trkpt>")
previous = trkpt
@ -170,11 +171,11 @@ data class Track (
fun trkpt_generator() = iterator<Trkpt>
{
val cursor: Cursor = database.connection.rawQuery(
var cursor: Cursor = database.connection.rawQuery(
"SELECT lat, lon, time, ele, accuracy, sat FROM trkpt WHERE device_id = ? AND time > ? AND time < ? ORDER BY time ASC",
arrayOf(device_id, start_time.time.toString(), end_time.time.toString())
)
Log.i("VOUSSOIR", "Track.trkpt_generator: Querying points between ${start_time} -- ${end_time}")
Log.i("VOUSSOIR", "Track.trkpt_generator: Querying points between ${start_time} -- ${end_time}, ${cursor.count} results")
val COLUMN_LAT = cursor.getColumnIndex("lat")
val COLUMN_LON = cursor.getColumnIndex("lon")
val COLUMN_ELE = cursor.getColumnIndex("ele")

View File

@ -20,6 +20,7 @@ import YesNoDialog
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Paint
import android.net.Uri
import android.os.Bundle
import android.os.Handler
@ -48,32 +49,38 @@ import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.ItemizedIconOverlay
import org.osmdroid.views.overlay.OverlayItem
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions
import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme
import org.y20k.trackbook.helpers.AppThemeHelper
import org.y20k.trackbook.helpers.DateTimeHelper
import org.y20k.trackbook.helpers.LengthUnitHelper
import org.y20k.trackbook.helpers.PreferencesHelper
import org.y20k.trackbook.helpers.createTrackOverlay
import org.y20k.trackbook.helpers.UiHelper
import org.y20k.trackbook.helpers.create_start_end_markers
import org.y20k.trackbook.helpers.iso8601_format
import org.y20k.trackbook.helpers.iso8601
import org.y20k.trackbook.helpers.iso8601_parse
import java.text.SimpleDateFormat
import java.util.*
class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
{
private lateinit var trackbook: Trackbook
lateinit var rootView: View
lateinit var save_track_button: ImageButton
lateinit var deleteButton: ImageButton
lateinit var zoom_in_button: FloatingActionButton
lateinit var zoom_out_button: FloatingActionButton
lateinit var trackNameView: MaterialTextView
lateinit var selected_trkpt_info: MaterialTextView
lateinit var track_query_start_date: DatePicker
lateinit var track_query_start_time: TimePicker
lateinit var track_query_end_date: DatePicker
lateinit var track_query_end_time: TimePicker
lateinit var delete_selected_trkpt_button: ImageButton
var track_query_start_time_previous: Int = 0
var track_query_end_time_previous: Int = 0
private lateinit var mapView: MapView
@ -95,22 +102,25 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
private lateinit var negativeElevationView: MaterialTextView
private lateinit var elevationDataViews: Group
private lateinit var track: Track
private lateinit var track_segment_overlays: ArrayDeque<Polyline>
private var track_geopoints: MutableList<IGeoPoint> = mutableListOf()
private var track_points_overlay: SimpleFastPointOverlay? = null
// private lateinit var trkpt_infowindow: InfoWindow
private var useImperialUnits: Boolean = false
private val handler: Handler = Handler(Looper.getMainLooper())
private var special_points_overlay: ItemizedIconOverlay<OverlayItem>? = null
private var track_overlay: SimpleFastPointOverlay? = null
val RERENDER_DELAY: Long = 1000
/* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
{
this.trackbook = (requireContext().applicationContext as Trackbook)
val database: Database = (requireActivity().applicationContext as Trackbook).database
track = Track(
database=database,
name=this.requireArguments().getString(Keys.ARG_TRACK_TITLE, ""),
device_id= this.requireArguments().getString(Keys.ARG_TRACK_DEVICE_ID, ""),
start_time= iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!),
end_time=iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!),
start_time=iso8601_parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!),
end_time=iso8601_parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!),
)
track.load_trkpts()
rootView = inflater.inflate(R.layout.fragment_track, container, false)
@ -131,6 +141,8 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
controller.setCenter(GeoPoint(track.view_latitude, track.view_longitude))
controller.setZoom(Keys.DEFAULT_ZOOM_LEVEL)
// trkpt_infowindow = MarkerInfoWindow(R.layout.trkpt_infowindow, mapView)
statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
statisticsView = rootView.findViewById(R.id.statistics_view)
distanceView = rootView.findViewById(R.id.statistics_data_distance)
@ -227,6 +239,27 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
}
})
selected_trkpt_info = rootView.findViewById(R.id.selected_trkpt_info)
delete_selected_trkpt_button = rootView.findViewById(R.id.delete_selected_trkpt_button)
delete_selected_trkpt_button.setOnClickListener {
Log.i("VOUSSOIR", "delete selected trkpt button.")
if (track_points_overlay != null)
{
val selected = (track_geopoints[track_points_overlay!!.selectedPoint] as Trkpt)
track_geopoints.remove(selected)
track_points_overlay!!.selectedPoint = null
Log.i("VOUSSOIR", selected.rendered_by_polyline?.actualPoints?.size.toString())
selected.rendered_by_polyline?.actualPoints?.remove(selected)
Log.i("VOUSSOIR", selected.rendered_by_polyline?.actualPoints?.size.toString())
selected.rendered_by_polyline?.setPoints(ArrayList(selected.rendered_by_polyline?.actualPoints))
Log.i("VOUSSOIR", selected.rendered_by_polyline?.actualPoints?.size.toString())
trackbook.database.delete_trkpt(selected.device_id, selected.time)
delete_selected_trkpt_button.visibility = View.GONE
selected_trkpt_info.text = ""
mapView.invalidate()
}
}
save_track_button.setOnClickListener {
openSaveGpxDialog()
}
@ -251,6 +284,7 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
statisticsSheetBehavior = BottomSheetBehavior.from<View>(statisticsSheet)
statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
track_segment_overlays = ArrayDeque<Polyline>(10)
render_track()
return rootView
@ -265,25 +299,81 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
fun render_track()
{
Log.i("VOUSSOIR", "TrackFragment.render_track")
if (special_points_overlay != null)
mapView.overlays.clear()
track_segment_overlays.clear()
delete_selected_trkpt_button.visibility = View.GONE
setupStatisticsViews()
if (track.trkpts.isEmpty())
{
mapView.overlays.remove(special_points_overlay)
return
}
if (track_overlay != null)
{
mapView.overlays.remove(track_overlay)
}
val geopoints: MutableList<IGeoPoint> = mutableListOf()
Log.i("VOUSSOIR", "MapOverlayHelper.createTrackOverlay")
track_geopoints = mutableListOf()
for (trkpt in track.trkpts)
{
geopoints.add(trkpt)
track_geopoints.add(trkpt)
}
if (track.trkpts.isNotEmpty())
var pl = new_track_segment_overlay()
var previous_time: Long = 0
for (trkpt in track.trkpts)
{
track_overlay = createTrackOverlay(requireContext(), mapView, geopoints, Keys.STATE_TRACKING_STOPPED)
special_points_overlay = create_start_end_markers(requireContext(), mapView, track.trkpts)
if (previous_time > 0 && (trkpt.time - previous_time) > Keys.STOP_OVER_THRESHOLD)
{
pl = new_track_segment_overlay()
}
pl.addPoint(trkpt)
trkpt.rendered_by_polyline = pl
previous_time = trkpt.time
}
setupStatisticsViews()
for (pl in track_segment_overlays)
{
create_start_end_markers(requireContext(), mapView, pl.actualPoints.first() as Trkpt, pl.actualPoints.last() as Trkpt)
}
val pointTheme = SimplePointTheme(track_geopoints, false)
val style = Paint()
style.style = Paint.Style.FILL
style.color = Keys.POLYLINE_COLOR
style.flags = Paint.ANTI_ALIAS_FLAG
val overlayOptions: SimpleFastPointOverlayOptions = SimpleFastPointOverlayOptions.getDefaultStyle()
.setAlgorithm(SimpleFastPointOverlayOptions.RenderingAlgorithm.MEDIUM_OPTIMIZATION)
.setSymbol(SimpleFastPointOverlayOptions.Shape.CIRCLE)
.setPointStyle(style)
.setRadius(((Keys.POLYLINE_THICKNESS + 1 ) / 2) * UiHelper.getDensityScalingFactor(requireContext()))
.setIsClickable(true)
.setCellSize(12)
track_points_overlay = SimpleFastPointOverlay(pointTheme, overlayOptions)
mapView.overlays.add(track_points_overlay)
track_points_overlay!!.setOnClickListener(object : SimpleFastPointOverlay.OnClickListener {
override fun onClick(points: SimpleFastPointOverlay.PointAdapter?, point: Int?)
{
if (points == null || point == null || point == 0)
{
return
}
val trkpt = (points[point]) as Trkpt
Log.i("VOUSSOIR", "Clicked ${trkpt.device_id} ${trkpt.time}")
selected_trkpt_info.text = "${trkpt.time}\n${iso8601(trkpt.time)}\n${trkpt.latitude}\n${trkpt.longitude}"
delete_selected_trkpt_button.visibility = View.VISIBLE
return
}
})
}
fun new_track_segment_overlay(): Polyline
{
var pl = Polyline(mapView)
pl.outlinePaint.strokeWidth = Keys.POLYLINE_THICKNESS
pl.outlinePaint.color = Keys.POLYLINE_COLOR
pl.infoWindow = null
track_segment_overlays.add(pl)
mapView.overlays.add(pl)
return pl
}
fun get_datetime(datepicker: DatePicker, timepicker: TimePicker, seconds: Int): Date
@ -348,7 +438,16 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
/* Overrides onZoom from MapListener */
override fun onZoom(event: ZoomEvent?): Boolean
{
return (event != null)
if (event == null)
{
return false
}
if (track_points_overlay == null)
{
return false
}
track_points_overlay!!.isEnabled = event.zoomLevel >= 16
return true
}
/* Overrides onScroll from MapListener */

View File

@ -30,9 +30,7 @@ import android.Manifest
import android.os.*
import android.util.Log
import androidx.core.content.ContextCompat
import org.osmdroid.api.IGeoPoint
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.overlay.simplefastpoint.LabelledGeoPoint
import java.util.*
import org.y20k.trackbook.helpers.*
@ -55,8 +53,8 @@ class TrackerService: Service()
var location_min_time_ms: Long = 0
private val RECENT_TRKPT_COUNT = 7200
lateinit var recent_trkpts: Deque<Trkpt>
lateinit var recent_displacement_trkpts: Deque<Trkpt>
var recent_trackpoints_for_mapview: MutableList<IGeoPoint> = mutableListOf()
lateinit var recent_displacement_locations: Deque<Location>
var recent_trackpoints_for_mapview: MutableList<GeoPoint> = mutableListOf()
var gpsLocationListenerRegistered: Boolean = false
var networkLocationListenerRegistered: Boolean = false
var bound: Boolean = false
@ -67,6 +65,7 @@ class TrackerService: Service()
private lateinit var notificationHelper: NotificationHelper
private lateinit var gpsLocationListener: LocationListener
private lateinit var networkLocationListener: LocationListener
var mapfragment: MapFragment? = null
private fun addGpsLocationListener()
{
@ -160,6 +159,11 @@ class TrackerService: Service()
currentBestLocation = location
if (mapfragment != null)
{
mapfragment!!.handler.postDelayed(mapfragment!!.location_update_redraw, 0)
}
if (trackingState != Keys.STATE_TRACKING_ACTIVE)
{
return
@ -195,7 +199,7 @@ class TrackerService: Service()
return
}
}
if (! (recent_displacement_trkpts.isEmpty() || isDifferentEnough(recent_displacement_trkpts.first().toLocation(), location, omitRests)))
if (! (recent_displacement_locations.isEmpty() || isDifferentEnough(recent_displacement_locations.first(), location, omitRests)))
{
Log.i("VOUSSOIR", "Omitting due to too close to previous.")
return
@ -215,10 +219,10 @@ class TrackerService: Service()
recent_trackpoints_for_mapview.removeFirst()
}
recent_displacement_trkpts.add(trkpt)
while (recent_displacement_trkpts.size > 5)
recent_displacement_locations.add(location)
while (recent_displacement_locations.size > 5)
{
recent_displacement_trkpts.removeFirst()
recent_displacement_locations.removeFirst()
}
if (location.time - lastCommit > Keys.COMMIT_INTERVAL)
@ -275,7 +279,7 @@ class TrackerService: Service()
trackbook = (applicationContext as Trackbook)
trackbook.load_homepoints()
recent_trkpts = ArrayDeque<Trkpt>(RECENT_TRKPT_COUNT)
recent_displacement_trkpts = ArrayDeque<Trkpt>(5)
recent_displacement_locations = ArrayDeque<Location>(5)
recent_trackpoints_for_mapview = mutableListOf()
use_gps_location = PreferencesHelper.load_location_gps()
use_network_location = PreferencesHelper.load_location_network()
@ -392,7 +396,7 @@ class TrackerService: Service()
addNetworkLocationListener()
trackingState = Keys.STATE_TRACKING_ACTIVE
PreferencesHelper.saveTrackingState(trackingState)
recent_displacement_trkpts.clear()
recent_displacement_locations.clear()
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
}
@ -402,7 +406,7 @@ class TrackerService: Service()
trackingState = Keys.STATE_TRACKING_STOPPED
PreferencesHelper.saveTrackingState(trackingState)
recent_displacement_trkpts.clear()
recent_displacement_locations.clear()
displayNotification()
stopForeground(STOP_FOREGROUND_DETACH)
}

View File

@ -33,7 +33,7 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main
import org.y20k.trackbook.helpers.UiHelper
import org.y20k.trackbook.helpers.iso8601_format
import org.y20k.trackbook.helpers.iso8601
import org.y20k.trackbook.tracklist.TracklistAdapter
/*
@ -46,7 +46,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
private lateinit var trackElementList: RecyclerView
private lateinit var tracklistOnboarding: ConstraintLayout
/* Overrides onCreateView from Fragment */
/* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
@ -67,18 +67,6 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
trackElementList.itemAnimator = DefaultItemAnimator()
trackElementList.adapter = tracklistAdapter
// enable swipe to delete
val swipeHandler = object : UiHelper.SwipeToDeleteCallback(activity as Context) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// ask user
val adapterPosition: Int = viewHolder.adapterPosition // first position in list is reserved for statistics
val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n- ${tracklistAdapter.getTrackName(adapterPosition)}"
YesNoDialog(this@TracklistFragment as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_DELETE_TRACK, messageString = dialogMessage, yesButton = R.string.dialog_yes_no_positive_button_delete_recording, payload = adapterPosition)
}
}
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(trackElementList)
// toggle onboarding layout
toggleOnboardingLayout()
@ -90,40 +78,15 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
val bundle: Bundle = bundleOf(
Keys.ARG_TRACK_TITLE to track.name,
Keys.ARG_TRACK_DEVICE_ID to track.device_id,
Keys.ARG_TRACK_START_TIME to iso8601_format.format(track.start_time),
Keys.ARG_TRACK_STOP_TIME to iso8601_format.format(track.end_time),
Keys.ARG_TRACK_START_TIME to iso8601(track.start_time),
Keys.ARG_TRACK_STOP_TIME to iso8601(track.end_time),
)
findNavController().navigate(R.id.fragment_track, bundle)
}
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String)
{
when (type)
{
Keys.DIALOG_DELETE_TRACK ->
{
when (dialogResult) {
// user tapped remove track
true ->
{
tracklistAdapter.delete_track_at_position(activity as Context, payload)
toggleOnboardingLayout()
}
// user tapped cancel
false ->
{
// The user slid the track over to the side and turned it red, we have to
// bring it back.
tracklistAdapter.notifyItemChanged(payload)
}
}
}
}
}
// toggle onboarding layout
private fun toggleOnboardingLayout() {
private fun toggleOnboardingLayout()
{
when (tracklistAdapter.isEmpty()) {
true -> {
// show onboarding layout
@ -143,11 +106,13 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
*/
inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false)
{
override fun supportsPredictiveItemAnimations(): Boolean {
override fun supportsPredictiveItemAnimations(): Boolean
{
return true
}
override fun onLayoutCompleted(state: RecyclerView.State?) {
override fun onLayoutCompleted(state: RecyclerView.State?)
{
super.onLayoutCompleted(state)
// handle delete request from TrackFragment - after layout calculations are complete
val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L

View File

@ -19,6 +19,7 @@ package org.y20k.trackbook
import android.location.Location
import org.osmdroid.api.IGeoPoint
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.overlay.Polyline
import org.y20k.trackbook.helpers.getNumberOfSatellites
class Trkpt(
@ -30,6 +31,7 @@ class Trkpt(
val accuracy: Float,
val time: Long,
val numberSatellites: Int = 0,
var rendered_by_polyline: Polyline? = null
) : GeoPoint(latitude, longitude, altitude)
{
constructor(device_id: String, location: Location) : this(

View File

@ -15,12 +15,25 @@ import java.text.SimpleDateFormat
import java.util.*
import kotlin.random.Random.Default.nextBits
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
private val RNG = SecureRandom()
fun iso8601(timestamp: Long): String
{
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
iso8601_format.timeZone = TimeZone.getTimeZone("UTC")
return iso8601_format.format(timestamp)
}
fun iso8601(datetime: Date): String
{
return iso8601_format.format(datetime)
return iso8601(datetime.time)
}
fun iso8601_parse(datetime: String): Date
{
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
iso8601_format.timeZone = TimeZone.getTimeZone("UTC")
return iso8601_format.parse(datetime)
}
fun random_int(): Int

View File

@ -83,15 +83,4 @@ object DateTimeHelper {
fun convertToReadableDateAndTime(date: Date, dateStyle: Int = DateFormat.SHORT, timeStyle: Int = DateFormat.SHORT): String {
return "${DateFormat.getDateInstance(dateStyle, Locale.getDefault()).format(date)} ${DateFormat.getTimeInstance(timeStyle, Locale.getDefault()).format(date)}"
}
/* Calculates time difference between two locations */
fun calculateTimeDistance(previousLocation: Location?, location: Location): Long {
var timeDifference: Long = 0L
// two data points needed to calculate time difference
if (previousLocation != null) {
// get time difference
timeDifference = location.time - previousLocation.time
}
return timeDifference
}
}

View File

@ -17,81 +17,27 @@
package org.y20k.trackbook.helpers
import android.content.Context
import android.graphics.Paint
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.osmdroid.api.IGeoPoint
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.ItemizedIconOverlay
import org.osmdroid.views.overlay.OverlayItem
import org.osmdroid.views.overlay.simplefastpoint.LabelledGeoPoint
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions
import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme
import org.y20k.trackbook.Keys
import org.y20k.trackbook.R
import org.y20k.trackbook.Trkpt
import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.*
fun createTrackOverlay(context: Context, map_view: MapView, geopoints: MutableList<IGeoPoint>, trackingState: Int): SimpleFastPointOverlay
{
Log.i("VOUSSOIR", "MapOverlayHelper.createTrackOverlay")
val pointTheme = SimplePointTheme(geopoints, false)
val style = Paint()
style.style = Paint.Style.FILL
style.color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red) else context.getColor(R.color.default_blue)
style.flags = Paint.ANTI_ALIAS_FLAG
val overlayOptions: SimpleFastPointOverlayOptions = SimpleFastPointOverlayOptions.getDefaultStyle()
.setAlgorithm(SimpleFastPointOverlayOptions.RenderingAlgorithm.MAXIMUM_OPTIMIZATION)
.setSymbol(SimpleFastPointOverlayOptions.Shape.CIRCLE)
.setPointStyle(style)
.setRadius(6F * UiHelper.getDensityScalingFactor(context)) // radius is set in px - scaling factor makes that display density independent (= dp)
.setIsClickable(true)
.setCellSize(12) // Sets the grid cell size used for indexing, in pixels. Larger cells result in faster rendering speed, but worse fidelity. Default is 10 pixels, for large datasets (>10k points), use 15.
var overlay = SimpleFastPointOverlay(pointTheme, overlayOptions)
overlay.setOnClickListener(object : SimpleFastPointOverlay.OnClickListener {
override fun onClick(points: SimpleFastPointOverlay.PointAdapter?, point: Int?)
{
if (points == null || point == null || point == 0)
{
return
}
val trkpt = (points[point]) as Trkpt
Log.i("VOUSSOIR", "Clicked ${trkpt.device_id} ${trkpt.time}")
// trackpoints.remove(points[point])
// map_view.overlays.remove(overlay)
// overlay = SimpleFastPointOverlay(pointTheme, overlayOptions)
// overlay.setOnClickListener(this)
// map_view.overlays.add(overlay)
// map_view.postInvalidate()
return
}
})
map_view.overlays.add(overlay)
return overlay
}
fun create_start_end_markers(context: Context, map_view: MapView, trkpts: Collection<Trkpt>): ItemizedIconOverlay<OverlayItem>?
fun create_start_end_markers(context: Context, map_view: MapView, startpoint: Trkpt, endpoint: Trkpt): ItemizedIconOverlay<OverlayItem>?
{
Log.i("VOUSSOIR", "MapOverlayHelper.create_start_end_markers")
if (trkpts.size == 0)
{
return null
}
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
val startpoint = trkpts.first()
val endpoint = trkpts.last()
val startmarker: OverlayItem = createOverlayItem(context, startpoint.latitude, startpoint.longitude, startpoint.accuracy, startpoint.provider, startpoint.time)
startmarker.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_blue_48dp)!!)
overlayItems.add(startmarker)
if (trkpts.size > 1)
if (startpoint != endpoint)
{
val endmarker: OverlayItem = createOverlayItem(context, endpoint.latitude, endpoint.longitude, endpoint.accuracy, endpoint.provider, endpoint.time)
endmarker.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_blue_48dp)!!)

View File

@ -12,19 +12,42 @@
android:layout_height="match_parent"
>
<TextView
android:id="@+id/selected_trkpt_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="right"
android:text="time\nlat\nlong"
app:fontFamily="monospace"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/delete_selected_trkpt_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Delete selected trackpoint"
android:src="@drawable/ic_delete_24dp"
android:visibility="gone"
app:backgroundTint="@color/default_transparent"
app:layout_constraintBottom_toTopOf="@+id/map"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_delete_24dp" />
<DatePicker
android:id="@+id/track_query_start_date"
android:layout_width="wrap_content"
android:layout_height="130dp"
android:translationX="-45dp"
android:translationY="-30dp"
android:calendarViewShown="false"
android:datePickerMode="spinner"
app:layout_constraintTop_toTopOf="parent"
android:scaleX="0.6"
android:scaleY="0.6"
android:translationX="-45dp"
android:translationY="-30dp"
app:layout_constraintStart_toStartOf="parent"
android:scaleX="0.5"
android:scaleY="0.5"
/>
app:layout_constraintTop_toTopOf="parent" />
<TimePicker
android:id="@+id/track_query_start_time"
@ -35,8 +58,8 @@
android:timePickerMode="spinner"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/track_query_start_date"
android:scaleX="0.5"
android:scaleY="0.5"
android:scaleX="0.6"
android:scaleY="0.6"
/>
<DatePicker
@ -49,8 +72,8 @@
android:calendarViewShown="false"
app:layout_constraintTop_toBottomOf="@+id/track_query_start_date"
app:layout_constraintStart_toStartOf="parent"
android:scaleX="0.5"
android:scaleY="0.5"
android:scaleX="0.6"
android:scaleY="0.6"
/>
<TimePicker
@ -62,8 +85,8 @@
android:timePickerMode="spinner"
app:layout_constraintTop_toBottomOf="@+id/track_query_start_time"
app:layout_constraintStart_toEndOf="@+id/track_query_start_date"
android:scaleX="0.5"
android:scaleY="0.5"
android:scaleX="0.6"
android:scaleY="0.6"
/>
<org.osmdroid.views.MapView