diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java @@ -21,6 +21,16 @@ import it.reyboz.bustorino.backend.ServiceType; abstract public class GtfsUtils { + + public static String stripGtfsPrefix(String routeID){ + String[] explo = routeID.split(":"); + //default is + String toParse = routeID; + if(explo.length>1) { + toParse = explo[1]; + } + return toParse; + } public static Pair<ServiceType, String> getRouteInfoFromGTFS(String routeID){ String[] explo = routeID.split(":"); //default is diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt @@ -164,8 +164,10 @@ val topic = mapTopic(line) if(elms.isEmpty()) respondersMap.remove(line) - else + else { client.subscribe(topic, QoS.AtMostOnce.value, null, null) + Log.d(DEBUG_TAG, "Resubscribed with topic $topic") + } } Log.d(DEBUG_TAG, "Reconnected to MQTT Mato Client") diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -48,14 +48,10 @@ import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops -import it.reyboz.bustorino.map.BusInfoWindow -import it.reyboz.bustorino.map.BusPositionUtils +import it.reyboz.bustorino.map.* import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder -import it.reyboz.bustorino.map.LocationOverlay -import it.reyboz.bustorino.map.MapViewModel -import it.reyboz.bustorino.map.MarkerUtils import it.reyboz.bustorino.viewmodels.LinesViewModel -import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel +import it.reyboz.bustorino.viewmodels.LivePositionsViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.osmdroid.config.Configuration @@ -129,13 +125,15 @@ private var showOnTopOfLine = true private var recyclerInitDone = false + private var useMQTTPositions = true + //position of live markers private val busPositionMarkersByTrip = HashMap<String,Marker>() private var busPositionsOverlay = FolderOverlay() private val tripMarkersAnimators = HashMap<String, ObjectAnimator>() - private val liveBusViewModel: MQTTPositionsViewModel by viewModels() + private val liveBusViewModel: LivePositionsViewModel by viewModels() @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -164,18 +162,25 @@ stopsRecyclerView.visibility = View.VISIBLE viewModel.setMapShowing(false) - liveBusViewModel.stopPositionsListening() + liveBusViewModel.stopMatoUpdates() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) } else{ stopsRecyclerView.visibility = View.GONE map.visibility = View.VISIBLE viewModel.setMapShowing(true) - liveBusViewModel.requestPosUpdates(lineID) + if(useMQTTPositions) + liveBusViewModel.requestMatoPosUpdates(lineID) + else + liveBusViewModel.requestGTFSUpdates() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) } } viewModel.setRouteIDQuery(lineID) + val keySourcePositions = getString(R.string.pref_positions_source) + useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions, "mqtt").contentEquals("mqtt") + viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } @@ -229,6 +234,7 @@ //DO NOTHING return@observe } + val filtdLineID = GtfsUtils.stripGtfsPrefix(lineID) //filter buses with direction, show those only with the same direction val outmap = HashMap<String, Pair<LivePositionUpdate, TripAndPatternWithStops?>>() val currentPattern = viewingPattern!!.pattern @@ -236,21 +242,28 @@ Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}") val patternsDirections = HashMap<String,Int>() for((tripId, pair) in it.entries){ + //remove trips with wrong line ideas + if(pair.first.routeID!=filtdLineID) + continue if(pair.second!=null && pair.second?.pattern !=null){ val dir = pair.second?.pattern?.directionId if(dir !=null && dir == currentPattern.directionId){ - outmap.set(tripId, pair) + outmap[tripId] = pair } patternsDirections.set(tripId,if (dir!=null) dir else -10) } else{ outmap[tripId] = pair //Log.d(DEBUG_TAG, "No pattern for tripID: $tripId") - patternsDirections.set(tripId, -10) + patternsDirections[tripId] = -10 } } Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n updateBusPositionsInMap(outmap) + //if not using MQTT positions + if(!useMQTTPositions){ + liveBusViewModel.requestDelayedGTFSUpdates(2000) + } } //download missing tripIDs @@ -653,7 +666,16 @@ Log.d(DEBUG_TAG, "Resetting paused from onResume") pausedFragment = false - liveBusViewModel.requestPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + val keySourcePositions = getString(R.string.pref_positions_source) + useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions, "mqtt").contentEquals("mqtt") + + //separate paths + if(useMQTTPositions) + liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + else + liveBusViewModel.requestGTFSUpdates() + if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ @@ -677,7 +699,7 @@ override fun onPause() { super.onPause() - liveBusViewModel.stopPositionsListening() + liveBusViewModel.stopMatoUpdates() pausedFragment = true //save map val center = map.mapCenter diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -50,7 +50,8 @@ import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops; import it.reyboz.bustorino.map.*; -import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel; +import it.reyboz.bustorino.viewmodels.GtfsPositionsViewModel; +import it.reyboz.bustorino.viewmodels.LivePositionsViewModel; import it.reyboz.bustorino.viewmodels.StopsMapViewModel; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; @@ -120,8 +121,9 @@ //the ViewModel from which we get the stop to display in the map private StopsMapViewModel stopsViewModel; - //private GTFSPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); - private MQTTPositionsViewModel positionsViewModel; + //private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); + private LivePositionsViewModel livePositionsViewModel; + private Boolean useMQTTViewModel = true; private final HashMap<String,Marker> busPositionMarkersByTrip = new HashMap<>(); private FolderOverlay busPositionsOverlay = null; @@ -230,6 +232,11 @@ //set map not done hasMapStartFinished = false; + String keySourcePositions=getString(R.string.pref_positions_source); + useMQTTViewModel = ( + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions,"mqtt").contentEquals("mqtt")); + //Start map from bundle if (savedInstanceState !=null) @@ -283,11 +290,12 @@ public void onAttach(@NonNull Context context) { super.onAttach(context); - //gtfsPosViewModel = new ViewModelProvider(this).get(GTFSPositionsViewModel.class); - //viewModel ViewModelProvider provider = new ViewModelProvider(this); - positionsViewModel = provider.get(MQTTPositionsViewModel.class); + //gtfsPosViewModel = provider.get(GtfsPositionsViewModel.class); + livePositionsViewModel = provider.get(LivePositionsViewModel.class); stopsViewModel = provider.get(StopsMapViewModel.class); + + if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { @@ -316,7 +324,7 @@ } } tripMarkersAnimators.clear(); - positionsViewModel.stopPositionsListening(); + if(useMQTTViewModel) livePositionsViewModel.stopMatoUpdates(); } @@ -347,21 +355,58 @@ bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); } + @Override public void onResume() { super.onResume(); + //TODO: cleanup duplicate code (maybe merging the positions classes?) if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); - if(positionsViewModel !=null) { + /// choose which to use + String keySourcePositions=getString(R.string.pref_positions_source); + useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions,"mqtt").contentEquals("mqtt"); + if(livePositionsViewModel !=null) { //gtfsPosViewModel.requestUpdates(); - positionsViewModel.requestPosUpdates(MQTTMatoClient.LINES_ALL); + if(useMQTTViewModel) + livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL); + else + livePositionsViewModel.requestGTFSUpdates(); //mapViewModel.testCascade(); - positionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { + livePositionsViewModel.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, + requireContext().getApplicationContext(), + "BusTO-MatoTripDownload"); + }); + } /*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"); }); } + */ + else Log.e(DEBUG_TAG, "livePositionsViewModel is null at onResume"); + } + + private void startRequestsPositions(){ + if (livePositionsViewModel != null) { + //should always be the case + livePositionsViewModel.getUpdatesWithTripAndPatterns().observe(getViewLifecycleOwner(), data -> { + 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 @@ -543,19 +588,7 @@ //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); busPositionsOverlay = new FolderOverlay(); } - - - if(positionsViewModel !=null){ - //should always be the case - positionsViewModel.getUpdatesWithTripAndPatterns().observe(getViewLifecycleOwner(), data->{ - Log.d(DEBUG_TAG, "Have "+data.size()+" trip updates, has Map start finished: "+hasMapStartFinished); - if (hasMapStartFinished) updateBusPositionsInMap(data); - //if(!isDetached()) - // gtfsPosViewModel.requestDelayedUpdates(4000); - }); - } else { - Log.e(DEBUG_TAG, "PositionsViewModel is null"); - } + startRequestsPositions(); if(stopsViewModel !=null){ stopsViewModel.getStopsInBoundingBox().observe(getViewLifecycleOwner(), diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt @@ -12,7 +12,7 @@ import androidx.fragment.app.viewModels import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.mato.MQTTMatoClient -import it.reyboz.bustorino.viewmodels.MQTTPositionsViewModel +import it.reyboz.bustorino.viewmodels.LivePositionsViewModel /** @@ -30,7 +30,7 @@ private lateinit var lineEditText: EditText - private val mqttViewModel: MQTTPositionsViewModel by viewModels() + private val mqttViewModel: LivePositionsViewModel by viewModels() /*private val requestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ override fun onResponse(response: ArrayList<GtfsPositionUpdate>?) { @@ -91,12 +91,12 @@ */ subscribed = if(subscribed){ //mqttMatoClient.desubscribe(listener) - mqttViewModel.stopPositionsListening() + mqttViewModel.stopMatoUpdates() buttonLaunch.text="Start" false } else{ //mqttMatoClient.startAndSubscribe(lineEditText.text.trim().toString(), listener) - mqttViewModel.requestPosUpdates(lineEditText.text.trim().toString()) + mqttViewModel.requestMatoPosUpdates(lineEditText.text.trim().toString()) buttonLaunch.text="Stop" true } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/GTFSPositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/GtfsPositionsViewModel.kt rename from app/src/main/java/it/reyboz/bustorino/viewmodels/GTFSPositionsViewModel.kt rename to app/src/main/java/it/reyboz/bustorino/viewmodels/GtfsPositionsViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/GTFSPositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/GtfsPositionsViewModel.kt @@ -32,7 +32,7 @@ /** * View Model for the map. For containing the stops, the trips and whatever */ -class GTFSPositionsViewModel(application: Application): AndroidViewModel(application) { +class GtfsPositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) private val netVolleyManager = NetworkVolleyManager.getInstance(application) @@ -62,11 +62,7 @@ } private val positionRequestErrorListener = Response.ErrorListener { - //error listener, it->VolleyError Log.e(DEBUG_TI, "Could not download the update, error:\n"+it.stackTrace) - - //TODO: launch again if needed - } fun requestUpdates(){ diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt rename from app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt rename to app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt @@ -20,29 +20,38 @@ import android.app.Application import android.util.Log import androidx.lifecycle.* +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.gtfs.TripAndPatternWithStops +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -typealias UpdatesMap = HashMap<String, LivePositionUpdate> - -class MQTTPositionsViewModel(application: Application): AndroidViewModel(application) { +class LivePositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) //private val updates = UpdatesMap() private val updatesLiveData = MutableLiveData<ArrayList<LivePositionUpdate>>() + private val netVolleyManager = NetworkVolleyManager.getInstance(application) + private var mqttClient = MQTTMatoClient.getInstance() private var lineListening = "" private var lastTimeReceived: Long = 0 - private val positionListener = MQTTMatoClient.Companion.MQTTMatoListener{ + private val gtfsRtRequestRunning = MutableLiveData<Boolean>(false) + + /** + * Responder to the MQTT Client + */ + private val matoPositionListener = MQTTMatoClient.Companion.MQTTMatoListener{ val mupds = ArrayList<LivePositionUpdate>() if(lineListening==MQTTMatoClient.LINES_ALL){ @@ -132,20 +141,20 @@ } - fun requestPosUpdates(line: String){ + fun requestMatoPosUpdates(line: String){ lineListening = line viewModelScope.launch { - mqttClient.startAndSubscribe(line,positionListener, getApplication()) + mqttClient.startAndSubscribe(line,matoPositionListener, getApplication()) } //updatePositions(1000) } - fun stopPositionsListening(){ + fun stopMatoUpdates(){ viewModelScope.launch { val tt = System.currentTimeMillis() - mqttClient.desubscribe(positionListener) + mqttClient.desubscribe(matoPositionListener) val time = System.currentTimeMillis() -tt Log.d(DEBUG_TI, "Took $time ms to unsubscribe") } @@ -157,8 +166,51 @@ updatesLiveData.postValue(updatesLiveData.value) } } + //Gtfs Real time + + + private val gtfsPositionsReqListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ + override fun onResponse(response: ArrayList<LivePositionUpdate>?) { + Log.i(DEBUG_TI,"Got response from the GTFS RT server") + response?.let {it:ArrayList<LivePositionUpdate> -> + 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) } + + } + } + gtfsRtRequestRunning.postValue(false) + + } + + } + private val positionRequestErrorListener = Response.ErrorListener { + Log.e(DEBUG_TI, "Could not download the update, error:\n"+it.stackTrace) + gtfsRtRequestRunning.postValue(false) + } + + fun requestGTFSUpdates(){ + if(gtfsRtRequestRunning.value == null || !gtfsRtRequestRunning.value!!) { + val request = GtfsRtPositionsRequest(positionRequestErrorListener, gtfsPositionsReqListener) + 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() + } + } companion object{ private const val DEBUG_TI = "BusTO-MQTTLiveData" } } \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -206,7 +206,12 @@ <string name="pref_shown_startup">Schermata da mostrare all\'avvio</string> <string name="pref_shown_startup_def_desc">Tocca per cambiare</string> + <string name="positions_source_pref_title">Fonte posizioni in tempo reale di bus e tram</string> + <array name="positions_source_sel"> + <item>MaTO (aggiornate più spesso, può non funzionare)</item> + <item>GTFS RT (più stabile)</item> + </array> <!-- lines --> <string name="long_press_stop_4_options">Tocca a lungo la fermata per le opzioni</string> diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -23,4 +23,11 @@ <item>gttjsonfetcher</item> <item>fivetscraper</item> </array> + + <string name="pref_positions_source">pref_positions_source</string> + <array name="positions_source_values"> + <item>mqtt</item> + <item>gtfsrt</item> + </array> + </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -243,6 +243,12 @@ <item>@string/lines</item> </array> + <string name="positions_source_pref_title">Source of real time positions for buses and trams</string> + <array name="positions_source_sel"> + <item>MaTO (updated more frequently, might be offline)</item> + <item>GTFS RT (more stable, less frequently updated)</item> + </array> + <string name="remove_all_trips">Remove all GTFS trips info</string> <string name="all_trips_removed">All GTFS trips have been removed from the database</string> </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -10,6 +10,13 @@ android:title="@string/pref_shown_startup" android:summary="%s" /> + + <androidx.preference.ListPreference + android:key="@string/pref_positions_source" + android:entries="@array/positions_source_sel" + android:entryValues="@array/positions_source_values" + android:title="@string/positions_source_pref_title" + /> </androidx.preference.PreferenceCategory> <androidx.preference.PreferenceCategory