checkpoint
This commit is contained in:
parent
2de4e0bae5
commit
bbd23d8fb1
2 changed files with 288 additions and 331 deletions
|
@ -22,44 +22,211 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.DatePicker
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TimePicker
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
|
import androidx.constraintlayout.widget.Group
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.textview.MaterialTextView
|
||||||
|
import org.osmdroid.api.IMapController
|
||||||
|
import org.osmdroid.events.MapListener
|
||||||
|
import org.osmdroid.events.ScrollEvent
|
||||||
|
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.TilesOverlay
|
||||||
|
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
|
||||||
|
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.create_start_end_markers
|
||||||
import org.y20k.trackbook.helpers.iso8601_format
|
import org.y20k.trackbook.helpers.iso8601_format
|
||||||
import org.y20k.trackbook.ui.TrackFragmentLayoutHolder
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class TrackFragment : Fragment(), YesNoDialog.YesNoDialogListener
|
class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener
|
||||||
{
|
{
|
||||||
/* Main class variables */
|
lateinit var rootView: View
|
||||||
private lateinit var layout: TrackFragmentLayoutHolder
|
lateinit var save_track_button: ImageButton
|
||||||
|
lateinit var deleteButton: ImageButton
|
||||||
|
lateinit var trackNameView: 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
|
||||||
|
var track_query_start_time_previous: Int = 0
|
||||||
|
var track_query_end_time_previous: Int = 0
|
||||||
|
private lateinit var mapView: MapView
|
||||||
|
private lateinit var controller: IMapController
|
||||||
|
private lateinit var statisticsSheetBehavior: BottomSheetBehavior<View>
|
||||||
|
private lateinit var statisticsSheet: NestedScrollView
|
||||||
|
private lateinit var statisticsView: View
|
||||||
|
private lateinit var distanceView: MaterialTextView
|
||||||
|
private lateinit var waypointsView: MaterialTextView
|
||||||
|
private lateinit var durationView: MaterialTextView
|
||||||
|
private lateinit var velocityView: MaterialTextView
|
||||||
|
private lateinit var recordingStartView: MaterialTextView
|
||||||
|
private lateinit var recordingStopView: MaterialTextView
|
||||||
|
private lateinit var recordingPausedView: MaterialTextView
|
||||||
|
private lateinit var recordingPausedLabelView: MaterialTextView
|
||||||
|
private lateinit var maxAltitudeView: MaterialTextView
|
||||||
|
private lateinit var minAltitudeView: MaterialTextView
|
||||||
|
private lateinit var positiveElevationView: MaterialTextView
|
||||||
|
private lateinit var negativeElevationView: MaterialTextView
|
||||||
|
private lateinit var elevationDataViews: Group
|
||||||
|
private lateinit var track: Track
|
||||||
|
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 */
|
/* Overrides onCreateView from Fragment */
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
// initialize layout
|
// initialize layout
|
||||||
val database: Database = (requireActivity().applicationContext as Trackbook).database
|
val database: Database = (requireActivity().applicationContext as Trackbook).database
|
||||||
val 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_format.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_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!),
|
||||||
)
|
)
|
||||||
layout = TrackFragmentLayoutHolder(activity as Context, inflater, container, track)
|
rootView = inflater.inflate(R.layout.fragment_track, container, false)
|
||||||
|
mapView = rootView.findViewById(R.id.map)
|
||||||
|
save_track_button = rootView.findViewById(R.id.save_button)
|
||||||
|
deleteButton = rootView.findViewById(R.id.delete_button)
|
||||||
|
trackNameView = rootView.findViewById(R.id.statistics_track_name_headline)
|
||||||
|
|
||||||
// set up share button
|
controller = mapView.controller
|
||||||
layout.save_track_button.setOnClickListener {
|
mapView.addMapListener(this)
|
||||||
|
mapView.isTilesScaledToDpi = true
|
||||||
|
mapView.setTileSource(TileSourceFactory.MAPNIK)
|
||||||
|
mapView.isVerticalMapRepetitionEnabled = false
|
||||||
|
mapView.setMultiTouchControls(true)
|
||||||
|
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||||
|
controller.setCenter(GeoPoint(track.view_latitude, track.view_longitude))
|
||||||
|
controller.setZoom(Keys.DEFAULT_ZOOM_LEVEL)
|
||||||
|
|
||||||
|
statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
|
||||||
|
statisticsView = rootView.findViewById(R.id.statistics_view)
|
||||||
|
distanceView = rootView.findViewById(R.id.statistics_data_distance)
|
||||||
|
waypointsView = rootView.findViewById(R.id.statistics_data_waypoints)
|
||||||
|
durationView = rootView.findViewById(R.id.statistics_data_duration)
|
||||||
|
velocityView = rootView.findViewById(R.id.statistics_data_velocity)
|
||||||
|
recordingStartView = rootView.findViewById(R.id.statistics_data_recording_start)
|
||||||
|
recordingStopView = rootView.findViewById(R.id.statistics_data_recording_stop)
|
||||||
|
recordingPausedLabelView = rootView.findViewById(R.id.statistics_p_recording_paused)
|
||||||
|
recordingPausedView = rootView.findViewById(R.id.statistics_data_recording_paused)
|
||||||
|
maxAltitudeView = rootView.findViewById(R.id.statistics_data_max_altitude)
|
||||||
|
minAltitudeView = rootView.findViewById(R.id.statistics_data_min_altitude)
|
||||||
|
positiveElevationView = rootView.findViewById(R.id.statistics_data_positive_elevation)
|
||||||
|
negativeElevationView = rootView.findViewById(R.id.statistics_data_negative_elevation)
|
||||||
|
elevationDataViews = rootView.findViewById(R.id.elevation_data)
|
||||||
|
|
||||||
|
useImperialUnits = PreferencesHelper.loadUseImperialUnits()
|
||||||
|
|
||||||
|
if (AppThemeHelper.isDarkModeOn(context as Activity))
|
||||||
|
{
|
||||||
|
mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
|
||||||
|
}
|
||||||
|
|
||||||
|
track.load_trkpts()
|
||||||
|
val actual_start_time: Date = if (track.trkpts.isEmpty()) track.start_time else Date(track.trkpts.first().time)
|
||||||
|
val actual_end_time: Date = if (track.trkpts.isEmpty()) track.end_time else Date(track.trkpts.last().time)
|
||||||
|
|
||||||
|
track_query_start_date = rootView.findViewById(R.id.track_query_start_date)
|
||||||
|
val start_cal = GregorianCalendar()
|
||||||
|
start_cal.time = actual_start_time
|
||||||
|
track_query_start_date.init(start_cal.get(Calendar.YEAR), start_cal.get(Calendar.MONTH), start_cal.get(Calendar.DAY_OF_MONTH), object: DatePicker.OnDateChangedListener {
|
||||||
|
override fun onDateChanged(p0: DatePicker?, p1: Int, p2: Int, p3: Int)
|
||||||
|
{
|
||||||
|
handler.removeCallbacks(requery_and_render)
|
||||||
|
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
track_query_start_time = rootView.findViewById(R.id.track_query_start_time)
|
||||||
|
track_query_start_time.setIs24HourView(true)
|
||||||
|
track_query_start_time.hour = actual_start_time.hours
|
||||||
|
track_query_start_time.minute = actual_start_time.minutes
|
||||||
|
track_query_start_time_previous = (actual_start_time.hours * 60) + actual_start_time.minutes
|
||||||
|
track_query_start_time.setOnTimeChangedListener(object : TimePicker.OnTimeChangedListener{
|
||||||
|
override fun onTimeChanged(p0: TimePicker?, p1: Int, p2: Int)
|
||||||
|
{
|
||||||
|
handler.removeCallbacks(requery_and_render)
|
||||||
|
val newminute = (p1 * 60) + p2
|
||||||
|
Log.i("VOUSSOIR", "End time changed $newminute")
|
||||||
|
if (newminute < track_query_start_time_previous && (track_query_start_time_previous - newminute > 60))
|
||||||
|
{
|
||||||
|
increment_datepicker(track_query_start_date)
|
||||||
|
}
|
||||||
|
else if (newminute > track_query_start_time_previous && (newminute - track_query_start_time_previous > 60))
|
||||||
|
{
|
||||||
|
decrement_datepicker(track_query_start_date)
|
||||||
|
}
|
||||||
|
track_query_start_time_previous = newminute
|
||||||
|
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
track_query_end_date = rootView.findViewById(R.id.track_query_end_date)
|
||||||
|
val end_cal = GregorianCalendar()
|
||||||
|
end_cal.time = actual_end_time
|
||||||
|
track_query_end_date.init(end_cal.get(Calendar.YEAR), end_cal.get(Calendar.MONTH), end_cal.get(Calendar.DAY_OF_MONTH), object: DatePicker.OnDateChangedListener {
|
||||||
|
override fun onDateChanged(p0: DatePicker?, p1: Int, p2: Int, p3: Int)
|
||||||
|
{
|
||||||
|
handler.removeCallbacks(requery_and_render)
|
||||||
|
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
track_query_end_time = rootView.findViewById(R.id.track_query_end_time)
|
||||||
|
track_query_end_time.setIs24HourView(true)
|
||||||
|
track_query_end_time.hour = actual_end_time.hours
|
||||||
|
track_query_end_time.minute = actual_end_time.minutes
|
||||||
|
track_query_end_time_previous = (actual_end_time.hours * 60) + actual_end_time.minutes
|
||||||
|
track_query_end_time.setOnTimeChangedListener(object : TimePicker.OnTimeChangedListener{
|
||||||
|
override fun onTimeChanged(p0: TimePicker?, p1: Int, p2: Int)
|
||||||
|
{
|
||||||
|
handler.removeCallbacks(requery_and_render)
|
||||||
|
val newminute = (p1 * 60) + p2
|
||||||
|
Log.i("VOUSSOIR", "End time changed $newminute")
|
||||||
|
if (newminute < track_query_end_time_previous && (track_query_end_time_previous - newminute > 60))
|
||||||
|
{
|
||||||
|
increment_datepicker(track_query_end_date)
|
||||||
|
}
|
||||||
|
else if (newminute > track_query_end_time_previous && (newminute - track_query_end_time_previous > 60))
|
||||||
|
{
|
||||||
|
decrement_datepicker(track_query_end_date)
|
||||||
|
}
|
||||||
|
track_query_end_time_previous = newminute
|
||||||
|
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
save_track_button.setOnClickListener {
|
||||||
openSaveGpxDialog()
|
openSaveGpxDialog()
|
||||||
}
|
}
|
||||||
// set up delete button
|
|
||||||
layout.deleteButton.setOnClickListener {
|
deleteButton.setOnClickListener {
|
||||||
val dialogMessage = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n${layout.trackNameView.text}"
|
val dialogMessage = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n${trackNameView.text}"
|
||||||
YesNoDialog(this@TrackFragment as YesNoDialog.YesNoDialogListener).show(
|
YesNoDialog(this@TrackFragment as YesNoDialog.YesNoDialogListener).show(
|
||||||
context = activity as Context,
|
context = activity as Context,
|
||||||
type = Keys.DIALOG_DELETE_TRACK,
|
type = Keys.DIALOG_DELETE_TRACK,
|
||||||
|
@ -68,7 +235,12 @@ class TrackFragment : Fragment(), YesNoDialog.YesNoDialogListener
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return layout.rootView
|
statisticsSheetBehavior = BottomSheetBehavior.from<View>(statisticsSheet)
|
||||||
|
statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
|
||||||
|
render_track()
|
||||||
|
|
||||||
|
return rootView
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overrides onResume from Fragment */
|
/* Overrides onResume from Fragment */
|
||||||
|
@ -77,6 +249,108 @@ class TrackFragment : Fragment(), YesNoDialog.YesNoDialogListener
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun render_track()
|
||||||
|
{
|
||||||
|
if (special_points_overlay != null)
|
||||||
|
{
|
||||||
|
mapView.overlays.remove(special_points_overlay)
|
||||||
|
}
|
||||||
|
if (track_overlay != null)
|
||||||
|
{
|
||||||
|
mapView.overlays.remove(track_overlay)
|
||||||
|
}
|
||||||
|
if (track.trkpts.isNotEmpty())
|
||||||
|
{
|
||||||
|
track_overlay = createTrackOverlay(requireContext(), mapView, track.trkpts, Keys.STATE_TRACKING_STOPPED)
|
||||||
|
special_points_overlay = create_start_end_markers(requireContext(), mapView, track.trkpts)
|
||||||
|
}
|
||||||
|
setupStatisticsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get_datetime(datepicker: DatePicker, timepicker: TimePicker, seconds: Int): Date
|
||||||
|
{
|
||||||
|
val cal = GregorianCalendar.getInstance()
|
||||||
|
cal.set(datepicker.year, datepicker.month, datepicker.dayOfMonth, timepicker.hour, timepicker.minute, seconds)
|
||||||
|
Log.i("VOUSSOIR", cal.time.toString())
|
||||||
|
return cal.time
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrement_datepicker(picker: DatePicker)
|
||||||
|
{
|
||||||
|
val cal = GregorianCalendar.getInstance()
|
||||||
|
cal.set(picker.year, picker.month, picker.dayOfMonth)
|
||||||
|
cal.add(Calendar.DATE, -1)
|
||||||
|
picker.updateDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun increment_datepicker(picker: DatePicker)
|
||||||
|
{
|
||||||
|
val cal = GregorianCalendar.getInstance()
|
||||||
|
cal.set(picker.year, picker.month, picker.dayOfMonth)
|
||||||
|
cal.add(Calendar.DATE, 1)
|
||||||
|
picker.updateDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupStatisticsViews()
|
||||||
|
{
|
||||||
|
val stats: TrackStatistics = track.statistics()
|
||||||
|
trackNameView.text = track.name
|
||||||
|
distanceView.text = LengthUnitHelper.convertDistanceToString(stats.distance, useImperialUnits)
|
||||||
|
waypointsView.text = track.trkpts.size.toString()
|
||||||
|
durationView.text = DateTimeHelper.convertToReadableTime(requireContext(), stats.duration)
|
||||||
|
velocityView.text = LengthUnitHelper.convertToVelocityString(stats.velocity, useImperialUnits)
|
||||||
|
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.start_time)
|
||||||
|
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.end_time)
|
||||||
|
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.max_altitude, useImperialUnits)
|
||||||
|
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.min_altitude, useImperialUnits)
|
||||||
|
positiveElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_ascent, useImperialUnits)
|
||||||
|
negativeElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_descent, useImperialUnits)
|
||||||
|
|
||||||
|
// inform user about possible accuracy issues with altitude measurements
|
||||||
|
elevationDataViews.referencedIds.forEach { id ->
|
||||||
|
(rootView.findViewById(id) as View).setOnClickListener{
|
||||||
|
Toast.makeText(context, R.string.toast_message_elevation_info, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// make track name on statistics sheet clickable
|
||||||
|
trackNameView.setOnClickListener {
|
||||||
|
toggleStatisticsSheetVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleStatisticsSheetVisibility()
|
||||||
|
{
|
||||||
|
when (statisticsSheetBehavior.state) {
|
||||||
|
BottomSheetBehavior.STATE_EXPANDED -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
else -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overrides onZoom from MapListener */
|
||||||
|
override fun onZoom(event: ZoomEvent?): Boolean
|
||||||
|
{
|
||||||
|
return (event != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overrides onScroll from MapListener */
|
||||||
|
override fun onScroll(event: ScrollEvent?): Boolean
|
||||||
|
{
|
||||||
|
return (event != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requery_and_render: Runnable = object : Runnable {
|
||||||
|
override fun run()
|
||||||
|
{
|
||||||
|
Log.i("VOUSSOIR", "requery_and_render")
|
||||||
|
track.start_time = get_datetime(track_query_start_date, track_query_start_time, seconds=0)
|
||||||
|
track.end_time = get_datetime(track_query_end_date, track_query_end_time, seconds=59)
|
||||||
|
track.load_trkpts()
|
||||||
|
Log.i("VOUSSOIR", "Reloaded ${track.trkpts.size} trkpts.")
|
||||||
|
render_track()
|
||||||
|
mapView.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Register the ActivityResultLauncher for saving GPX */
|
/* Register the ActivityResultLauncher for saving GPX */
|
||||||
private val requestSaveGpxLauncher = registerForActivityResult(StartActivityForResult(), this::requestSaveGpxResult)
|
private val requestSaveGpxLauncher = registerForActivityResult(StartActivityForResult(), this::requestSaveGpxResult)
|
||||||
|
|
||||||
|
@ -93,7 +367,7 @@ class TrackFragment : Fragment(), YesNoDialog.YesNoDialogListener
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val outputsuccess: Uri? = layout.track.export_gpx(activity as Context, targetUri)
|
val outputsuccess: Uri? = track.export_gpx(activity as Context, targetUri)
|
||||||
if (outputsuccess == null)
|
if (outputsuccess == null)
|
||||||
{
|
{
|
||||||
Toast.makeText(activity as Context, "failed to export for some reason", Toast.LENGTH_LONG).show()
|
Toast.makeText(activity as Context, "failed to export for some reason", Toast.LENGTH_LONG).show()
|
||||||
|
@ -126,7 +400,7 @@ class TrackFragment : Fragment(), YesNoDialog.YesNoDialogListener
|
||||||
/* Opens up a file picker to select the save location */
|
/* Opens up a file picker to select the save location */
|
||||||
private fun openSaveGpxDialog()
|
private fun openSaveGpxDialog()
|
||||||
{
|
{
|
||||||
val export_name: String = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(layout.track.start_time) + " " + layout.track.device_id + Keys.GPX_FILE_EXTENSION
|
val export_name: String = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(track.start_time) + " " + track.device_id + Keys.GPX_FILE_EXTENSION
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = Keys.MIME_TYPE_GPX
|
type = Keys.MIME_TYPE_GPX
|
||||||
|
|
|
@ -1,317 +0,0 @@
|
||||||
/*
|
|
||||||
* TrackFragmentLayoutHolder.kt
|
|
||||||
* Implements the TrackFragmentLayoutHolder class
|
|
||||||
* A TrackFragmentLayoutHolder hold references to the main views of a track fragment
|
|
||||||
*
|
|
||||||
* This file is part of
|
|
||||||
* TRACKBOOK - Movement Recorder for Android
|
|
||||||
*
|
|
||||||
* Copyright (c) 2016-22 - Y20K.org
|
|
||||||
* Licensed under the MIT-License
|
|
||||||
* http://opensource.org/licenses/MIT
|
|
||||||
*
|
|
||||||
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
|
|
||||||
* https://github.com/osmdroid/osmdroid
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.y20k.trackbook.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.DatePicker
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.TimePicker
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.constraintlayout.widget.Group
|
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import org.osmdroid.api.IMapController
|
|
||||||
import org.osmdroid.events.MapListener
|
|
||||||
import org.osmdroid.events.ScrollEvent
|
|
||||||
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.TilesOverlay
|
|
||||||
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
|
|
||||||
import org.y20k.trackbook.Keys
|
|
||||||
import org.y20k.trackbook.R
|
|
||||||
import org.y20k.trackbook.Track
|
|
||||||
import org.y20k.trackbook.TrackStatistics
|
|
||||||
import org.y20k.trackbook.helpers.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
data class TrackFragmentLayoutHolder(
|
|
||||||
private var context: Context,
|
|
||||||
private var inflater: LayoutInflater,
|
|
||||||
private var container: ViewGroup?,
|
|
||||||
var track: Track
|
|
||||||
): MapListener
|
|
||||||
{
|
|
||||||
val rootView: View
|
|
||||||
val save_track_button: ImageButton
|
|
||||||
val deleteButton: ImageButton
|
|
||||||
val trackNameView: MaterialTextView
|
|
||||||
val track_query_start_date: DatePicker
|
|
||||||
val track_query_start_time: TimePicker
|
|
||||||
val track_query_end_date: DatePicker
|
|
||||||
val track_query_end_time: TimePicker
|
|
||||||
var track_query_start_time_previous: Int
|
|
||||||
var track_query_end_time_previous: Int
|
|
||||||
private val mapView: MapView
|
|
||||||
private var controller: IMapController
|
|
||||||
private val statisticsSheetBehavior: BottomSheetBehavior<View>
|
|
||||||
private val statisticsSheet: NestedScrollView
|
|
||||||
private val statisticsView: View
|
|
||||||
private val distanceView: MaterialTextView
|
|
||||||
private val waypointsView: MaterialTextView
|
|
||||||
private val durationView: MaterialTextView
|
|
||||||
private val velocityView: MaterialTextView
|
|
||||||
private val recordingStartView: MaterialTextView
|
|
||||||
private val recordingStopView: MaterialTextView
|
|
||||||
private val recordingPausedView: MaterialTextView
|
|
||||||
private val recordingPausedLabelView: MaterialTextView
|
|
||||||
private val maxAltitudeView: MaterialTextView
|
|
||||||
private val minAltitudeView: MaterialTextView
|
|
||||||
private val positiveElevationView: MaterialTextView
|
|
||||||
private val negativeElevationView: MaterialTextView
|
|
||||||
private val elevationDataViews: Group
|
|
||||||
private val useImperialUnits: Boolean
|
|
||||||
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
|
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_track, container, false)
|
|
||||||
mapView = rootView.findViewById(R.id.map)
|
|
||||||
save_track_button = rootView.findViewById(R.id.save_button)
|
|
||||||
deleteButton = rootView.findViewById(R.id.delete_button)
|
|
||||||
trackNameView = rootView.findViewById(R.id.statistics_track_name_headline)
|
|
||||||
|
|
||||||
track.load_trkpts()
|
|
||||||
val actual_start_time: Date = if (track.trkpts.isEmpty()) track.start_time else Date(track.trkpts.first().time)
|
|
||||||
val actual_end_time: Date = if (track.trkpts.isEmpty()) track.end_time else Date(track.trkpts.last().time)
|
|
||||||
|
|
||||||
track_query_start_date = rootView.findViewById(R.id.track_query_start_date)
|
|
||||||
val start_cal = GregorianCalendar()
|
|
||||||
start_cal.time = actual_start_time
|
|
||||||
track_query_start_date.init(start_cal.get(Calendar.YEAR), start_cal.get(Calendar.MONTH), start_cal.get(Calendar.DAY_OF_MONTH), object: DatePicker.OnDateChangedListener {
|
|
||||||
override fun onDateChanged(p0: DatePicker?, p1: Int, p2: Int, p3: Int)
|
|
||||||
{
|
|
||||||
handler.removeCallbacks(requery_and_render)
|
|
||||||
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
track_query_start_time = rootView.findViewById(R.id.track_query_start_time)
|
|
||||||
track_query_start_time.setIs24HourView(true)
|
|
||||||
track_query_start_time.hour = actual_start_time.hours
|
|
||||||
track_query_start_time.minute = actual_start_time.minutes
|
|
||||||
track_query_start_time_previous = (actual_start_time.hours * 60) + actual_start_time.minutes
|
|
||||||
track_query_start_time.setOnTimeChangedListener(object : TimePicker.OnTimeChangedListener{
|
|
||||||
override fun onTimeChanged(p0: TimePicker?, p1: Int, p2: Int)
|
|
||||||
{
|
|
||||||
handler.removeCallbacks(requery_and_render)
|
|
||||||
val newminute = (p1 * 60) + p2
|
|
||||||
Log.i("VOUSSOIR", "End time changed $newminute")
|
|
||||||
if (newminute < track_query_start_time_previous && (track_query_start_time_previous - newminute > 60))
|
|
||||||
{
|
|
||||||
increment_datepicker(track_query_start_date)
|
|
||||||
}
|
|
||||||
else if (newminute > track_query_start_time_previous && (newminute - track_query_start_time_previous > 60))
|
|
||||||
{
|
|
||||||
decrement_datepicker(track_query_start_date)
|
|
||||||
}
|
|
||||||
track_query_start_time_previous = newminute
|
|
||||||
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
track_query_end_date = rootView.findViewById(R.id.track_query_end_date)
|
|
||||||
val end_cal = GregorianCalendar()
|
|
||||||
end_cal.time = actual_end_time
|
|
||||||
track_query_end_date.init(end_cal.get(Calendar.YEAR), end_cal.get(Calendar.MONTH), end_cal.get(Calendar.DAY_OF_MONTH), object: DatePicker.OnDateChangedListener {
|
|
||||||
override fun onDateChanged(p0: DatePicker?, p1: Int, p2: Int, p3: Int)
|
|
||||||
{
|
|
||||||
handler.removeCallbacks(requery_and_render)
|
|
||||||
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
track_query_end_time = rootView.findViewById(R.id.track_query_end_time)
|
|
||||||
track_query_end_time.setIs24HourView(true)
|
|
||||||
track_query_end_time.hour = actual_end_time.hours
|
|
||||||
track_query_end_time.minute = actual_end_time.minutes
|
|
||||||
track_query_end_time_previous = (actual_end_time.hours * 60) + actual_end_time.minutes
|
|
||||||
track_query_end_time.setOnTimeChangedListener(object : TimePicker.OnTimeChangedListener{
|
|
||||||
override fun onTimeChanged(p0: TimePicker?, p1: Int, p2: Int)
|
|
||||||
{
|
|
||||||
handler.removeCallbacks(requery_and_render)
|
|
||||||
val newminute = (p1 * 60) + p2
|
|
||||||
Log.i("VOUSSOIR", "End time changed $newminute")
|
|
||||||
if (newminute < track_query_end_time_previous && (track_query_end_time_previous - newminute > 60))
|
|
||||||
{
|
|
||||||
increment_datepicker(track_query_end_date)
|
|
||||||
}
|
|
||||||
else if (newminute > track_query_end_time_previous && (newminute - track_query_end_time_previous > 60))
|
|
||||||
{
|
|
||||||
decrement_datepicker(track_query_end_date)
|
|
||||||
}
|
|
||||||
track_query_end_time_previous = newminute
|
|
||||||
handler.postDelayed(requery_and_render, RERENDER_DELAY)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
controller = mapView.controller
|
|
||||||
mapView.addMapListener(this)
|
|
||||||
mapView.isTilesScaledToDpi = true
|
|
||||||
mapView.setTileSource(TileSourceFactory.MAPNIK)
|
|
||||||
mapView.isVerticalMapRepetitionEnabled = false
|
|
||||||
mapView.setMultiTouchControls(true)
|
|
||||||
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
|
||||||
controller.setCenter(GeoPoint(track.view_latitude, track.view_longitude))
|
|
||||||
controller.setZoom(Keys.DEFAULT_ZOOM_LEVEL)
|
|
||||||
|
|
||||||
statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
|
|
||||||
statisticsView = rootView.findViewById(R.id.statistics_view)
|
|
||||||
distanceView = rootView.findViewById(R.id.statistics_data_distance)
|
|
||||||
waypointsView = rootView.findViewById(R.id.statistics_data_waypoints)
|
|
||||||
durationView = rootView.findViewById(R.id.statistics_data_duration)
|
|
||||||
velocityView = rootView.findViewById(R.id.statistics_data_velocity)
|
|
||||||
recordingStartView = rootView.findViewById(R.id.statistics_data_recording_start)
|
|
||||||
recordingStopView = rootView.findViewById(R.id.statistics_data_recording_stop)
|
|
||||||
recordingPausedLabelView = rootView.findViewById(R.id.statistics_p_recording_paused)
|
|
||||||
recordingPausedView = rootView.findViewById(R.id.statistics_data_recording_paused)
|
|
||||||
maxAltitudeView = rootView.findViewById(R.id.statistics_data_max_altitude)
|
|
||||||
minAltitudeView = rootView.findViewById(R.id.statistics_data_min_altitude)
|
|
||||||
positiveElevationView = rootView.findViewById(R.id.statistics_data_positive_elevation)
|
|
||||||
negativeElevationView = rootView.findViewById(R.id.statistics_data_negative_elevation)
|
|
||||||
elevationDataViews = rootView.findViewById(R.id.elevation_data)
|
|
||||||
|
|
||||||
useImperialUnits = PreferencesHelper.loadUseImperialUnits()
|
|
||||||
|
|
||||||
if (AppThemeHelper.isDarkModeOn(context as Activity)) {
|
|
||||||
mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
|
|
||||||
}
|
|
||||||
|
|
||||||
statisticsSheetBehavior = BottomSheetBehavior.from<View>(statisticsSheet)
|
|
||||||
statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
||||||
|
|
||||||
render_track()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun render_track()
|
|
||||||
{
|
|
||||||
if (special_points_overlay != null)
|
|
||||||
{
|
|
||||||
mapView.overlays.remove(special_points_overlay)
|
|
||||||
}
|
|
||||||
if (track_overlay != null)
|
|
||||||
{
|
|
||||||
mapView.overlays.remove(track_overlay)
|
|
||||||
}
|
|
||||||
if (track.trkpts.isNotEmpty())
|
|
||||||
{
|
|
||||||
track_overlay = createTrackOverlay(context, mapView, track.trkpts, Keys.STATE_TRACKING_STOPPED)
|
|
||||||
special_points_overlay = create_start_end_markers(context, mapView, track.trkpts)
|
|
||||||
}
|
|
||||||
setupStatisticsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get_datetime(datepicker: DatePicker, timepicker: TimePicker, seconds: Int): Date
|
|
||||||
{
|
|
||||||
val cal = GregorianCalendar.getInstance()
|
|
||||||
cal.set(datepicker.year, datepicker.month, datepicker.dayOfMonth, timepicker.hour, timepicker.minute, seconds)
|
|
||||||
Log.i("VOUSSOIR", cal.time.toString())
|
|
||||||
return cal.time
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decrement_datepicker(picker: DatePicker)
|
|
||||||
{
|
|
||||||
val cal = GregorianCalendar.getInstance()
|
|
||||||
cal.set(picker.year, picker.month, picker.dayOfMonth)
|
|
||||||
cal.add(Calendar.DATE, -1)
|
|
||||||
picker.updateDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun increment_datepicker(picker: DatePicker)
|
|
||||||
{
|
|
||||||
val cal = GregorianCalendar.getInstance()
|
|
||||||
cal.set(picker.year, picker.month, picker.dayOfMonth)
|
|
||||||
cal.add(Calendar.DATE, 1)
|
|
||||||
picker.updateDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupStatisticsViews()
|
|
||||||
{
|
|
||||||
val stats: TrackStatistics = track.statistics()
|
|
||||||
trackNameView.text = track.name
|
|
||||||
distanceView.text = LengthUnitHelper.convertDistanceToString(stats.distance, useImperialUnits)
|
|
||||||
waypointsView.text = track.trkpts.size.toString()
|
|
||||||
durationView.text = DateTimeHelper.convertToReadableTime(context, stats.duration)
|
|
||||||
velocityView.text = LengthUnitHelper.convertToVelocityString(stats.velocity, useImperialUnits)
|
|
||||||
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.start_time)
|
|
||||||
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.end_time)
|
|
||||||
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.max_altitude, useImperialUnits)
|
|
||||||
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.min_altitude, useImperialUnits)
|
|
||||||
positiveElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_ascent, useImperialUnits)
|
|
||||||
negativeElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_descent, useImperialUnits)
|
|
||||||
|
|
||||||
// inform user about possible accuracy issues with altitude measurements
|
|
||||||
elevationDataViews.referencedIds.forEach { id ->
|
|
||||||
(rootView.findViewById(id) as View).setOnClickListener{
|
|
||||||
Toast.makeText(context, R.string.toast_message_elevation_info, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// make track name on statistics sheet clickable
|
|
||||||
trackNameView.setOnClickListener {
|
|
||||||
toggleStatisticsSheetVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Shows/hides the statistics sheet */
|
|
||||||
private fun toggleStatisticsSheetVisibility()
|
|
||||||
{
|
|
||||||
when (statisticsSheetBehavior.state) {
|
|
||||||
BottomSheetBehavior.STATE_EXPANDED -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
||||||
else -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Overrides onZoom from MapListener */
|
|
||||||
override fun onZoom(event: ZoomEvent?): Boolean
|
|
||||||
{
|
|
||||||
return (event != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Overrides onScroll from MapListener */
|
|
||||||
override fun onScroll(event: ScrollEvent?): Boolean
|
|
||||||
{
|
|
||||||
return (event != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requery_and_render: Runnable = object : Runnable {
|
|
||||||
override fun run()
|
|
||||||
{
|
|
||||||
Log.i("VOUSSOIR", "requery_and_render")
|
|
||||||
track.start_time = get_datetime(track_query_start_date, track_query_start_time, seconds=0)
|
|
||||||
track.end_time = get_datetime(track_query_end_date, track_query_end_time, seconds=59)
|
|
||||||
track.load_trkpts()
|
|
||||||
Log.i("VOUSSOIR", "Reloaded ${track.trkpts.size} trkpts.")
|
|
||||||
render_track()
|
|
||||||
mapView.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue