checkpoint

This commit is contained in:
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() 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) fun insert_trkpt(trkpt: Trkpt)
{ {
Log.i("VOUSSOIR", "Database.insert_trkpt") Log.i("VOUSSOIR", "Database.insert_trkpt")
@ -60,7 +68,7 @@ class Database(val trackbook: Trackbook)
put("device_id", trkpt.device_id) put("device_id", trkpt.device_id)
put("lat", trkpt.latitude) put("lat", trkpt.latitude)
put("lon", trkpt.longitude) put("lon", trkpt.longitude)
put("time", GregorianCalendar.getInstance().time.time) put("time", trkpt.time)
put("accuracy", trkpt.accuracy) put("accuracy", trkpt.accuracy)
put("sat", trkpt.numberSatellites) put("sat", trkpt.numberSatellites)
put("ele", trkpt.altitude) put("ele", trkpt.altitude)
@ -108,8 +116,9 @@ class Database(val trackbook: Trackbook)
{ {
begin_transaction() 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 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 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. // The pragmas don't seem to execute unless you call moveToNext.
var cursor: Cursor var cursor: Cursor
cursor = this.connection.rawQuery("PRAGMA journal_mode = DELETE", null) cursor = this.connection.rawQuery("PRAGMA journal_mode = DELETE", null)

View file

@ -16,6 +16,7 @@
package org.y20k.trackbook package org.y20k.trackbook
import android.graphics.Color
import java.util.* import java.util.*
/* /*
@ -109,4 +110,7 @@ object Keys {
// notification // notification
const val TRACKER_SERVICE_NOTIFICATION_ID: Int = 1 const val TRACKER_SERVICE_NOTIFICATION_ID: Int = 1
const val NOTIFICATION_CHANNEL_RECORDING: String = "notificationChannelIdRecordingChannel" 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.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar 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.events.MapEventsReceiver
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint 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.Overlay
import org.osmdroid.views.overlay.OverlayItem import org.osmdroid.views.overlay.OverlayItem
import org.osmdroid.views.overlay.Polygon import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.TilesOverlay import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.compass.CompassOverlay import org.osmdroid.views.overlay.compass.CompassOverlay
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
import org.y20k.trackbook.helpers.* import org.y20k.trackbook.helpers.*
/*
* MapFragment class
*/
class MapFragment : Fragment() class MapFragment : Fragment()
{ {
/* Main class variables */ private lateinit var trackbook: Trackbook
private var bound: Boolean = false 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 trackingState: Int = Keys.STATE_TRACKING_STOPPED
private var gpsProviderActive: Boolean = false private var gpsProviderActive: Boolean = false
private var networkProviderActive: Boolean = false private var networkProviderActive: Boolean = false
private lateinit var currentBestLocation: Location 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 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_in_button: FloatingActionButton
lateinit var zoom_out_button: FloatingActionButton lateinit var zoom_out_button: FloatingActionButton
lateinit var mainButton: ExtendedFloatingActionButton lateinit var currentLocationButton: FloatingActionButton
private lateinit var mapView: MapView private var current_track_overlay: Polyline? = null
private var current_position_overlays = ArrayList<Overlay>() 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 var homepoints_overlays = ArrayList<Overlay>()
private lateinit var database_changed_listener: DatabaseChangedListener private lateinit var locationErrorBar: Snackbar
/* Overrides onCreate from Fragment */ /* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?) override fun onCreate(savedInstanceState: Bundle?)
{ {
Log.i("VOUSSOIR", "MapFragment.onCreate") Log.i("VOUSSOIR", "MapFragment.onCreate")
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
thismapfragment = this
this.trackbook = (requireContext().applicationContext as Trackbook) this.trackbook = (requireContext().applicationContext as Trackbook)
database_changed_listener = object: DatabaseChangedListener database_changed_listener = object: DatabaseChangedListener
{ {
@ -99,7 +97,7 @@ class MapFragment : Fragment()
Log.i("VOUSSOIR", "MapFragment database_ready_changed to ${trackbook.database.ready}") Log.i("VOUSSOIR", "MapFragment database_ready_changed to ${trackbook.database.ready}")
if (trackbook.database.ready) if (trackbook.database.ready)
{ {
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) create_homepoint_overlays()
} }
else else
{ {
@ -116,7 +114,7 @@ class MapFragment : Fragment()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
{ {
Log.i("VOUSSOIR", "MapFragment.onCreateView") Log.i("VOUSSOIR", "MapFragment.onCreateView")
// find views
rootView = inflater.inflate(R.layout.fragment_map, container, false) rootView = inflater.inflate(R.layout.fragment_map, container, false)
mapView = rootView.findViewById(R.id.map) mapView = rootView.findViewById(R.id.map)
currentLocationButton = rootView.findViewById(R.id.location_button) currentLocationButton = rootView.findViewById(R.id.location_button)
@ -130,8 +128,6 @@ class MapFragment : Fragment()
true true
} }
mapView.isLongClickable = true mapView.isLongClickable = true
// basic map setup
mapView.isTilesScaledToDpi = true mapView.isTilesScaledToDpi = true
mapView.isVerticalMapRepetitionEnabled = false mapView.isVerticalMapRepetitionEnabled = false
mapView.setTileSource(TileSourceFactory.MAPNIK) mapView.setTileSource(TileSourceFactory.MAPNIK)
@ -145,10 +141,8 @@ class MapFragment : Fragment()
} }
val densityScalingFactor: Float = UiHelper.getDensityScalingFactor(requireContext()) val densityScalingFactor: Float = UiHelper.getDensityScalingFactor(requireContext())
val compassOverlay = CompassOverlay(requireContext(), InternalCompassOrientationProvider(requireContext()), mapView) val compassOverlay = CompassOverlay(requireContext(), InternalCompassOrientationProvider(requireContext()), mapView)
compassOverlay.enableCompass() compassOverlay.enableCompass()
// compassOverlay.setCompassCenter(36f, 36f + (statusBarHeight / densityScalingFactor)) // TODO uncomment when transparent status bar is re-implemented
val screen_width = Resources.getSystem().displayMetrics.widthPixels val screen_width = Resources.getSystem().displayMetrics.widthPixels
compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f) compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f)
mapView.overlays.add(compassOverlay) mapView.overlays.add(compassOverlay)
@ -187,7 +181,7 @@ class MapFragment : Fragment()
radius=radius, radius=radius,
) )
trackbook.load_homepoints() trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) create_homepoint_overlays()
dialog.dismiss() dialog.dismiss()
} }
@ -198,28 +192,34 @@ class MapFragment : Fragment()
mapView.overlays.add(MapEventsOverlay(receiver)) mapView.overlays.add(MapEventsOverlay(receiver))
trackbook.load_homepoints() trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) create_homepoint_overlays()
if (database_changed_listener !in trackbook.database_changed_listeners) if (database_changed_listener !in trackbook.database_changed_listeners)
{ {
trackbook.database_changed_listeners.add(database_changed_listener) trackbook.database_changed_listeners.add(database_changed_listener)
} }
create_current_position_overlays(currentBestLocation, trackingState)
centerMap(currentBestLocation) centerMap(currentBestLocation)
// initialize track overlays current_track_overlay = null
currentTrackOverlay = null
// initialize main button state
update_main_button()
mapView.setOnTouchListener { v, event -> mapView.setOnTouchListener { v, event ->
continuous_auto_center = false continuous_auto_center = false
false false
} }
// set up buttons update_main_button()
mainButton.setOnClickListener { mainButton.setOnClickListener {
handleTrackingManagementMenu() if (trackingState == Keys.STATE_TRACKING_ACTIVE)
{
trackerService.stopTracking()
}
else
{
startTracking()
}
handler.post(location_update_redraw)
} }
currentLocationButton.setOnClickListener { currentLocationButton.setOnClickListener {
centerMap(currentBestLocation, animated=true) centerMap(currentBestLocation, animated=true)
@ -238,6 +238,7 @@ class MapFragment : Fragment()
/* Overrides onStart from Fragment */ /* Overrides onStart from Fragment */
override fun onStart() override fun onStart()
{ {
Log.i("VOUSSOIR", "MapFragment.onStart")
super.onStart() super.onStart()
// request location permission if denied // request location permission if denied
if (ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_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") Log.i("VOUSSOIR", "MapFragment.onPause")
super.onPause() super.onPause()
if (::trackerService.isInitialized)
{
trackerService.mapfragment = null
}
saveBestLocationState(currentBestLocation) saveBestLocationState(currentBestLocation)
if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) { if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) {
trackerService.removeGpsLocationListener() trackerService.removeGpsLocationListener()
@ -278,6 +283,10 @@ class MapFragment : Fragment()
override fun onStop() override fun onStop()
{ {
super.onStop() super.onStop()
if (::trackerService.isInitialized)
{
trackerService.mapfragment = null
}
// unbind from TrackerService // unbind from TrackerService
if (bound) if (bound)
{ {
@ -290,6 +299,7 @@ class MapFragment : Fragment()
{ {
Log.i("VOUSSOIR", "MapFragment.onDestroy") Log.i("VOUSSOIR", "MapFragment.onDestroy")
super.onDestroy() super.onDestroy()
trackerService.mapfragment = null
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (database_changed_listener in trackbook.database_changed_listeners) if (database_changed_listener in trackbook.database_changed_listeners)
@ -314,14 +324,17 @@ class MapFragment : Fragment()
toggleLocationErrorBar(gpsProviderActive, networkProviderActive) toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
} }
/* Start recording waypoints */ private fun startTracking()
private fun startTracking() { {
// start service via intent so that it keeps running after unbind // start service via intent so that it keeps running after unbind
val intent = Intent(activity, TrackerService::class.java) 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 // ... start service in foreground to prevent it being killed on Oreo
activity?.startForegroundService(intent) activity?.startForegroundService(intent)
} else { }
else
{
activity?.startService(intent) activity?.startService(intent)
} }
trackerService.startTracking() trackerService.startTracking()
@ -334,16 +347,9 @@ class MapFragment : Fragment()
bound = false bound = false
// unregister listener for changes in shared preferences // unregister listener for changes in shared preferences
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener) PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
// stop receiving location updates if (::trackerService.isInitialized)
handler.removeCallbacks(periodicLocationRequestRunnable) {
} trackerService.mapfragment = null
/* 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()
} }
} }
@ -374,7 +380,8 @@ class MapFragment : Fragment()
continuous_auto_center = true continuous_auto_center = true
} }
fun saveBestLocationState(currentBestLocation: Location) { fun saveBestLocationState(currentBestLocation: Location)
{
PreferencesHelper.saveCurrentBestLocation(currentBestLocation) PreferencesHelper.saveCurrentBestLocation(currentBestLocation)
PreferencesHelper.saveZoomLevel(mapView.zoomLevelDouble) PreferencesHelper.saveZoomLevel(mapView.zoomLevelDouble)
continuous_auto_center = true continuous_auto_center = true
@ -395,38 +402,26 @@ class MapFragment : Fragment()
/* Mark current position on map */ /* Mark current position on map */
fun create_current_position_overlays(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED) fun create_current_position_overlays(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED)
{ {
// Log.i("VOUSSOIR", "MapFragmentLayoutHolder.markCurrentPosition")
clear_current_position_overlays() clear_current_position_overlays()
val locationIsOld: Boolean = !(isRecentEnough(location)) val locationIsOld: Boolean = !(isRecentEnough(location))
// create marker
val newMarker: Drawable val newMarker: Drawable
val fillcolor: Int 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) fillcolor = Color.argb(64, 220, 61, 51)
if (locationIsOld) newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_red_24dp)!!
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!!
}
else
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_red_24dp)!!
}
} }
else else
{ {
fillcolor = Color.argb(64, 60, 152, 219) fillcolor = Color.argb(64, 60, 152, 219)
if(locationIsOld) newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_blue_24dp)!!
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_black_24dp)!!
}
else
{
newMarker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_marker_location_blue_24dp)!!
}
} }
val current_location_radius = Polygon() 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() fun clear_homepoint_overlays()
{ {
for (ov in homepoints_overlays) for (ov in homepoints_overlays)
@ -459,15 +469,16 @@ class MapFragment : Fragment()
homepoints_overlays.clear() homepoints_overlays.clear()
} }
fun create_homepoint_overlays(context: Context, map_view: MapView, homepoints: List<Homepoint>) fun create_homepoint_overlays()
{ {
Log.i("VOUSSOIR", "MapFragmentLayoutHolder.createHomepointOverlays") Log.i("VOUSSOIR", "MapFragmentLayoutHolder.createHomepointOverlays")
val context = requireContext()
val newMarker: Drawable = ContextCompat.getDrawable(context, R.drawable.ic_homepoint_24dp)!! val newMarker: Drawable = ContextCompat.getDrawable(context, R.drawable.ic_homepoint_24dp)!!
clear_homepoint_overlays() clear_homepoint_overlays()
for (homepoint in homepoints) for (homepoint in trackbook.homepoints)
{ {
val p = Polygon() val p = Polygon()
p.points = Polygon.pointsAsCircle(GeoPoint(homepoint.location.latitude, homepoint.location.longitude), homepoint.location.accuracy.toDouble()) 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) homepoints_overlays.add(p)
val overlayItems: java.util.ArrayList<OverlayItem> = java.util.ArrayList<OverlayItem>() 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) overlayItem.setMarker(newMarker)
overlayItems.add(overlayItem) overlayItems.add(overlayItem)
val homepoint_overlay = ItemizedIconOverlay<OverlayItem>(context, overlayItems, val homepoint_overlay = ItemizedIconOverlay<OverlayItem>(context, overlayItems,
@ -504,14 +522,14 @@ class MapFragment : Fragment()
delete_button.setOnClickListener { delete_button.setOnClickListener {
trackbook.database.delete_homepoint(homepoint.id) trackbook.database.delete_homepoint(homepoint.id)
trackbook.load_homepoints() trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) create_homepoint_overlays()
dialog.dismiss() dialog.dismiss()
} }
save_button.setOnClickListener { save_button.setOnClickListener {
val radius = radius_input.text.toString().toDoubleOrNull() ?: 25.0 val radius = radius_input.text.toString().toDoubleOrNull() ?: 25.0
trackbook.database.update_homepoint(homepoint.id, name=name_input.text.toString(), radius=radius) trackbook.database.update_homepoint(homepoint.id, name=name_input.text.toString(), radius=radius)
trackbook.load_homepoints() trackbook.load_homepoints()
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints) create_homepoint_overlays()
dialog.dismiss() 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() fun update_main_button()
{ {
mainButton.isEnabled = trackbook.database.ready mainButton.isEnabled = trackbook.database.ready
@ -592,14 +597,13 @@ class MapFragment : Fragment()
// get reference to tracker service // get reference to tracker service
val binder = service as TrackerService.LocalBinder val binder = service as TrackerService.LocalBinder
trackerService = binder.service trackerService = binder.service
trackerService.mapfragment = thismapfragment
// get state of tracking and update button if necessary // get state of tracking and update button if necessary
trackingState = trackerService.trackingState trackingState = trackerService.trackingState
update_main_button() update_main_button()
// register listener for changes in shared preferences // register listener for changes in shared preferences
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener) PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
// start listening for location updates // start listening for location updates
handler.removeCallbacks(periodicLocationRequestRunnable)
handler.postDelayed(periodicLocationRequestRunnable, 0)
} }
override fun onServiceDisconnected(arg0: ComponentName) 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() override fun run()
{ {
Log.i("VOUSSOIR", "MapFragment.location_update_redraw")
currentBestLocation = trackerService.currentBestLocation currentBestLocation = trackerService.currentBestLocation
gpsProviderActive = trackerService.gpsProviderActive gpsProviderActive = trackerService.gpsProviderActive
networkProviderActive = trackerService.networkProviderActive networkProviderActive = trackerService.networkProviderActive
trackingState = trackerService.trackingState trackingState = trackerService.trackingState
// update location and track
create_current_position_overlays(currentBestLocation, trackingState) create_current_position_overlays(currentBestLocation, trackingState)
create_current_track_overlay(trackerService.recent_trackpoints_for_mapview, trackingState) if (current_track_overlay == null)
// center map, if it had not been dragged/zoomed before {
create_track_overlay()
}
current_track_overlay!!.setPoints(trackerService.recent_trackpoints_for_mapview)
if (continuous_auto_center) if (continuous_auto_center)
{ {
centerMap(currentBestLocation, animated=false) 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.content.Context
import android.database.Cursor import android.database.Cursor
import android.database.DatabaseUtils.dumpCursorToString
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import org.y20k.trackbook.helpers.iso8601_format import org.y20k.trackbook.helpers.iso8601
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -33,7 +34,7 @@ data class Track (
var start_time: Date, var start_time: Date,
var end_time: Date, var end_time: Date,
var name: String = "", var name: String = "",
val trkpts: ArrayDeque<Trkpt> = ArrayDeque<Trkpt>(), val trkpts: ArrayList<Trkpt> = ArrayList<Trkpt>(),
var view_latitude: Double = Keys.DEFAULT_LATITUDE, var view_latitude: Double = Keys.DEFAULT_LATITUDE,
var view_longitude: Double = Keys.DEFAULT_LONGITUDE, var view_longitude: Double = Keys.DEFAULT_LONGITUDE,
var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
@ -73,7 +74,7 @@ data class Track (
> >
""".trimIndent()) """.trimIndent())
write("\t<metadata>") 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\t<device>${this.device_id}</device>")
write("\t</metadata>") write("\t</metadata>")
@ -81,7 +82,6 @@ data class Track (
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
dateFormat.timeZone = TimeZone.getTimeZone("UTC") dateFormat.timeZone = TimeZone.getTimeZone("UTC")
write("\t<trk>") write("\t<trk>")
write("\t\t<name>${this.name}</name>")
write("\t\t<trkseg>") write("\t\t<trkseg>")
var previous: Trkpt? = null 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<trkpt lat=\"${trkpt.latitude}\" lon=\"${trkpt.longitude}\">")
write("\t\t\t\t<ele>${trkpt.altitude}</ele>") 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\t<sat>${trkpt.numberSatellites}</sat>")
write("\t\t\t</trkpt>") write("\t\t\t</trkpt>")
previous = trkpt previous = trkpt
@ -170,11 +171,11 @@ data class Track (
fun trkpt_generator() = iterator<Trkpt> 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", "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()) 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_LAT = cursor.getColumnIndex("lat")
val COLUMN_LON = cursor.getColumnIndex("lon") val COLUMN_LON = cursor.getColumnIndex("lon")
val COLUMN_ELE = cursor.getColumnIndex("ele") val COLUMN_ELE = cursor.getColumnIndex("ele")

View file

@ -20,6 +20,7 @@ import YesNoDialog
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Paint
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -48,32 +49,38 @@ import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.ItemizedIconOverlay import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.OverlayItem
import org.osmdroid.views.overlay.TilesOverlay import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay 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.AppThemeHelper
import org.y20k.trackbook.helpers.DateTimeHelper import org.y20k.trackbook.helpers.DateTimeHelper
import org.y20k.trackbook.helpers.LengthUnitHelper import org.y20k.trackbook.helpers.LengthUnitHelper
import org.y20k.trackbook.helpers.PreferencesHelper 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.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.text.SimpleDateFormat
import java.util.* import java.util.*
class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
{ {
private lateinit var trackbook: Trackbook
lateinit var rootView: View lateinit var rootView: View
lateinit var save_track_button: ImageButton lateinit var save_track_button: ImageButton
lateinit var deleteButton: ImageButton lateinit var deleteButton: ImageButton
lateinit var zoom_in_button: FloatingActionButton lateinit var zoom_in_button: FloatingActionButton
lateinit var zoom_out_button: FloatingActionButton lateinit var zoom_out_button: FloatingActionButton
lateinit var trackNameView: MaterialTextView lateinit var trackNameView: MaterialTextView
lateinit var selected_trkpt_info: MaterialTextView
lateinit var track_query_start_date: DatePicker lateinit var track_query_start_date: DatePicker
lateinit var track_query_start_time: TimePicker lateinit var track_query_start_time: TimePicker
lateinit var track_query_end_date: DatePicker lateinit var track_query_end_date: DatePicker
lateinit var track_query_end_time: TimePicker 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_start_time_previous: Int = 0
var track_query_end_time_previous: Int = 0 var track_query_end_time_previous: Int = 0
private lateinit var mapView: MapView private lateinit var mapView: MapView
@ -95,22 +102,25 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
private lateinit var negativeElevationView: MaterialTextView private lateinit var negativeElevationView: MaterialTextView
private lateinit var elevationDataViews: Group private lateinit var elevationDataViews: Group
private lateinit var track: Track 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 var useImperialUnits: Boolean = false
private val handler: Handler = Handler(Looper.getMainLooper()) 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 val RERENDER_DELAY: Long = 1000
/* Overrides onCreateView from Fragment */ /* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
{ {
this.trackbook = (requireContext().applicationContext as Trackbook)
val database: Database = (requireActivity().applicationContext as Trackbook).database val database: Database = (requireActivity().applicationContext as Trackbook).database
track = Track( track = Track(
database=database, database=database,
name=this.requireArguments().getString(Keys.ARG_TRACK_TITLE, ""), name=this.requireArguments().getString(Keys.ARG_TRACK_TITLE, ""),
device_id= this.requireArguments().getString(Keys.ARG_TRACK_DEVICE_ID, ""), device_id= this.requireArguments().getString(Keys.ARG_TRACK_DEVICE_ID, ""),
start_time= iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!), start_time=iso8601_parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!),
end_time=iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!), end_time=iso8601_parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!),
) )
track.load_trkpts() track.load_trkpts()
rootView = inflater.inflate(R.layout.fragment_track, container, false) 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.setCenter(GeoPoint(track.view_latitude, track.view_longitude))
controller.setZoom(Keys.DEFAULT_ZOOM_LEVEL) controller.setZoom(Keys.DEFAULT_ZOOM_LEVEL)
// trkpt_infowindow = MarkerInfoWindow(R.layout.trkpt_infowindow, mapView)
statisticsSheet = rootView.findViewById(R.id.statistics_sheet) statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
statisticsView = rootView.findViewById(R.id.statistics_view) statisticsView = rootView.findViewById(R.id.statistics_view)
distanceView = rootView.findViewById(R.id.statistics_data_distance) 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 { save_track_button.setOnClickListener {
openSaveGpxDialog() openSaveGpxDialog()
} }
@ -251,6 +284,7 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
statisticsSheetBehavior = BottomSheetBehavior.from<View>(statisticsSheet) statisticsSheetBehavior = BottomSheetBehavior.from<View>(statisticsSheet)
statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
track_segment_overlays = ArrayDeque<Polyline>(10)
render_track() render_track()
return rootView return rootView
@ -265,25 +299,81 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
fun render_track() fun render_track()
{ {
Log.i("VOUSSOIR", "TrackFragment.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) Log.i("VOUSSOIR", "MapOverlayHelper.createTrackOverlay")
{ track_geopoints = mutableListOf()
mapView.overlays.remove(track_overlay)
}
val geopoints: MutableList<IGeoPoint> = mutableListOf()
for (trkpt in track.trkpts) 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) if (previous_time > 0 && (trkpt.time - previous_time) > Keys.STOP_OVER_THRESHOLD)
special_points_overlay = create_start_end_markers(requireContext(), mapView, track.trkpts) {
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 fun get_datetime(datepicker: DatePicker, timepicker: TimePicker, seconds: Int): Date
@ -348,7 +438,16 @@ class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
/* Overrides onZoom from MapListener */ /* Overrides onZoom from MapListener */
override fun onZoom(event: ZoomEvent?): Boolean 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 */ /* Overrides onScroll from MapListener */

View file

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

View file

@ -33,7 +33,7 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import org.y20k.trackbook.helpers.UiHelper 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 import org.y20k.trackbook.tracklist.TracklistAdapter
/* /*
@ -46,7 +46,7 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
private lateinit var trackElementList: RecyclerView private lateinit var trackElementList: RecyclerView
private lateinit var tracklistOnboarding: ConstraintLayout private lateinit var tracklistOnboarding: ConstraintLayout
/* Overrides onCreateView from Fragment */ /* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?) override fun onCreate(savedInstanceState: Bundle?)
{ {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -67,18 +67,6 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
trackElementList.itemAnimator = DefaultItemAnimator() trackElementList.itemAnimator = DefaultItemAnimator()
trackElementList.adapter = tracklistAdapter 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 // toggle onboarding layout
toggleOnboardingLayout() toggleOnboardingLayout()
@ -90,40 +78,15 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
val bundle: Bundle = bundleOf( val bundle: Bundle = bundleOf(
Keys.ARG_TRACK_TITLE to track.name, Keys.ARG_TRACK_TITLE to track.name,
Keys.ARG_TRACK_DEVICE_ID to track.device_id, Keys.ARG_TRACK_DEVICE_ID to track.device_id,
Keys.ARG_TRACK_START_TIME to iso8601_format.format(track.start_time), Keys.ARG_TRACK_START_TIME to iso8601(track.start_time),
Keys.ARG_TRACK_STOP_TIME to iso8601_format.format(track.end_time), Keys.ARG_TRACK_STOP_TIME to iso8601(track.end_time),
) )
findNavController().navigate(R.id.fragment_track, bundle) 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 // toggle onboarding layout
private fun toggleOnboardingLayout() { private fun toggleOnboardingLayout()
{
when (tracklistAdapter.isEmpty()) { when (tracklistAdapter.isEmpty()) {
true -> { true -> {
// show onboarding layout // show onboarding layout
@ -143,11 +106,13 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
*/ */
inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false) inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false)
{ {
override fun supportsPredictiveItemAnimations(): Boolean { override fun supportsPredictiveItemAnimations(): Boolean
{
return true return true
} }
override fun onLayoutCompleted(state: RecyclerView.State?) { override fun onLayoutCompleted(state: RecyclerView.State?)
{
super.onLayoutCompleted(state) super.onLayoutCompleted(state)
// handle delete request from TrackFragment - after layout calculations are complete // handle delete request from TrackFragment - after layout calculations are complete
val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L 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 android.location.Location
import org.osmdroid.api.IGeoPoint import org.osmdroid.api.IGeoPoint
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
import org.osmdroid.views.overlay.Polyline
import org.y20k.trackbook.helpers.getNumberOfSatellites import org.y20k.trackbook.helpers.getNumberOfSatellites
class Trkpt( class Trkpt(
@ -30,6 +31,7 @@ class Trkpt(
val accuracy: Float, val accuracy: Float,
val time: Long, val time: Long,
val numberSatellites: Int = 0, val numberSatellites: Int = 0,
var rendered_by_polyline: Polyline? = null
) : GeoPoint(latitude, longitude, altitude) ) : GeoPoint(latitude, longitude, altitude)
{ {
constructor(device_id: String, location: Location) : this( constructor(device_id: String, location: Location) : this(

View file

@ -15,12 +15,25 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.random.Random.Default.nextBits import kotlin.random.Random.Default.nextBits
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
private val RNG = SecureRandom() 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 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 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 { 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)}" 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 package org.y20k.trackbook.helpers
import android.content.Context import android.content.Context
import android.graphics.Paint
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.osmdroid.api.IGeoPoint
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.ItemizedIconOverlay import org.osmdroid.views.overlay.ItemizedIconOverlay
import org.osmdroid.views.overlay.OverlayItem 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.R
import org.y20k.trackbook.Trkpt import org.y20k.trackbook.Trkpt
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
fun createTrackOverlay(context: Context, map_view: MapView, geopoints: MutableList<IGeoPoint>, trackingState: Int): SimpleFastPointOverlay fun create_start_end_markers(context: Context, map_view: MapView, startpoint: Trkpt, endpoint: Trkpt): ItemizedIconOverlay<OverlayItem>?
{
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>?
{ {
Log.i("VOUSSOIR", "MapOverlayHelper.create_start_end_markers") Log.i("VOUSSOIR", "MapOverlayHelper.create_start_end_markers")
if (trkpts.size == 0)
{
return null
}
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>() 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) 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)!!) startmarker.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_blue_48dp)!!)
overlayItems.add(startmarker) 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) 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)!!) endmarker.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_blue_48dp)!!)

View file

@ -12,19 +12,42 @@
android:layout_height="match_parent" 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 <DatePicker
android:id="@+id/track_query_start_date" android:id="@+id/track_query_start_date"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="130dp" android:layout_height="130dp"
android:translationX="-45dp"
android:translationY="-30dp"
android:calendarViewShown="false" android:calendarViewShown="false"
android:datePickerMode="spinner" 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" app:layout_constraintStart_toStartOf="parent"
android:scaleX="0.5" app:layout_constraintTop_toTopOf="parent" />
android:scaleY="0.5"
/>
<TimePicker <TimePicker
android:id="@+id/track_query_start_time" android:id="@+id/track_query_start_time"
@ -35,8 +58,8 @@
android:timePickerMode="spinner" android:timePickerMode="spinner"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/track_query_start_date" app:layout_constraintStart_toEndOf="@+id/track_query_start_date"
android:scaleX="0.5" android:scaleX="0.6"
android:scaleY="0.5" android:scaleY="0.6"
/> />
<DatePicker <DatePicker
@ -49,8 +72,8 @@
android:calendarViewShown="false" android:calendarViewShown="false"
app:layout_constraintTop_toBottomOf="@+id/track_query_start_date" app:layout_constraintTop_toBottomOf="@+id/track_query_start_date"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:scaleX="0.5" android:scaleX="0.6"
android:scaleY="0.5" android:scaleY="0.6"
/> />
<TimePicker <TimePicker
@ -62,8 +85,8 @@
android:timePickerMode="spinner" android:timePickerMode="spinner"
app:layout_constraintTop_toBottomOf="@+id/track_query_start_time" app:layout_constraintTop_toBottomOf="@+id/track_query_start_time"
app:layout_constraintStart_toEndOf="@+id/track_query_start_date" app:layout_constraintStart_toEndOf="@+id/track_query_start_date"
android:scaleX="0.5" android:scaleX="0.6"
android:scaleY="0.5" android:scaleY="0.6"
/> />
<org.osmdroid.views.MapView <org.osmdroid.views.MapView