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