diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt
index a604eff..86f30e5 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt
+++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt
@@ -1,65 +1,75 @@
/*
BusTO - Backend components
Copyright (C) 2023 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend.gtfs
-import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship
import com.google.transit.realtime.GtfsRealtime.VehiclePosition
-import com.google.transit.realtime.GtfsRealtime.VehiclePosition.OccupancyStatus
data class LivePositionUpdate(
val tripID: String, //tripID WITHOUT THE "gtt:" prefix
val startTime: String?,
val startDate: String?,
val routeID: String,
val vehicle: String,
- val latitude: Double,
- val longitude: Double,
- val bearing: Float?,
-
+ var latitude: Double,
+ var longitude: Double,
+ var bearing: Float?,
+ //the timestamp IN SECONDS
val timestamp: Long,
val nextStop: String?,
/*val vehicleInfo: VehicleInfo,
val occupancyStatus: OccupancyStatus?,
val scheduleRelationship: ScheduleRelationship?
*/
+ //var tripInfo: TripAndPatternWithStops?,
){
constructor(position: VehiclePosition) : this(
position.trip.tripId,
position.trip.startTime,
position.trip.startDate,
position.trip.routeId,
position.vehicle.label,
position.position.latitude.toDouble(),
position.position.longitude.toDouble(),
position.position.bearing,
position.timestamp,
null
)
/*data class VehicleInfo(
val id: String,
val label:String
)
*/
+ /*fun withNewPositionAndBearing(latitude: Double, longitude: Double, bearing: Float) =
+ LivePositionUpdate(this.tripID, this.startTime, this.startTime,
+ this.routeID, this.vehicle, latitude, longitude, bearing,
+ this.timestamp,this.nextStop)
+
+ fun withNewPosition(latitude: Double, longitude: Double) =
+ LivePositionUpdate(this.tripID, this.startTime, this.startTime,
+ this.routeID, this.vehicle, latitude, longitude, this.bearing,
+ this.timestamp,this.nextStop)
+
+ */
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt
index cdf88f1..4cad925 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt
@@ -1,767 +1,750 @@
/*
BusTO - Fragments components
Copyright (C) 2020 Andrea Ugo
Copyright (C) 2021 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments
import android.Manifest
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.location.LocationManager
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import it.reyboz.bustorino.R
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
import it.reyboz.bustorino.backend.mato.MQTTMatoClient
import it.reyboz.bustorino.backend.utils
import it.reyboz.bustorino.data.gtfs.MatoPattern
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import it.reyboz.bustorino.map.BusInfoWindow
import it.reyboz.bustorino.map.CustomInfoWindow
import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder
import it.reyboz.bustorino.map.LocationOverlay
import it.reyboz.bustorino.map.LocationOverlay.OverlayCallbacks
import it.reyboz.bustorino.map.MarkerUtils
import it.reyboz.bustorino.middleware.GeneralActivity
import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import it.reyboz.bustorino.viewmodels.StopsMapViewModel
import org.osmdroid.config.Configuration
import org.osmdroid.events.DelayedMapListener
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.FolderOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.infowindow.InfoWindow
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
open class MapFragmentKt : ScreenBaseFragment() {
protected var listenerMain: FragmentListenerMain? = null
private var shownStops: HashSet? = null
private lateinit var map: MapView
var ctx: Context? = null
private lateinit var mLocationOverlay: LocationOverlay
private lateinit var stopsFolderOverlay: FolderOverlay
private var savedMapState: Bundle? = null
protected lateinit var btCenterMap: ImageButton
protected lateinit var btFollowMe: ImageButton
protected var coordLayout: CoordinatorLayout? = null
private var hasMapStartFinished = false
private var followingLocation = false
//the ViewModel from which we get the stop to display in the map
private val stopsViewModel: StopsMapViewModel by viewModels()
//private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class);
private val livePositionsViewModel: LivePositionsViewModel by viewModels()
private var useMQTTViewModel = true
private val busPositionMarkersByTrip = HashMap()
private var busPositionsOverlay: FolderOverlay? = null
private val tripMarkersAnimators = HashMap()
protected val responder = TouchResponder { stopID, stopName ->
if (listenerMain != null) {
Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID")
listenerMain!!.requestArrivalsForStopID(stopID)
}
}
protected val locationCallbacks: OverlayCallbacks = object : OverlayCallbacks {
override fun onDisableFollowMyLocation() {
updateGUIForLocationFollowing(false)
followingLocation = false
}
override fun onEnableFollowMyLocation() {
updateGUIForLocationFollowing(true)
followingLocation = true
}
}
private val positionRequestLauncher =
registerForActivityResult, Map>(
ActivityResultContracts.RequestMultiplePermissions(),
ActivityResultCallback { result ->
if (result == null) {
Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?")
} else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION]
&& java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) {
// We can use the position, restart location overlay
map.overlays.remove(mLocationOverlay)
startLocationOverlay(true, map)
if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
return@ActivityResultCallback ///@registerForActivityResult
val locationManager =
requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
@SuppressLint("MissingPermission") val userLocation =
locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (userLocation != null) {
map!!.controller.setZoom(POSITION_FOUND_ZOOM)
val startPoint = GeoPoint(userLocation)
setLocationFollowing(true)
map!!.controller.setCenter(startPoint)
}
} else Log.w(DEBUG_TAG, "No location permission")
})
//public static MapFragment getInstance(@NonNull Stop stop){
// return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID);
//}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//use the same layout as the activity
val root = inflater.inflate(R.layout.fragment_map, container, false)
val context = requireContext()
ctx = context.applicationContext
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(context))
map = root.findViewById(R.id.map)
map.setTileSource(TileSourceFactory.MAPNIK)
//map.setTilesScaledToDpi(true);
map.setFlingEnabled(true)
// add ability to zoom with 2 fingers
map.setMultiTouchControls(true)
btCenterMap = root.findViewById(R.id.centerMapImageButton)
btFollowMe = root.findViewById(R.id.followUserImageButton)
coordLayout = root.findViewById(R.id.coord_layout)
//setup FolderOverlay
stopsFolderOverlay = FolderOverlay()
//setup Bus Markers Overlay
busPositionsOverlay = FolderOverlay()
//reset shown bus updates
busPositionMarkersByTrip.clear()
tripMarkersAnimators.clear()
//set map not done
hasMapStartFinished = false
val keySourcePositions = getString(R.string.pref_positions_source)
useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE)
.contentEquals(SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE)
//Start map from bundle
if (savedInstanceState != null) startMap(arguments, savedInstanceState) else startMap(
arguments, savedMapState
)
//set listeners
map.addMapListener(DelayedMapListener(object : MapListener {
override fun onScroll(paramScrollEvent: ScrollEvent): Boolean {
requestStopsToShow()
//Log.d(DEBUG_TAG, "Scrolling");
//if (moveTriggeredByCode) moveTriggeredByCode =false;
//else setLocationFollowing(false);
return true
}
override fun onZoom(event: ZoomEvent): Boolean {
requestStopsToShow()
return true
}
}))
btCenterMap.setOnClickListener(View.OnClickListener { v: View? ->
//Log.i(TAG, "centerMap clicked ");
if (Permissions.bothLocationPermissionsGranted(context)) {
val myPosition = mLocationOverlay!!.myLocation
map.getController().animateTo(myPosition)
} else Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_SHORT)
.show()
})
btFollowMe.setOnClickListener(View.OnClickListener { v: View? ->
//Log.i(TAG, "btFollowMe clicked ");
if (Permissions.bothLocationPermissionsGranted(context)) setLocationFollowing(!followingLocation) else Toast.makeText(
context, R.string.enable_position_message_map, Toast.LENGTH_SHORT
)
.show()
})
return root
}
override fun onAttach(context: Context) {
super.onAttach(context)
listenerMain = if (context is FragmentListenerMain) {
context
} else {
throw RuntimeException(
context.toString()
+ " must implement FragmentListenerMain"
)
}
}
override fun onDetach() {
super.onDetach()
listenerMain = null
Log.w(DEBUG_TAG, "Fragment detached")
}
override fun onPause() {
super.onPause()
Log.w(DEBUG_TAG, "On pause called mapfrag")
saveMapState()
for (animator in tripMarkersAnimators.values) {
if (animator != null && animator.isRunning) {
animator.cancel()
}
}
tripMarkersAnimators.clear()
- if (useMQTTViewModel) livePositionsViewModel!!.stopMatoUpdates()
+ if (useMQTTViewModel) livePositionsViewModel.stopMatoUpdates()
}
/**
* Save the map state inside the fragment
* (calls saveMapState(bundle))
*/
private fun saveMapState() {
savedMapState = Bundle()
saveMapState(savedMapState!!)
}
/**
* Save the state of the map to restore it to a later time
* @param bundle the bundle in which to save the data
*/
private fun saveMapState(bundle: Bundle) {
Log.d(DEBUG_TAG, "Saving state, location following: $followingLocation")
bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation)
if (map == null) {
//The map is null, it can happen?
Log.e(DEBUG_TAG, "Cannot save map center, map is null")
return
}
val loc = map!!.mapCenter
bundle.putDouble(MAP_CENTER_LAT_KEY, loc.latitude)
bundle.putDouble(MAP_CENTER_LON_KEY, loc.longitude)
bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map!!.zoomLevelDouble)
}
override fun onResume() {
super.onResume()
//TODO: cleanup duplicate code (maybe merging the positions classes?)
if (listenerMain != null) listenerMain!!.readyGUIfor(FragmentKind.MAP)
/// choose which to use
val keySourcePositions = getString(R.string.pref_positions_source)
useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE)
.contentEquals(
SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE
)
- if (livePositionsViewModel != null) {
- //gtfsPosViewModel.requestUpdates();
- if (useMQTTViewModel) livePositionsViewModel!!.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL)
- else livePositionsViewModel!!.requestGTFSUpdates()
- //mapViewModel.testCascade();
- livePositionsViewModel!!.isLastWorkResultGood.observe(this) { d: Boolean ->
- Log.d(
- DEBUG_TAG, "Last trip download result is $d"
- )
- }
- livePositionsViewModel!!.tripsGtfsIDsToQuery.observe(this) { dat: List ->
- Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat")
- livePositionsViewModel!!.downloadTripsFromMato(dat)
- }
- } /*else if(gtfsPosViewModel!=null){
- gtfsPosViewModel.requestUpdates();
- gtfsPosViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> {
- Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat);
- //gtfsPosViewModel.downloadTripsFromMato(dat);
- MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(),
- "BusTO-MatoTripDownload");
- });
+ //gtfsPosViewModel.requestUpdates();
+ if (useMQTTViewModel) livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL)
+ else livePositionsViewModel.requestGTFSUpdates()
+ //mapViewModel.testCascade();
+ livePositionsViewModel.isLastWorkResultGood.observe(this) { d: Boolean ->
+ Log.d(
+ DEBUG_TAG, "Last trip download result is $d"
+ )
+ }
+ livePositionsViewModel.tripsGtfsIDsToQuery.observe(this) { dat: List ->
+ Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat")
+ livePositionsViewModel.downloadTripsFromMato(dat)
}
- */ else Log.e(DEBUG_TAG, "livePositionsViewModel is null at onResume")
//rerequest stop
stopsViewModel!!.requestStopsInBoundingBox(map!!.boundingBox)
}
private fun startRequestsPositions() {
if (livePositionsViewModel != null) {
//should always be the case
livePositionsViewModel!!.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> ->
Log.d(
DEBUG_TAG,
"Have " + data.size + " trip updates, has Map start finished: " + hasMapStartFinished
)
if (hasMapStartFinished) updateBusPositionsInMap(data)
if (!isDetached && !useMQTTViewModel) livePositionsViewModel!!.requestDelayedGTFSUpdates(
3000
)
}
} else {
Log.e(DEBUG_TAG, "PositionsViewModel is null")
}
}
override fun onSaveInstanceState(outState: Bundle) {
saveMapState(outState)
super.onSaveInstanceState(outState)
}
//own methods
/**
* Switch following the location on and off
* @param value true if we want to follow location
*/
fun setLocationFollowing(value: Boolean) {
followingLocation = value
if (mLocationOverlay == null || context == null || map == null) //nothing else to do
return
if (value) {
mLocationOverlay!!.enableFollowLocation()
} else {
mLocationOverlay!!.disableFollowLocation()
}
}
/**
* Do all the stuff you need to do on the gui, when parameter is changed to value
* @param following value
*/
protected fun updateGUIForLocationFollowing(following: Boolean) {
if (following) btFollowMe!!.setImageResource(R.drawable.ic_follow_me_on) else btFollowMe!!.setImageResource(
R.drawable.ic_follow_me
)
}
/**
* Build the location overlay. Enable only when
* a) we know we have the permission
* b) the location map is set
*/
private fun startLocationOverlay(enableLocation: Boolean, map: MapView?) {
checkNotNull(activity) { "Cannot enable LocationOverlay now" }
// Location Overlay
// from OpenBikeSharing (THANK GOD)
Log.d(DEBUG_TAG, "Starting position overlay")
val imlp = GpsMyLocationProvider(requireActivity().baseContext)
imlp.locationUpdateMinDistance = 5f
imlp.locationUpdateMinTime = 2000
val overlay = LocationOverlay(imlp, map, locationCallbacks)
if (enableLocation) overlay.enableMyLocation()
overlay.isOptionsMenuEnabled = true
//map.getOverlays().add(this.mLocationOverlay);
mLocationOverlay = overlay
map!!.overlays.add(mLocationOverlay)
}
fun startMap(incoming: Bundle?, savedInstanceState: Bundle?) {
//Check that we're attached
val activity = if (activity is GeneralActivity) activity as GeneralActivity? else null
if (context == null || activity == null) {
//we are not attached
Log.e(DEBUG_TAG, "Calling startMap when not attached")
return
} else {
Log.d(DEBUG_TAG, "Starting map from scratch")
}
//clear previous overlays
map!!.overlays.clear()
//parse incoming bundle
var marker: GeoPoint? = null
var name: String? = null
var ID: String? = null
var routesStopping: String? = ""
if (incoming != null) {
val lat = incoming.getDouble(BUNDLE_LATIT)
val lon = incoming.getDouble(BUNDLE_LONGIT)
marker = GeoPoint(lat, lon)
name = incoming.getString(BUNDLE_NAME)
ID = incoming.getString(BUNDLE_ID)
routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, "")
}
//ask for location permission
if (!Permissions.bothLocationPermissionsGranted(activity)) {
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
//TODO: show dialog for permission rationale
Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT)
.show()
}
positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
}
shownStops = HashSet()
// move the map on the marker position or on a default view point: Turin, Piazza Castello
// and set the start zoom
val mapController = map!!.controller
var startPoint: GeoPoint? = null
startLocationOverlay(
Permissions.bothLocationPermissionsGranted(activity),
map
)
// set the center point
if (marker != null) {
//startPoint = marker;
mapController.setZoom(POSITION_FOUND_ZOOM)
setLocationFollowing(false)
// put the center a little bit off (animate later)
startPoint = GeoPoint(marker)
startPoint.latitude = marker.latitude + utils.angleRawDifferenceFromMeters(20.0)
startPoint.longitude = marker.longitude - utils.angleRawDifferenceFromMeters(20.0)
//don't need to do all the rest since we want to show a point
} else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) {
mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY))
mapController.setCenter(
GeoPoint(
savedInstanceState.getDouble(MAP_CENTER_LAT_KEY),
savedInstanceState.getDouble(MAP_CENTER_LON_KEY)
)
)
Log.d(
DEBUG_TAG,
"Location following from savedInstanceState: " + savedInstanceState.getBoolean(
FOLLOWING_LOCAT_KEY
)
)
setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY))
} else {
Log.d(DEBUG_TAG, "No position found from intent or saved state")
var found = false
val locationManager =
requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
//check for permission
if (Permissions.bothLocationPermissionsGranted(activity)) {
@SuppressLint("MissingPermission") val userLocation =
locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (userLocation != null) {
val distan = utils.measuredistanceBetween(
userLocation.latitude, userLocation.longitude,
DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON
)
if (distan < 100000.0) {
mapController.setZoom(POSITION_FOUND_ZOOM)
startPoint = GeoPoint(userLocation)
found = true
setLocationFollowing(true)
}
}
}
if (!found) {
startPoint = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)
mapController.setZoom(NO_POSITION_ZOOM)
setLocationFollowing(false)
}
}
// set the minimum zoom level
map!!.minZoomLevel = 15.0
//add contingency check (shouldn't happen..., but)
if (startPoint != null) {
mapController.setCenter(startPoint)
}
//add stops overlay
//map.getOverlays().add(mLocationOverlay);
map!!.overlays.add(stopsFolderOverlay)
Log.d(DEBUG_TAG, "Requesting stops load")
// This is not necessary, by setting the center we already move
// the map and we trigger a stop request
//requestStopsToShow();
if (marker != null) {
// make a marker with the info window open for the searched marker
//TODO: make Stop Bundle-able
val stopMarker = makeMarker(marker, ID, name, routesStopping, true)
map!!.controller.animateTo(marker)
}
//add the overlays with the bus stops
if (busPositionsOverlay == null) {
//Log.i(DEBUG_TAG, "Null bus positions overlay,redo");
busPositionsOverlay = FolderOverlay()
}
startRequestsPositions()
if (stopsViewModel != null) {
stopsViewModel!!.stopsInBoundingBox.observe(viewLifecycleOwner) { stops: List? ->
showStopsMarkers(
stops
)
}
} else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null")
map!!.overlays.add(busPositionsOverlay)
//set map as started
hasMapStartFinished = true
}
/**
* Start a request to load the stops that are in the current view
* from the database
*/
private fun requestStopsToShow() {
// get the top, bottom, left and right screen's coordinate
val bb = map!!.boundingBox
Log.d(
DEBUG_TAG,
- "Requesting stops in bounding box, stopViewModel is null " + (stopsViewModel == null)
+ "Requesting stops in bounding box, stopViewModel is null " + (false)
)
- if (stopsViewModel != null) {
- stopsViewModel!!.requestStopsInBoundingBox(bb)
- }
+ stopsViewModel.requestStopsInBoundingBox(bb)
}
private fun updateBusMarker(
marker: Marker?,
posUpdate: LivePositionUpdate,
justCreated: Boolean
) {
val position: GeoPoint
val updateID = posUpdate.tripID
if (!justCreated) {
position = marker!!.position
if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) {
val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude)
val valueAnimator = MarkerUtils.makeMarkerAnimator(
map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200
)
valueAnimator.setAutoCancel(true)
tripMarkersAnimators[updateID] = valueAnimator
valueAnimator.start()
}
//marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()));
} else {
position = GeoPoint(posUpdate.latitude, posUpdate.longitude)
marker!!.position = position
}
- if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f
+ marker.rotation = posUpdate.bearing?.let { it*-1f } ?: 0.0f
}
private fun updateBusPositionsInMap(tripsPatterns: HashMap>) {
Log.d(DEBUG_TAG, "Updating positions of the buses")
//if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
val noPatternsTrips = ArrayList()
for (tripID in tripsPatterns.keys) {
val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue
//check if Marker is already created
if (busPositionMarkersByTrip.containsKey(tripID)) {
//need to change the position of the marker
val marker = busPositionMarkersByTrip[tripID]!!
updateBusMarker(marker, update, false)
if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) {
val window = marker.infoWindow as BusInfoWindow
if (tripWithPatternStops != null) {
//Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID);
window.setPatternAndDraw(tripWithPatternStops.pattern)
}
}
} else {
//marker is not there, need to make it
- if (map == null) Log.e(
- DEBUG_TAG,
- "Creating marker with null map, things will explode"
- )
val marker = Marker(map)
/*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources(
getResources(),
R.drawable.point_heading_icon,
R.dimen.map_icons_size, R.dimen.map_icons_size);
*/
//String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID());
val mdraw =
ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, null)!!
//mdraw.setBounds(0,0,28,28);
marker.icon = mdraw
if (tripWithPatternStops == null) {
noPatternsTrips.add(tripID)
}
var markerPattern: MatoPattern? = null
if (tripWithPatternStops != null && tripWithPatternStops.pattern != null) markerPattern =
tripWithPatternStops.pattern
marker.infoWindow =
BusInfoWindow(map!!, update, markerPattern, false) { pattern: MatoPattern? -> }
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
updateBusMarker(marker, update, true)
// the overlay is null when it's not attached yet?5
// cannot recreate it because it becomes null very soon
// if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
//save the marker
if (busPositionsOverlay != null) {
busPositionsOverlay!!.add(marker)
busPositionMarkersByTrip[tripID] = marker
}
}
}
if (noPatternsTrips.size > 0) {
Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips")
}
}
/**
* Add stops as Markers on the map
* @param stops the list of stops that must be included
*/
protected fun showStopsMarkers(stops: List?) {
if (context == null || stops == null) {
//we are not attached
return
}
var good = true
for (stop in stops) {
if (shownStops!!.contains(stop.ID)) {
continue
}
if (stop.longitude == null || stop.latitude == null) continue
shownStops!!.add(stop.ID)
if (!map!!.isShown) {
if (good) Log.d(
DEBUG_TAG,
"Need to show stop but map is not shown, probably detached already"
)
good = false
continue
} else if (map!!.repository == null) {
Log.e(DEBUG_TAG, "Map view repository is null")
}
val marker = GeoPoint(stop.latitude!!, stop.longitude!!)
val stopMarker = makeMarker(marker, stop, false)
stopsFolderOverlay!!.add(stopMarker)
if (!map!!.overlays.contains(stopsFolderOverlay)) {
Log.w(DEBUG_TAG, "Map doesn't have folder overlay")
}
good = true
}
//Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay");
//force redraw of markers
map!!.invalidate()
}
fun makeMarker(geoPoint: GeoPoint?, stop: Stop, isStartMarker: Boolean): Marker {
return makeMarker(
geoPoint, stop.ID,
stop.stopDefaultName,
stop.routesThatStopHereToString(), isStartMarker
)
}
fun makeMarker(
geoPoint: GeoPoint?, stopID: String?, stopName: String?,
routesStopping: String?, isStartMarker: Boolean
): Marker {
// add a marker
val marker = Marker(map)
// set custom info window as info window
val popup = CustomInfoWindow(
map, stopID, stopName, routesStopping,
responder, R.layout.linedetail_stop_infowindow, R.color.red_darker
)
marker.infoWindow = popup
// make the marker clickable
marker.setOnMarkerClickListener { thisMarker: Marker, mapView: MapView? ->
if (thisMarker.isInfoWindowOpen) {
// on second click
Log.w(DEBUG_TAG, "Pressed on the click marker")
} else {
// on first click
// hide all opened info window
InfoWindow.closeAllInfoWindowsOn(map)
// show this particular info window
thisMarker.showInfoWindow()
// move the map to its position
map!!.controller.animateTo(thisMarker.position)
}
true
}
// set its position
marker.position = geoPoint
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
// add to it an icon
//marker.setIcon(getResources().getDrawable(R.drawable.bus_marker));
marker.icon = ResourcesCompat.getDrawable(resources, R.drawable.bus_stop, ctx!!.theme)
// add to it a title
marker.title = stopName
// set the description as the ID
marker.snippet = stopID
// show popup info window of the searched marker
if (isStartMarker) {
marker.showInfoWindow()
//map.getController().animateTo(marker.getPosition());
}
return marker
}
override fun getBaseViewForSnackBar(): View? {
return coordLayout
}
companion object {
//private static final String TAG = "Busto-MapActivity";
private const val MAP_CURRENT_ZOOM_KEY = "map-current-zoom"
private const val MAP_CENTER_LAT_KEY = "map-center-lat"
private const val MAP_CENTER_LON_KEY = "map-center-lon"
private const val FOLLOWING_LOCAT_KEY = "following"
const val BUNDLE_LATIT = "lat"
const val BUNDLE_LONGIT = "lon"
const val BUNDLE_NAME = "name"
const val BUNDLE_ID = "ID"
const val BUNDLE_ROUTES_STOPPING = "routesStopping"
const val FRAGMENT_TAG = "BusTOMapFragment"
private const val DEFAULT_CENTER_LAT = 45.0708
private const val DEFAULT_CENTER_LON = 7.6858
private const val POSITION_FOUND_ZOOM = 18.3
const val NO_POSITION_ZOOM = 17.1
private const val DEBUG_TAG = FRAGMENT_TAG
@JvmStatic
fun getInstance(): MapFragmentKt {
return MapFragmentKt()
}
@JvmStatic
fun getInstance(stop: Stop): MapFragmentKt {
val fragment = MapFragmentKt()
val args = Bundle()
args.putDouble(MapFragment.BUNDLE_LATIT, stop.latitude!!)
args.putDouble(MapFragment.BUNDLE_LONGIT, stop.longitude!!)
args.putString(MapFragment.BUNDLE_NAME, stop.stopDisplayName)
args.putString(MapFragment.BUNDLE_ID, stop.ID)
args.putString(MapFragment.BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString())
fragment.arguments = args
return fragment
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
index 85bdda4..a220cb2 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
@@ -1,732 +1,888 @@
package it.reyboz.bustorino.fragments
import android.Manifest
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.LinearInterpolator
import android.widget.ImageButton
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.preference.PreferenceManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.gson.Gson
import com.google.gson.JsonObject
import it.reyboz.bustorino.R
import it.reyboz.bustorino.backend.Stop
+import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
+import it.reyboz.bustorino.backend.mato.MQTTMatoClient
+import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
+import it.reyboz.bustorino.fragments.MapFragmentKt.Companion
+import it.reyboz.bustorino.fragments.SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE
+import it.reyboz.bustorino.map.MapUtils
import it.reyboz.bustorino.map.Styles
import it.reyboz.bustorino.util.Permissions
+import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import it.reyboz.bustorino.viewmodels.StopsMapViewModel
import org.maplibre.android.MapLibre
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
import org.maplibre.android.location.LocationComponent
import org.maplibre.android.location.LocationComponentActivationOptions
import org.maplibre.android.location.LocationComponentOptions
import org.maplibre.android.location.engine.LocationEngineRequest
import org.maplibre.android.location.modes.CameraMode
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.OnMapReadyCallback
import org.maplibre.android.maps.Style
import org.maplibre.android.plugins.annotation.Symbol
import org.maplibre.android.plugins.annotation.SymbolManager
import org.maplibre.android.plugins.annotation.SymbolOptions
+import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.layers.Property.ICON_ANCHOR_CENTER
import org.maplibre.android.style.layers.PropertyFactory
import org.maplibre.android.style.layers.SymbolLayer
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [MapLibreFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class MapLibreFragment : Fragment(), OnMapReadyCallback {
//private var param1: String? = null
//private var param2: String? = null
// Declare a variable for MapView
private lateinit var mapView: MapView
private lateinit var locationComponent: LocationComponent
private var lastLocation: Location? = null
private val stopsViewModel: StopsMapViewModel by viewModels()
private val gson = Gson()
private var stopsShowing = ArrayList(0)
private var isBottomSheetShowing = false
private lateinit var symbolManager: SymbolManager
protected var map: MapLibreMap? = null
// Sources for stops and buses
private lateinit var stopsSource: GeoJsonSource
private lateinit var busesSource: GeoJsonSource
private var isStopsLayerStarted = false
private var lastStopsSizeShown = 0
private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0)
private lateinit var mapStyle: Style
+ private var mapInitCompleted =false
//bottom Sheet behavior
private lateinit var bottomSheetBehavior: BottomSheetBehavior
private var bottomLayout: RelativeLayout? = null
private lateinit var stopTitleTextView: TextView
private lateinit var stopNumberTextView: TextView
private lateinit var linesPassingTextView: TextView
private lateinit var arrivalsCard: CardView
private lateinit var directionsCard: CardView
private var stopActiveSymbol: Symbol? = null
// Location stuff
private lateinit var locationManager: LocationManager
private lateinit var showUserPositionButton: ImageButton
private lateinit var centerUserButton: ImageButton
private lateinit var followUserButton: ImageButton
private var followingUserLocation = false
private var ignoreCameraMovementForFollowing = true
private var enablingPositionFromClick = false
private val positionRequestLauncher =
registerForActivityResult, Map>(
ActivityResultContracts.RequestMultiplePermissions(),
ActivityResultCallback { result ->
if (result == null) {
Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?")
} else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION]
&& java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) {
// We can use the position, restart location overlay
Log.d(DEBUG_TAG, "HAVE THE PERMISSIONS")
if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
return@ActivityResultCallback ///@registerForActivityResult
val locationManager =
requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
@SuppressLint("MissingPermission") val userLocation =
locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (userLocation != null) {
if(LatLng(userLocation.latitude, userLocation.longitude).distanceTo(DEFAULT_LATLNG) >= MAX_DIST_KM*1000){
setMapLocationEnabled(true, true, false)
}
} else requestInitialUserLocation()
} else{
Toast.makeText(requireContext(),"User location disabled", Toast.LENGTH_SHORT).show()
Log.w(DEBUG_TAG, "No location permission")
}
})
private val showUserPositionRequestLauncher =
registerForActivityResult, Map>(
ActivityResultContracts.RequestMultiplePermissions(),
ActivityResultCallback { result ->
if (result == null) {
Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?")
} else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION]
&& java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) {
// We can use the position, restart location overlay
if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
return@ActivityResultCallback ///@registerForActivityResult
setMapLocationEnabled(true, true, enablingPositionFromClick)
} else Log.w(DEBUG_TAG, "No location permission")
})
+ //BUS POSITIONS
+ private var useMQTTViewModel = true
+ private val livePositionsViewModel : LivePositionsViewModel by viewModels()
+ private val positionsByVehDict = HashMap(5)
+ private val animatorsByVeh = HashMap()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/*arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
*/
MapLibre.getInstance(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val rootView = inflater.inflate(R.layout.fragment_map_libre,
container, false)
// Init layout view
// Init the MapView
mapView = rootView.findViewById(R.id.libreMapView)
mapView.getMapAsync(this) //{ //map ->
//map.setStyle("https://demotiles.maplibre.org/style.json") }
//init bottom sheet
val bottomSheet = rootView.findViewById(R.id.bottom_sheet)
bottomLayout = bottomSheet
stopTitleTextView = bottomSheet.findViewById(R.id.stopTitleTextView)
stopNumberTextView = bottomSheet.findViewById(R.id.stopNumberTextView)
linesPassingTextView = bottomSheet.findViewById(R.id.linesPassingTextView)
arrivalsCard = bottomSheet.findViewById(R.id.arrivalsCardButton)
directionsCard = bottomSheet.findViewById(R.id.directionsCardButton)
showUserPositionButton = rootView.findViewById(R.id.locationEnableIcon)
showUserPositionButton.setOnClickListener(this::switchUserLocationStatus)
followUserButton = rootView.findViewById(R.id.followUserImageButton)
centerUserButton = rootView.findViewById(R.id.centerMapImageButton)
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
arrivalsCard.setOnClickListener {
if(context!=null){
Toast.makeText(context,"ARRIVALS", Toast.LENGTH_SHORT).show()
}
}
centerUserButton.setOnClickListener {
if(context!=null && locationComponent.isLocationComponentEnabled) {
val location = locationComponent.lastKnownLocation
location?.let {
mapView.getMapAsync { map ->
map.animateCamera(CameraUpdateFactory.newCameraPosition(
CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build()), 500)
}
}
}
}
followUserButton.setOnClickListener {
if(context!=null && locationComponent.isLocationComponentEnabled){
if(followingUserLocation)
locationComponent.cameraMode = CameraMode.NONE
else locationComponent.cameraMode = CameraMode.TRACKING
setFollowingUser(!followingUserLocation)
}
}
locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (haveLocationPermissions()) {
requestInitialUserLocation()
} else{
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
//TODO: show dialog for permission rationale
Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT)
.show()
}
positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
}
// Setup close button
rootView.findViewById(R.id.btnClose).setOnClickListener {
hideStopBottomSheet()
}
return rootView
}
/**
- * This method sets up the map
+ * This method sets up the map and the layers
*/
override fun onMapReady(mapReady: MapLibreMap) {
this.map = mapReady
//TODO: Check if we have the user last position and start the map there
mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)).zoom(
15.0).build()
val mjson = Styles.getJsonStyleFromAsset(requireContext(), "map_style_good_noshops.json")//ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json")
activity?.run {
mapReady.setStyle(Style.Builder().fromJson(mjson!!)) { style ->
mapStyle = style
//setupLayers(style)
symbolManager = SymbolManager(mapView,mapReady,style)
symbolManager.iconAllowOverlap = true
symbolManager.textAllowOverlap = true
symbolManager.addClickListener{ _ ->
if (stopActiveSymbol!=null){
hideStopBottomSheet()
return@addClickListener true
} else
return@addClickListener false
}
// Start observing data
observeViewModels()
initMapLocation(style, mapReady, requireContext())
+ initStopsLayer(style, FeatureCollection.fromFeatures(ArrayList()))
+ setupBusLayer(style)
}
mapReady.addOnCameraIdleListener {
map?.let {
val newBbox = it.projection.visibleRegion.latLngBounds
if ((newBbox.center==lastBBox.center) && (newBbox.latitudeSpan==lastBBox.latitudeSpan) && (newBbox.longitudeSpan==lastBBox.latitudeSpan)){
//do nothing
} else {
stopsViewModel.loadStopsInLatLngBounds(newBbox)
lastBBox = newBbox
//if we are moving away from the position, disable it
/*
*/
}
}
}
mapReady.addOnCameraMoveStartedListener {
if (ignoreCameraMovementForFollowing){
ignoreCameraMovementForFollowing = false
}
else if (followingUserLocation){
setFollowingUser(false)
}
}
mapReady.addOnMapClickListener { point ->
val screenPoint = mapReady.projection.toScreenLocation(point)
val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID)
if (features.isNotEmpty()) {
val feature = features[0]
val id = feature.getStringProperty("id")
val name = feature.getStringProperty("name")
//Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show()
val stop = stopsViewModel.getStopByID(id)
stop?.let {
if (isBottomSheetShowing){
hideStopBottomSheet()
}
showStopInBottomSheet(it)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
isBottomSheetShowing = true
//move camera
if(it.latitude!=null && it.longitude!=null)
//mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(it.latitude!!, it.longitude!!)).build()
mapReady.animateCamera(CameraUpdateFactory.newLatLng(LatLng(it.latitude!!,it.longitude!!)),750)
}
-
-
-
-
return@addOnMapClickListener true
}
false
}
- //makeStyleMapBoxUrl(false))
-
+ mapInitCompleted = true
+ // we start requesting the bus positions now
+ startRequestingPositions()
}
}
private fun initStopsLayer(style: Style, features:FeatureCollection){
stopsSource = GeoJsonSource(STOPS_SOURCE_ID,features)
style.addSource(stopsSource)
// add icon
style.addImage(STOP_IMAGE_ID,
ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!)
style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!)
// Stops layer
val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID)
stopsLayer.withProperties(
PropertyFactory.iconImage(STOP_IMAGE_ID),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconIgnorePlacement(true)
)
style.addLayerBelow(stopsLayer, "label_country_1")
isStopsLayerStarted = true
}
/**
* Setup the Map Layers
*/
- //private fun setupLayers(style: Style) {
+ private fun setupBusLayer(style: Style) {
// Buses source
- // TODO when adding the buses
- //busesSource = GeoJsonSource(BUSES_SOURCE_ID)
- //style.addSource(busesSource)
+ busesSource = GeoJsonSource(BUSES_SOURCE_ID)
+ style.addSource(busesSource)
+ style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!)
- /*
- // TODO when adding the buses
// Buses layer
val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply {
withProperties(
- PropertyFactory.iconImage("bus"),
- PropertyFactory.iconSize(1.0f),
+ PropertyFactory.iconImage("bus_symbol"),
+ //PropertyFactory.iconSize(1.0f),
PropertyFactory.iconAllowOverlap(true),
+ PropertyFactory.iconIgnorePlacement(true),
PropertyFactory.iconRotate(Expression.get("bearing"))
)
}
- style.addLayer(busesLayer)
- */
- //}
+ style.addLayerAbove(busesLayer, STOPS_LAYER_ID)
+
+ }
private fun showStopInBottomSheet(stop: Stop?){
if (stop==null) return
bottomLayout?.let {
//lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
stopTitleTextView.text = stop.stopDefaultName
stopNumberTextView.text = stop.ID
val string_show = if (stop.numRoutesStopping==0) ""
else if (stop.numRoutesStopping <= 1)
requireContext().getString(R.string.line_fill, stop.routesThatStopHereToString())
else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString())
linesPassingTextView.text = string_show
}
//add stop marker
if (stop.latitude!=null && stop.longitude!=null) {
/*val marker = map?.addMarker(
MarkerOptions()
.position(LatLng(stop.latitude!!, stop.longitude!!)) // example coords
.icon(
//IconFactory.getInstance(requireContext()).fromBitmap(
getIconFromVectorDrawable(requireContext(), R.drawable.bus_stop_new_highlight)
//R.drawable.bus_stop_new_highlight)
//IconFactory.getInstance(requireContext())
//.fromResource(R.drawable.bus_stop_new_highlight)
)
.title(stop.stopDefaultName)
)
*/
stopActiveSymbol = symbolManager.create(
SymbolOptions()
.withLatLng(LatLng(stop.latitude!!, stop.longitude!!))
.withIconImage(STOP_ACTIVE_IMG)
.withIconAnchor(ICON_ANCHOR_CENTER)
)
}
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onResume() {
super.onResume()
mapView.onResume()
+
+ val keySourcePositions = getString(R.string.pref_positions_source)
+ useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .getString(keySourcePositions, LIVE_POSITIONS_PREF_MQTT_VALUE)
+ .contentEquals(LIVE_POSITIONS_PREF_MQTT_VALUE)
+
+ if (useMQTTViewModel) livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL)
+ else livePositionsViewModel.requestGTFSUpdates()
+ //mapViewModel.testCascade();
+ livePositionsViewModel.isLastWorkResultGood.observe(this) { d: Boolean ->
+ Log.d(
+ DEBUG_TAG, "Last trip download result is $d"
+ )
+ }
+ livePositionsViewModel.tripsGtfsIDsToQuery.observe(this) { dat: List ->
+ Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat")
+ livePositionsViewModel.downloadTripsFromMato(dat)
+ }
}
override fun onPause() {
super.onPause()
mapView.onPause()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
private fun observeViewModels() {
// Observe stops
stopsViewModel.stopsToShow.observe(viewLifecycleOwner) { stops ->
stopsShowing = ArrayList(stops)
displayStops(stopsShowing)
}
+
}
/**
* Add the stops to the layers
*/
private fun displayStops(stops: List?) {
if (stops.isNullOrEmpty()) return
if (stops.size==lastStopsSizeShown){
Log.d(DEBUG_TAG, "Not updating, we have the same stop (can only increase!)")
return
}
val features = ArrayList()//stops.mapNotNull { stop ->
//stop.latitude?.let { lat ->
// stop.longitude?.let { lon ->
for (s in stops){
if (s.latitude!=null && s.longitude!=null)
features.add(
Feature.fromGeometry(
Point.fromLngLat(s.longitude!!, s.latitude!!),
JsonObject().apply {
addProperty("id", s.ID)
addProperty("name", s.stopDefaultName)
addProperty("routes", s.routesThatStopHereToString()) // Add routes array to JSON object
}
)
)
}
Log.d(DEBUG_TAG,"Have put ${features.size} stops to display")
// if the layer is already started, substitute the stops inside, otherwise start it
if (isStopsLayerStarted) {
stopsSource.setGeoJson(FeatureCollection.fromFeatures(features))
lastStopsSizeShown = features.size
} else
map?.let { initStopsLayer(mapStyle, FeatureCollection.fromFeatures(features))
Log.d(DEBUG_TAG,"Started stops layer on map")
lastStopsSizeShown = features.size
}
}
// Hide the bottom sheet and remove extra symbol
private fun hideStopBottomSheet(){
if (stopActiveSymbol!=null){
symbolManager.delete(stopActiveSymbol)
stopActiveSymbol = null
}
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
isBottomSheetShowing = false
}
+ // --------------- BUS LOCATIONS STUFF --------------------------
+ /**
+ * Start requesting position updates
+ */
+ private fun startRequestingPositions() {
+ livePositionsViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> ->
+ Log.d(
+ DEBUG_TAG,
+ "Have " + data.size + " trip updates, has Map start finished: " + mapInitCompleted
+ )
+ if (mapInitCompleted) updateBusPositionsInMap(data)
+ if (!isDetached && !useMQTTViewModel) livePositionsViewModel.requestDelayedGTFSUpdates(
+ 3000
+ )
+ }
+ }
+
+ private fun updateBusPositionsInMap(incomingData: HashMap>){
+ val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle })
+ val vehsOld = HashSet(positionsByVehDict.keys)
+ for (upsWithTrp in incomingData.values){
+ val pos = upsWithTrp.first
+ val vehID = pos.vehicle
+ if (vehsOld.contains(vehID)){
+ //update position
+ moveVehicleToNewPosition(pos)
+ } else{
+ // update it simply
+ positionsByVehDict[vehID] = pos
+ }
+ }
+ //remove old positions
+ vehsOld.removeAll(vehsNew)
+ //now vehsOld contains the vehicles id for those that have NOT been updated
+ val currentTimeStamp = System.currentTimeMillis() /1000
+ for(vehID in vehsOld){
+ //remove after 2 minutes of inactivity
+ if (positionsByVehDict[vehID]!!.timestamp - currentTimeStamp > 2*60){
+ positionsByVehDict.remove(vehID)
+ }
+ }
+
+ //update UI
+ updatePositionsIcons()
+ }
+
+ /**
+ * This is the tricky part, animating the transitions
+ * Basically, we need to set the new positions with the data and redraw them all
+ */
+ private fun moveVehicleToNewPosition(positionUpdate: LivePositionUpdate){
+ if (positionUpdate.vehicle !in positionsByVehDict.keys)
+ return
+ val vehID = positionUpdate.vehicle
+ val currentUpdate = positionsByVehDict[positionUpdate.vehicle]
+ currentUpdate?.let {
+ //cancel current animation on vehicle
+ animatorsByVeh[vehID]?.cancel()
+
+ val currentPos = LatLng(it.latitude, it.longitude)
+ val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
+ val valueAnimator = ValueAnimator.ofObject(MapUtils.LatLngEvaluator(), currentPos, newPos)
+ valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
+ private var latLng: LatLng? = null
+ override fun onAnimationUpdate(animation: ValueAnimator) {
+ latLng = animation.animatedValue as LatLng
+ //update position on animation
+ val update = positionsByVehDict[positionUpdate.vehicle]!!
+ latLng?.let { ll->
+ update.latitude = ll.latitude
+ update.longitude = ll.longitude
+ updatePositionsIcons()
+ }
+ }
+ })
+ /*valueAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ super.onAnimationStart(animation)
+ val update = positionsByVehDict[positionUpdate.vehicle]!!
+ update
+ }
+ })*/
+ //set the new position as the current one but with the old lat and lng
+ positionUpdate.latitude = currentUpdate.latitude
+ positionUpdate.longitude = currentUpdate.longitude
+ positionsByVehDict[vehID] = positionUpdate
+ // valueAnimator.duration = 500
+ valueAnimator.interpolator = LinearInterpolator()
+ valueAnimator.start()
+
+ animatorsByVeh[vehID] = valueAnimator
+
+ } ?: {
+ Log.e(DEBUG_TAG, "Have to run animation for veh ${positionUpdate.vehicle} but not in the dict, adding")
+ positionsByVehDict[positionUpdate.vehicle] = positionUpdate
+ }
+ }
+
+ /**
+ * Update the bus positions displayed on the map, from the existing data
+ */
+ private fun updatePositionsIcons(){
+ val features = ArrayList()//stops.mapNotNull { stop ->
+ //stop.latitude?.let { lat ->
+ // stop.longitude?.let { lon ->
+ for (pos in positionsByVehDict.values){
+ //if (s.latitude!=null && s.longitude!=null)
+ features.add(
+ Feature.fromGeometry(
+ Point.fromLngLat(pos.longitude, pos.latitude),
+ JsonObject().apply {
+ addProperty("veh", pos.vehicle)
+ addProperty("trip", pos.tripID)
+ addProperty("bearing", pos.bearing?.let { it }?:0.0f) // Add routes array to JSON object
+ }
+ )
+ )
+ }
+ busesSource.setGeoJson(FeatureCollection.fromFeatures(features))
+ }
// ------ LOCATION STUFF -----
/*private fun checkAndRequestInitialUserLocation() {
if (ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
requestInitialUserLocation()
} else {
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), LOCATION_PERMISSION_REQUEST_CODE)
}
}
*/
@SuppressLint("MissingPermission")
private fun requestInitialUserLocation() {
val provider :String? = LocationManager.GPS_PROVIDER//getBestLocationProvider()
provider?.let {
setLocationIconEnabled(true)
- Toast.makeText(requireContext(), "Determining the location", Toast.LENGTH_SHORT).show()
+ Toast.makeText(requireContext(), R.string.position_searching_message, Toast.LENGTH_SHORT).show()
locationManager.requestSingleUpdate(it, object : LocationListener {
override fun onLocationChanged(location: Location) {
val userLatLng = LatLng(location.latitude, location.longitude)
val distanceToTarget = userLatLng.distanceTo(DEFAULT_LATLNG)
if (distanceToTarget <= MAX_DIST_KM*1000.0) {
map?.let{
//initMapLocation(mapStyle,map!!,requireContext())
setMapLocationEnabled(true, true, false)
}
} else {
Toast.makeText(context, "You are too far, not showing the position", Toast.LENGTH_SHORT).show()
}
}
override fun onProviderDisabled(provider: String) {}
override fun onProviderEnabled(provider: String) {}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
}, null)
} ?: run {
Toast.makeText(context, "No suitable location provider found.", Toast.LENGTH_SHORT).show()
}
}
/**
* Initialize the map location, but do not enable the component
*/
@SuppressLint("MissingPermission")
private fun initMapLocation(style: Style, map: MapLibreMap, context: Context){
locationComponent = map.locationComponent
val locationComponentOptions =
LocationComponentOptions.builder(context)
.pulseEnabled(true)
.build()
val locationComponentActivationOptions =
buildLocationComponentActivationOptions(style, locationComponentOptions, context)
locationComponent.activateLocationComponent(locationComponentActivationOptions)
locationComponent.isLocationComponentEnabled = false
lastLocation?.let {
if (it.accuracy < 200)
locationComponent.forceLocationUpdate(it)
}
}
private fun buildLocationComponentActivationOptions(
style: Style,
locationComponentOptions: LocationComponentOptions,
context: Context
): LocationComponentActivationOptions {
return LocationComponentActivationOptions
.builder(context, style)
.locationComponentOptions(locationComponentOptions)
.useDefaultLocationEngine(true)
.locationEngineRequest(
LocationEngineRequest.Builder(750)
.setFastestInterval(750)
.setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
.build()
)
.build()
}
private fun haveLocationPermissions(): Boolean{
return !(ActivityCompat.checkSelfPermission(
requireContext(),Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(requireContext(),Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
}
/**
* Handles logic of enabling the user location on the map
*/
@SuppressLint("MissingPermission")
private fun setMapLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) {
if (enabled) {
val permissionOk = assumePermissions || haveLocationPermissions()
if (permissionOk) {
Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions")
locationComponent.isLocationComponentEnabled = true
locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING
setFollowingUser(true)
showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
if (fromClick) Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show()
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
//TODO: show dialog for permission rationale
Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show()
}
Log.d(DEBUG_TAG, "Requesting permission to show user location")
enablingPositionFromClick = fromClick
showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
}
} else{
locationComponent.isLocationComponentEnabled = false
setFollowingUser(false)
showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
if (fromClick) Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show() }
}
private fun setLocationIconEnabled(enabled: Boolean){
if (enabled)
showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
else
showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
}
/**
* Helper method for GUI
*/
private fun updateFollowingIcon(enabled: Boolean){
if(enabled)
followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me_on))
else
followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me))
}
private fun setFollowingUser(following: Boolean){
updateFollowingIcon(following)
followingUserLocation = following
if(following)
ignoreCameraMovementForFollowing = true
}
private fun switchUserLocationStatus(view: View?){
if(locationComponent.isLocationComponentEnabled) setMapLocationEnabled(false, false, true)
else{
Log.d(DEBUG_TAG, "Request enable location")
setMapLocationEnabled(true, false, true)
}
}
companion object {
private const val STOPS_SOURCE_ID = "stops-source"
private const val STOPS_LAYER_ID = "stops-layer"
private const val STOPS_LAYER_SEL_ID ="stops-layer-selected"
private const val BUSES_SOURCE_ID = "buses-source"
private const val BUSES_LAYER_ID = "buses-layer"
private const val STOP_IMAGE_ID ="bus-stop-icon"
private const val DEFAULT_CENTER_LAT = 45.0708
private const val DEFAULT_CENTER_LON = 7.6858
private val DEFAULT_LATLNG = LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)
private const val POSITION_FOUND_ZOOM = 16.5
private const val NO_POSITION_ZOOM = 17.1
private const val MAX_DIST_KM = 90.0
private const val ACCESS_TOKEN="KxO8lF4U3kiO63m0c7lzqDCDrMUVg1OA2JVzRXxxmYSyjugr1xpe4W4Db5rFNvbQ"
private const val MAPLIBRE_URL = "https://api.jawg.io/styles/"
private const val DEBUG_TAG = "BusTO-MapLibreFrag"
private const val STOP_ACTIVE_IMG = "Stop-active"
private const val LOCATION_PERMISSION_REQUEST_CODE = 981202
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment MapLibreFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
MapLibreFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
private fun makeStyleUrl(style: String = "jawg-streets") =
"${MAPLIBRE_URL+ style}.json?access-token=${ACCESS_TOKEN}"
private fun makeStyleMapBoxUrl(dark: Boolean) =
if(dark)
"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
else //"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
"https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
const val OPENFREEMAP_LIBERY = "https://tiles.openfreemap.org/styles/liberty"
const val OPENFREEMAP_BRIGHT = "https://tiles.openfreemap.org/styles/bright"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt
index c76d45d..9e6d7f7 100644
--- a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt
+++ b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt
@@ -1,40 +1,41 @@
package it.reyboz.bustorino.map
import android.animation.ObjectAnimator
import android.util.Log
import androidx.core.content.res.ResourcesCompat
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
import it.reyboz.bustorino.data.gtfs.MatoPattern
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
class BusPositionUtils {
companion object{
@JvmStatic
public fun updateBusPositionMarker(map: MapView, marker: Marker?, posUpdate: LivePositionUpdate,
tripMarkersAnimators: HashMap,
justCreated: Boolean) {
val position: GeoPoint
val updateID = posUpdate.tripID
if (!justCreated) {
position = marker!!.position
if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) {
val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude)
val valueAnimator = MarkerUtils.makeMarkerAnimator(
map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200
)
valueAnimator.setAutoCancel(true)
tripMarkersAnimators.put(updateID, valueAnimator)
valueAnimator.start()
}
//marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()));
} else {
position = GeoPoint(posUpdate.latitude, posUpdate.longitude)
marker!!.position = position
}
- if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f
+ //if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f
+ marker.rotation = posUpdate.bearing?.let { it*-1f } ?: 0.0f
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapUtils.kt b/app/src/main/java/it/reyboz/bustorino/map/MapUtils.kt
new file mode 100644
index 0000000..87f9080
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/map/MapUtils.kt
@@ -0,0 +1,27 @@
+package it.reyboz.bustorino.map
+
+import android.animation.TypeEvaluator
+import org.maplibre.android.geometry.LatLng
+
+
+class MapUtils {
+ companion object{
+ @JvmStatic
+ fun shortestRotation(from: Float, to: Float): Float {
+ var delta = (to - from) % 360
+ if (delta > 180) delta -= 360
+ if (delta < -180) delta += 360
+ return from + delta
+ }
+ }
+
+ //TODO: Do the same for LatLng and bearing, if possible
+ class LatLngEvaluator : TypeEvaluator {
+ private val latLng = LatLng()
+ override fun evaluate(fraction: Float, startValue: LatLng, endValue: LatLng): LatLng {
+ latLng.latitude = startValue.latitude + (endValue.latitude - startValue.latitude) * fraction
+ latLng.longitude = startValue.longitude + (endValue.longitude - startValue.longitude) * fraction
+ return latLng
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt
index 27f3df6..af29f77 100644
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt
@@ -1,287 +1,288 @@
/*
BusTO - ViewModel components
Copyright (C) 2023 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.viewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.android.volley.DefaultRetryPolicy
import com.android.volley.Response
import it.reyboz.bustorino.backend.NetworkVolleyManager
import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
import it.reyboz.bustorino.backend.mato.MQTTMatoClient
import it.reyboz.bustorino.data.GtfsRepository
import it.reyboz.bustorino.data.MatoPatternsDownloadWorker
import it.reyboz.bustorino.data.MatoTripsDownloadWorker
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet
class LivePositionsViewModel(application: Application): AndroidViewModel(application) {
private val gtfsRepo = GtfsRepository(application)
//private val updates = UpdatesMap()
- private val updatesLiveData = MutableLiveData>()
+ private val positionsToBeMatchedLiveData = MutableLiveData>()
private val netVolleyManager = NetworkVolleyManager.getInstance(application)
private var mqttClient = MQTTMatoClient()
private var lineListening = ""
private var lastTimeReceived: Long = 0
private val gtfsRtRequestRunning = MutableLiveData(false)
private val lastFailedTripsRequest = HashMap()
private val workManager = WorkManager.getInstance(application)
private var lastRequestedDownloadTrips = MutableLiveData>()
var isLastWorkResultGood = workManager
.getWorkInfosForUniqueWorkLiveData(MatoTripsDownloadWorker.TAG_TRIPS).map { it ->
if (it.isEmpty()) return@map false
var res = true
if(it[0].state == WorkInfo.State.FAILED){
val currDate = Date()
res = false
lastRequestedDownloadTrips.value?.let { trips->
for(tr in trips){
lastFailedTripsRequest[tr] = currDate
}
}
}
return@map res
}
/**
* Responder to the MQTT Client
*/
private val matoPositionListener = MQTTMatoClient.Companion.MQTTMatoListener{
val mupds = ArrayList()
if(lineListening==MQTTMatoClient.LINES_ALL){
for(sdic in it.values){
for(update in sdic.values){
mupds.add(update)
}
}
} else{
//we're listening to one
if (it.containsKey(lineListening.trim()) ){
for(up in it[lineListening]?.values!!){
mupds.add(up)
}
}
}
val time = System.currentTimeMillis()
if(lastTimeReceived == (0.toLong()) || (time-lastTimeReceived)>500){
- updatesLiveData.postValue(mupds)
+ positionsToBeMatchedLiveData.postValue(mupds)
lastTimeReceived = time
}
}
//find the trip IDs in the updates
- private val tripsIDsInUpdates = updatesLiveData.map { it ->
+ private val tripsIDsInUpdates = positionsToBeMatchedLiveData.map { it ->
//Log.d(DEBUG_TI, "Updates map has keys ${upMap.keys}")
it.map { pos -> "gtt:"+pos.tripID }
}
// get the trip IDs in the DB
private val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap {
//Log.i(DEBUG_TI, "tripsIds in updates: ${it.size}")
gtfsRepo.gtfsDao.getTripPatternStops(it)
}
//trip IDs to query, which are not present in the DB
//REMEMBER TO OBSERVE THIS IN THE MAP
val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns ->
val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID }
Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB")
if (tripsIDsInUpdates.value!=null)
return@map tripsIDsInUpdates.value!!.filter { !(tripNames.contains(it) || it.contains("null"))}
else {
Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??")
return@map ArrayList()
}
}
// unify trips with updates
+ // TODO: Unify trip data inside the LivePositionUpdate
val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns->
Log.i(DEBUG_TI, "Mapping trips and patterns")
val mdict = HashMap>()
//missing patterns
val routesToDownload = HashSet()
- if(updatesLiveData.value!=null)
- for(update in updatesLiveData.value!!){
+ if(positionsToBeMatchedLiveData.value!=null)
+ for(update in positionsToBeMatchedLiveData.value!!){
val trID:String = update.tripID
var found = false
for(trip in tripPatterns){
if (trip.pattern == null){
//pattern is null, which means we have to download
// the pattern data from MaTO
routesToDownload.add(trip.trip.routeID)
}
if (trip.trip.tripID == "gtt:$trID"){
found = true
//insert directly
mdict[trID] = Pair(update,trip)
break
}
}
if (!found){
//Log.d(DEBUG_TI, "Cannot find pattern ${tr}")
//give the update anyway
mdict[trID] = Pair(update,null)
}
}
//have to request download of missing Patterns
if (routesToDownload.size > 0){
Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload")
//downloadMissingPatterns (ArrayList(routesToDownload))
MatoPatternsDownloadWorker.downloadPatternsForRoutes(routesToDownload.toList(), getApplication())
}
return@map mdict
}
fun requestMatoPosUpdates(line: String){
lineListening = line
viewModelScope.launch {
mqttClient.startAndSubscribe(line,matoPositionListener, getApplication())
}
//updatePositions(1000)
}
fun stopMatoUpdates(){
viewModelScope.launch {
val tt = System.currentTimeMillis()
mqttClient.stopMatoRequests(matoPositionListener)
val time = System.currentTimeMillis() -tt
Log.d(DEBUG_TI, "Took $time ms to unsubscribe")
}
}
fun retriggerPositionUpdate(){
- if(updatesLiveData.value!=null){
- updatesLiveData.postValue(updatesLiveData.value)
+ if(positionsToBeMatchedLiveData.value!=null){
+ positionsToBeMatchedLiveData.postValue(positionsToBeMatchedLiveData.value)
}
}
//Gtfs Real time
private val gtfsPositionsReqListener = object: GtfsRtPositionsRequest.Companion.RequestListener{
override fun onResponse(response: ArrayList?) {
Log.i(DEBUG_TI,"Got response from the GTFS RT server")
response?.let {it:ArrayList ->
if (it.size == 0) {
Log.w(DEBUG_TI,"No position updates from the GTFS RT server")
return
}
else {
//Log.i(DEBUG_TI, "Posting value to positionsLiveData")
- viewModelScope.launch { updatesLiveData.postValue(it) }
+ viewModelScope.launch { positionsToBeMatchedLiveData.postValue(it) }
}
}
gtfsRtRequestRunning.postValue(false)
}
}
private val positionRequestErrorListener = Response.ErrorListener {
Log.e(DEBUG_TI, "Could not download the update", it)
gtfsRtRequestRunning.postValue(false)
}
fun requestGTFSUpdates(){
if(gtfsRtRequestRunning.value == null || !gtfsRtRequestRunning.value!!) {
val request = GtfsRtPositionsRequest(positionRequestErrorListener, gtfsPositionsReqListener)
request.setRetryPolicy(
DefaultRetryPolicy(1000,10,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)
)
netVolleyManager.requestQueue.add(request)
Log.i(DEBUG_TI, "Requested GTFS realtime position updates")
gtfsRtRequestRunning.value = true
}
}
fun requestDelayedGTFSUpdates(timems: Long){
viewModelScope.launch {
delay(timems)
requestGTFSUpdates()
}
}
override fun onCleared() {
//stop the MQTT Service
Log.d(DEBUG_TI, "Clearing the live positions view model, stopping the mqttClient")
mqttClient.disconnect()
super.onCleared()
}
//Request trips download
fun downloadTripsFromMato(trips: List): Boolean{
if(trips.isEmpty())
return false
var shouldContinue = false
val currentDateTime = Date().time
for (tr in trips){
if (!lastFailedTripsRequest.containsKey(tr)){
shouldContinue = true
break
} else{
//Log.i(DEBUG_TI, "Last time the trip has failed is ${lastFailedTripsRequest[tr]}")
if ((lastFailedTripsRequest[tr]!!.time - currentDateTime) > MAX_TIME_RETRY){
shouldContinue =true
break
}
}
}
if (shouldContinue) {
//if one trip
val workRequ =MatoTripsDownloadWorker.requestMatoTripsDownload(trips, getApplication(), "BusTO-MatoTripsDown")
workRequ?.let { req ->
Log.d(DEBUG_TI, "Enqueueing new work, saving work info")
lastRequestedDownloadTrips.postValue(trips)
//isLastWorkResultGood =
}
} else{
Log.w(DEBUG_TI, "Requested to fetch data for ${trips.size} trips but they all have failed before in the last $MAX_MINUTES_RETRY mins")
}
return shouldContinue
}
companion object{
private const val DEBUG_TI = "BusTO-LivePosViewModel"
private const val MAX_MINUTES_RETRY = 3
private const val MAX_TIME_RETRY = MAX_MINUTES_RETRY*60*1000 //3 minutes (in milliseconds)
}
}
\ No newline at end of file