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 index 385de81..caf7657 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt @@ -1,323 +1,329 @@ package it.reyboz.bustorino.backend.mato import android.content.Context import android.util.Log import androidx.lifecycle.LifecycleOwner import info.mqtt.android.service.Ack import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.eclipse.paho.client.mqttv3.* import info.mqtt.android.service.MqttAndroidClient import info.mqtt.android.service.QoS import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence import org.json.JSONArray import org.json.JSONException import java.lang.ref.WeakReference import java.util.ArrayList import java.util.Properties typealias PositionsMap = HashMap > class MQTTMatoClient private constructor(): MqttCallbackExtended{ private var isStarted = false private var subscribedToAll = false private lateinit var client: MqttAndroidClient //private var clientID = "" private val respondersMap = HashMap>>() private val currentPositions = PositionsMap() private lateinit var lifecycle: LifecycleOwner private var context: Context?= null private fun connect(context: Context, iMqttActionListener: IMqttActionListener?){ val clientID = "mqttjs_${getRandomString(8)}" client = MqttAndroidClient(context,SERVER_ADDR,clientID,Ack.AUTO_ACK) val options = MqttConnectOptions() //options.sslProperties = options.isCleanSession = true val headersPars = Properties() headersPars.setProperty("Origin","https://mato.muoversiatorino.it") headersPars.setProperty("Host","mapi.5t.torino.it") options.customWebSocketHeaders = headersPars //actually connect client.connect(options,null, iMqttActionListener) isStarted = true client.setCallback(this) if (this.context ==null) this.context = context.applicationContext } override fun connectComplete(reconnect: Boolean, serverURI: String?) { Log.d(DEBUG_TAG, "Connected to server, reconnect: $reconnect") Log.d(DEBUG_TAG, "Have listeners: $respondersMap") } fun startAndSubscribe(lineId: String, responder: MQTTMatoListener, context: Context): Boolean{ //start the client, and then subscribe to the topic val topic = mapTopic(lineId) synchronized(this) { if(!isStarted){ connect(context, object : IMqttActionListener{ override fun onSuccess(asyncActionToken: IMqttToken?) { val disconnectedBufferOptions = DisconnectedBufferOptions() disconnectedBufferOptions.isBufferEnabled = true disconnectedBufferOptions.bufferSize = 100 disconnectedBufferOptions.isPersistBuffer = false disconnectedBufferOptions.isDeleteOldestMessages = false client.setBufferOpts(disconnectedBufferOptions) client.subscribe(topic, QoS.AtMostOnce.value) } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.e(DEBUG_TAG, "FAILED To connect to the server") } }) //wait for connection } else { client.subscribe(topic, QoS.AtMostOnce.value) } } synchronized(this){ if (!respondersMap.contains(lineId)) respondersMap[lineId] = ArrayList() respondersMap[lineId]!!.add(WeakReference(responder)) Log.d(DEBUG_TAG, "Add MQTT Listener for line $lineId, topic $topic") } return true } fun desubscribe(responder: MQTTMatoListener){ var removed = false for ((line,v)in respondersMap.entries){ var done = false for (el in v){ if (el.get()==null){ v.remove(el) } else if(el.get() == responder){ v.remove(el) done = true } if (done) break } if(done) Log.d(DEBUG_TAG, "Removed one listener for line $line, listeners: $v") //if (done) break if (v.isEmpty()){ //actually unsubscribe client.unsubscribe( mapTopic(line)) } removed = done || removed } + // remove lines that have no responders + for(line in respondersMap.keys){ + if(respondersMap[line]?.isEmpty() == true){ + respondersMap.remove(line) + } + } Log.d(DEBUG_TAG, "Removed: $removed, respondersMap: $respondersMap") } fun getPositions(): PositionsMap{ return currentPositions } fun sendUpdateToResponders(responders: ArrayList>): Boolean{ var sent = false for (wrD in responders) if (wrD.get() == null) responders.remove(wrD) else { wrD.get()!!.onUpdateReceived(currentPositions) sent = true } return sent } override fun connectionLost(cause: Throwable?) { Log.w(DEBUG_TAG, "Lost connection in MQTT Mato Client") synchronized(this){ // isStarted = false //var i = 0 // while(i < 20 && !isStarted) { connect(context!!, object: IMqttActionListener{ override fun onSuccess(asyncActionToken: IMqttToken?) { //relisten to messages for ((line,elms) in respondersMap.entries){ val topic = mapTopic(line) if(elms.isEmpty()) respondersMap.remove(line) else client.subscribe(topic, QoS.AtMostOnce.value, null, null) } Log.d(DEBUG_TAG, "Reconnected to MQTT Mato Client") } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.w(DEBUG_TAG, "Failed to reconnect to MQTT server") } }) } } override fun messageArrived(topic: String?, message: MqttMessage?) { if (topic==null || message==null) return parseMessageAndAddToList(topic, message) //GlobalScope.launch { } } private fun parseMessageAndAddToList(topic: String, message: MqttMessage){ val vals = topic.split("/") val lineId = vals[1] val vehicleId = vals[2] val timestamp = (System.currentTimeMillis() / 1000 ) as Long val messString = String(message.payload) try { val jsonList = JSONArray(messString) //val full = if(jsonList.length()>7) { // if (jsonList.get(7).equals(null)) null else jsonList.getInt(7) //}else null /*val posUpdate = MQTTPositionUpdate(lineId+"U", vehicleId, jsonList.getDouble(0), jsonList.getDouble(1), if(jsonList.get(2).equals(null)) null else jsonList.getInt(2), if(jsonList.get(3).equals(null)) null else jsonList.getInt(3), if(jsonList.get(4).equals(null)) null else jsonList.getString(4)+"U", if(jsonList.get(5).equals(null)) null else jsonList.getInt(5), if(jsonList.get(6).equals(null)) null else jsonList.getInt(6), //full ) */ if(jsonList.get(4)==null){ Log.d(DEBUG_TAG, "We have null tripId: line $lineId veh $vehicleId: $jsonList") return } val posUpdate = LivePositionUpdate( jsonList.getString(4)+"U", null, null, lineId+"U", vehicleId, jsonList.getDouble(0), //latitude jsonList.getDouble(1), //longitude if(jsonList.get(2).equals(null)) null else jsonList.getInt(2).toFloat(), //"heading" (same as bearing?) timestamp, if(jsonList.get(6).equals(null)) null else jsonList.getInt(6).toString() //nextStop ) //add update var valid = false if(!currentPositions.contains(lineId)) currentPositions[lineId] = HashMap() currentPositions[lineId]?.let{ it[vehicleId] = posUpdate valid = true } var sent = false if (LINES_ALL in respondersMap.keys) { sent = sendUpdateToResponders(respondersMap[LINES_ALL]!!) } if(lineId in respondersMap.keys){ sent = sendUpdateToResponders(respondersMap[lineId]!!) or sent } if(!sent){ Log.w(DEBUG_TAG, "We have received an update but apparently there is no one to send it") var emptyResp = true for(en in respondersMap.values){ if(!en.isEmpty()){ emptyResp=false break } } //try unsubscribing to all if(emptyResp) { Log.d(DEBUG_TAG, "Unsubscribe all") client.unsubscribe(LINES_ALL) } } //Log.d(DEBUG_TAG, "We have update on line $lineId, vehicle $vehicleId") } catch (e: JSONException){ Log.e(DEBUG_TAG,"Cannot decipher message on topic $topic, line $lineId, veh $vehicleId") e.printStackTrace() } } override fun deliveryComplete(token: IMqttDeliveryToken?) { //NOT USED (we're not sending any messages) } companion object{ const val SERVER_ADDR="wss://mapi.5t.torino.it:443/scre" const val LINES_ALL="ALL" private const val DEBUG_TAG="BusTO-MatoMQTT" @Volatile private var instance: MQTTMatoClient? = null fun getInstance() = instance?: synchronized(this){ instance?: MQTTMatoClient().also { instance= it } } @JvmStatic fun mapTopic(lineId: String): String{ return if(lineId== LINES_ALL || lineId == "#") "#" else{ "/${lineId}/#" } } fun getRandomString(length: Int) : String { val allowedChars = ('a'..'f') + ('0'..'9') return (1..length) .map { allowedChars.random() } .joinToString("") } fun interface MQTTMatoListener{ //positionsMap is a dict with line -> vehicle -> Update fun onUpdateReceived(posUpdates: PositionsMap) } } } data class MQTTPositionUpdate( val lineId: String, val vehicleId: String, val latitude: Double, val longitude: Double, val heading: Int?, val speed: Int?, val tripId: String?, val direct: Int?, val nextStop: Int?, //val full: Int? ) \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.java b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.java deleted file mode 100644 index c521381..0000000 --- a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - BusTO - Data components - 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.data; - -import android.database.sqlite.SQLiteDatabase; -import it.reyboz.bustorino.backend.Result; -import it.reyboz.bustorino.backend.Stop; - -import java.util.List; -import java.util.concurrent.Executor; - -public class OldDataRepository { - - private final Executor executor; - private final NextGenDB nextGenDB; - - public OldDataRepository(Executor executor, final NextGenDB nextGenDB) { - this.executor = executor; - this.nextGenDB = nextGenDB; - } - - public void requestStopsWithGtfsIDs(final List gtfsIDs, - final Callback> callback){ - executor.execute(() -> { - - try { - //final NextGenDB dbHelper = new NextGenDB(context); - final SQLiteDatabase db = nextGenDB.getReadableDatabase(); - - final List stops = NextGenDB.queryAllStopsWithGtfsIDs(db, gtfsIDs); - //Result> result = Result.success; - - callback.onComplete(Result.success(stops)); - } catch (Exception e){ - callback.onComplete(Result.failure(e)); - } - }); - } - - public interface Callback{ - void onComplete(Result result); - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt new file mode 100644 index 0000000..380b67d --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt @@ -0,0 +1,69 @@ +/* + BusTO - Data components + 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.data + +import android.content.Context +import it.reyboz.bustorino.backend.Result +import it.reyboz.bustorino.backend.Stop +import java.util.concurrent.Executor + +class OldDataRepository(private val executor: Executor, private val nextGenDB: NextGenDB) { + + constructor(executor: Executor, context: Context): this(executor, NextGenDB.getInstance(context)) + fun requestStopsWithGtfsIDs( + gtfsIDs: List?, + callback: Callback> + ) { + executor.execute { + try { + //final NextGenDB dbHelper = new NextGenDB(context); + val db = nextGenDB.readableDatabase + val stops: List = NextGenDB.queryAllStopsWithGtfsIDs(db, gtfsIDs) + //Result> result = Result.success; + callback.onComplete(Result.success(stops)) + } catch (e: Exception) { + callback.onComplete(Result.failure(e)) + } + } + } + + fun requestStopsInArea( + latitFrom: Double, + latitTo: Double, + longitFrom: Double, + longitTo: Double, + callback: Callback> + ){ + //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); + executor.execute { + val stops = nextGenDB.queryAllInsideMapView( + latitFrom, latitTo, + longitFrom, longitTo + ) + if (stops!=null) + callback.onComplete(Result.success(stops)) + } + + } + + + + fun interface Callback { + fun onComplete(result: Result) + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java index 444934b..3a0c77c 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,285 +1,285 @@ /* BusTO (fragments) Copyright (C) 2018 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.content.Context; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import android.os.AsyncTask; import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.middleware.*; import java.lang.ref.WeakReference; import java.util.List; /** * Helper class to manage the fragments and their needs */ public class FragmentHelper { //GeneralActivity act; private final FragmentListenerMain listenerMain; private final WeakReference managerWeakRef; private Stop lastSuccessfullySearchedBusStop; //support for multiple frames private final int secondaryFrameLayout; private final int primaryFrameLayout; private final Context context; public static final int NO_FRAME = -3; private static final String DEBUG_TAG = "BusTO FragmHelper"; private WeakReference lastTaskRef; private boolean shouldHaltAllActivities=false; public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) { this(listener,framan, context,mainFrame,NO_FRAME); } public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) { this.listenerMain = listener; this.managerWeakRef = new WeakReference<>(fraMan); this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; this.context = context.getApplicationContext(); } /** * Get the last successfully searched bus stop or NULL * * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { this.lastSuccessfullySearchedBusStop = stop; } public void setLastTaskRef(AsyncTask task) { this.lastTaskRef = new WeakReference<>(task); } /** * Called when you need to create a fragment for a specified Palina * @param p the Stop that needs to be displayed */ public void createOrUpdateStopFragment(Palina p, boolean addToBackStack){ boolean sameFragment; ArrivalsFragment arrivalsFragment = null; if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } FragmentManager fm = managerWeakRef.get(); if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment) { arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); //Log.d(DEBUG_TAG, "Arrivals are for fragment with same stop?"); if (arrivalsFragment == null) sameFragment = false; else sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); } else { sameFragment = false; Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment"); } setLastSuccessfullySearchedBusStop(p); if (sameFragment){ Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); if (arrivalsFragment == null) sameFragment = false; } if(!sameFragment) { //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); if (displayName != null && displayName.length() > 0) { arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); } else { arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } String probableTag = ArrivalsFragment.getFragmentTag(p); attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack)); } // DO NOT CALL `setListAdapter` ever on arrivals fragment arrivalsFragment.updateFragmentData(p); // enable fragment auto refresh arrivalsFragment.setReloadOnResume(true); listenerMain.hideKeyboard(); toggleSpinner(false); } /** * Called when you need to display the results of a search of stops * @param resultList the List of stops found * @param query String queried */ public void createStopListFragment(List resultList, String query, boolean addToBackStack){ listenerMain.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } attachFragmentToContainer(managerWeakRef.get(),listfragment, new AttachParameters("search_"+query, false,addToBackStack)); listfragment.setStopList(resultList); - listenerMain.readyGUIfor(FragmentKind.STOPS); + //listenerMain.readyGUIfor(FragmentKind.STOPS); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ listenerMain.toggleSpinner(on); } /** * Attach a new fragment to a cointainer * @param fm the FragmentManager * @param fragment the Fragment * @param parameters attach parameters */ protected void attachFragmentToContainer(FragmentManager fm,Fragment fragment, AttachParameters parameters){ if(shouldHaltAllActivities) //nothing to do return; FragmentTransaction ft = fm.beginTransaction(); int frameID; if(parameters.attachToSecondaryFrame && secondaryFrameLayout!=NO_FRAME) // ft.replace(secondaryFrameLayout,fragment,tag); frameID = secondaryFrameLayout; else frameID = primaryFrameLayout; switch (parameters.transaction){ case REPLACE: ft.replace(frameID,fragment,parameters.tag); } if (parameters.addToBackStack) ft.addToBackStack("state_"+parameters.tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); if(!fm.isDestroyed() && !shouldHaltAllActivities) ft.commit(); //fm.executePendingTransactions(); } public synchronized void setBlockAllActivities(boolean shouldI) { this.shouldHaltAllActivities = shouldI; } public void stopLastRequestIfNeeded(boolean interruptIfRunning){ if(lastTaskRef == null) return; AsyncTask task = lastTaskRef.get(); if(task!=null){ task.cancel(interruptIfRunning); } } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ public void showErrorMessage(Fetcher.Result res, SearchRequestType type){ //TODO: implement a common set of errors for all fragments if (res==null){ Log.e(DEBUG_TAG, "Asked to show result with null result"); return; } Log.d(DEBUG_TAG, "Showing result for "+res); switch (res){ case OK: break; case CLIENT_OFFLINE: showToastMessage(R.string.network_error, true); break; case SERVER_ERROR: if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true); } else { showToastMessage(R.string.network_error, true); } case PARSER_ERROR: default: showShortToast(R.string.internal_error); break; case QUERY_TOO_SHORT: showShortToast(R.string.query_too_short); break; case EMPTY_RESULT_SET: if (type == SearchRequestType.STOPS) showShortToast(R.string.no_bus_stop_have_this_name); else if(type == SearchRequestType.ARRIVALS){ showShortToast(R.string.no_arrivals_stop); } break; case NOT_FOUND: showShortToast(R.string.no_bus_stop_have_this_name); break; } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; if (context != null) Toast.makeText(context, messageID, length).show(); } private void showShortToast(int messageID){ showToastMessage(messageID, true); } enum Transaction{ REPLACE, } static final class AttachParameters { String tag; boolean attachToSecondaryFrame; Transaction transaction; boolean addToBackStack; public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.transaction = transaction; this.addToBackStack = addToBackStack; } public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.addToBackStack = addToBackStack; this.transaction = Transaction.REPLACE; } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index c9d6786..7673975 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,725 +1,728 @@ /* BusTO - Fragments 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.fragments import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context import android.graphics.Paint import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.GtfsUtils import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.PolylineParser import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.MatoTripsDownloadWorker 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.CustomInfoWindow.TouchResponder 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 kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.BoundingBox 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.Polyline import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList class LinesDetailFragment() : ScreenBaseFragment() { private lateinit var lineID: String private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null //private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List private lateinit var map: MapView private var viewingPattern: MatoPatternWithStops? = null private val viewModel: LinesViewModel by viewModels() private val mapViewModel: MapViewModel by viewModels() private var firstInit = true private var pausedFragment = false private lateinit var switchButton: ImageButton private lateinit var stopsRecyclerView: RecyclerView //adapter for recyclerView private val stopAdapterListener= object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { fragmentListener?.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(fragmentListener == null){ Log.e(DEBUG_TAG, "Fragment listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { TODO("Not yet implemented") } } private var polyline: Polyline? = null //private var stopPosList = ArrayList() private lateinit var stopsOverlay: FolderOverlay //fragment actions private lateinit var fragmentListener: CommonFragmentListener private val stopTouchResponder = TouchResponder { stopID, stopName -> Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") fragmentListener.requestArrivalsForStopID(stopID) } private var showOnTopOfLine = true private var recyclerInitDone = false //position of live markers private val busPositionMarkersByTrip = HashMap() private var busPositionsOverlay = FolderOverlay() private val tripMarkersAnimators = HashMap() private val liveBusViewModel: MQTTPositionsViewModel by viewModels() @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) lineID = requireArguments().getString(LINEID_KEY, "") switchButton = rootView.findViewById(R.id.switchImageButton) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) val titleTextView = rootView.findViewById(R.id.titleTextView) titleTextView.text = getString(R.string.line)+" "+GtfsUtils.getLineNameFromGtfsID(lineID) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter initializeMap(rootView) initializeRecyclerView() switchButton.setOnClickListener{ if(map.visibility == View.VISIBLE){ map.visibility = View.GONE stopsRecyclerView.visibility = View.VISIBLE viewModel.setMapShowing(false) liveBusViewModel.stopPositionsListening() 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) switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) } } viewModel.setRouteIDQuery(lineID) viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } /* We have the pattern and the stops here, time to display them */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> if(map.visibility ==View.VISIBLE) showPatternWithStopsOnMap(stops) else{ if(stopsRecyclerView.visibility==View.VISIBLE) showStopsAsList(stops) } } if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){ val patt = viewModel.selectedPatternLiveData.value!! Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}") showPattern(patt) pausedFragment = false } Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") //listeners patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = currentPatterns.get(position) //viewModel.setPatternToDisplay(patternWithStops) setPatternAndReqStops(patternWithStops) Log.d(DEBUG_TAG, "item Selected, cleaning bus markers") if(map?.visibility == View.VISIBLE) { busPositionsOverlay.closeAllInfoWindows() busPositionsOverlay.items.clear() busPositionMarkersByTrip.clear() stopAnimations() tripMarkersAnimators.clear() liveBusViewModel.retriggerPositionUpdate() } } override fun onNothingSelected(p0: AdapterView<*>?) { } } //live bus positions liveBusViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ if(map.visibility == View.GONE || viewingPattern ==null){ //DO NOTHING return@observe } //filter buses with direction, show those only with the same direction val outmap = HashMap>() val currentPattern = viewingPattern!!.pattern val numUpds = it.entries.size Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}") val patternsDirections = HashMap() for((tripId, pair) in it.entries){ if(pair.second!=null && pair.second?.pattern !=null){ val dir = pair.second?.pattern?.directionId if(dir !=null && dir == currentPattern.directionId){ outmap.set(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) } } Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n updateBusPositionsInMap(outmap) } //download missing tripIDs liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.downloadTripsFromMato( it, requireContext().applicationContext, "BusTO-MatoTripDownload" ) } return rootView } private fun initializeMap(rootView : View){ val ctx = requireContext().applicationContext Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)) map = rootView.findViewById(R.id.lineMap) map.let { it.setTileSource(TileSourceFactory.MAPNIK) /* object : OnlineTileSourceBase("USGS Topo", 0, 18, 256, "", arrayOf("https://basemap.nationalmap.gov/ArcG IS/rest/services/USGSTopo/MapServer/tile/" )) { override fun getTileURLString(pMapTileIndex: Long) : String{ return baseUrl + MapTileIndex.getZoom(pMapTileIndex)+"/" + MapTileIndex.getY(pMapTileIndex) + "/" + MapTileIndex.getX(pMapTileIndex)+ mImageFilenameEnding; } } */ stopsOverlay = FolderOverlay() busPositionsOverlay = FolderOverlay() //map.setTilesScaledToDpi(true); //map.setTilesScaledToDpi(true); it.setFlingEnabled(true) it.setUseDataConnection(true) // add ability to zoom with 2 fingers it.setMultiTouchControls(true) it.minZoomLevel = 11.0 //map controller setup val mapController = it.controller var zoom = 12.0 var centerMap = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ " zoom ${mapViewModel.currentZoom.value}") zoom = mapViewModel.currentZoom.value!! centerMap = GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!) /*viewLifecycleOwner.lifecycleScope.launch { delay(100) Log.d(DEBUG_TAG, "zooming back to point") controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), mapViewModel.currentZoom.value!!,null,null) //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) //controller.setZoom(mapViewModel.currentZoom.value!!) */ } mapController.setZoom(zoom) mapController.setCenter(centerMap) Log.d(DEBUG_TAG, "Initializing map, first init $firstInit") //map.invalidate() it.overlayManager.add(stopsOverlay) it.overlayManager.add(busPositionsOverlay) zoomToCurrentPattern() firstInit = false } } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") - fragmentListener.readyGUIfor(FragmentKind.LINES) } private fun stopAnimations(){ for(anim in tripMarkersAnimators.values){ anim.cancel() } } private fun savePatternsToShow(patterns: List){ currentPatterns = patterns.sortedBy { p-> p.pattern.code } patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } viewingPattern?.let { showPattern(it) } } /** * Called when the position of the spinner is updated */ private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") viewModel.selectedPatternLiveData.value = patternWithStops viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewingPattern = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } private fun showPattern(patternWs: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() for(k in currentPatterns.indices){ if(currentPatterns[k].pattern.code.trim() == code){ pos = k break } } Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") if(pos>=0) patternsSpinner.setSelection(pos) //set pattern setPatternAndReqStops(patternWs) } private fun zoomToCurrentPattern(){ var pointsList: List if(viewingPattern==null) { Log.e(DEBUG_TAG, "asked to zoom to pattern but current viewing pattern is null") if(polyline!=null) pointsList = polyline!!.actualPoints else { Log.d(DEBUG_TAG, "The polyline is null") return } }else{ val pattern = viewingPattern!!.pattern pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) } var maxLat = -4000.0 var minLat = -4000.0 var minLong = -4000.0 var maxLong = -4000.0 for (p in pointsList){ // get max latitude if(maxLat == -4000.0) maxLat = p.latitude else if (maxLat < p.latitude) maxLat = p.latitude // find min latitude if (minLat == -4000.0) minLat = p.latitude else if (minLat > p.latitude) minLat = p.latitude if(maxLong == -4000.0 || maxLong < p.longitude ) maxLong = p.longitude if (minLong == -4000.0 || minLong > p.longitude) minLong = p.longitude } val del = 0.008 //map.controller.c Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) } private fun showPatternWithStopsOnMap(stops: List){ Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") if(viewingPattern==null || map == null) return val pattern = viewingPattern!!.pattern val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) var maxLat = -4000.0 var minLat = -4000.0 var minLong = -4000.0 var maxLong = -4000.0 for (p in pointsList){ // get max latitude if(maxLat == -4000.0) maxLat = p.latitude else if (maxLat < p.latitude) maxLat = p.latitude // find min latitude if (minLat == -4000.0) minLat = p.latitude else if (minLat > p.latitude) minLat = p.latitude if(maxLong == -4000.0 || maxLong < p.longitude ) maxLong = p.longitude if (minLong == -4000.0 || minLong > p.longitude) minLong = p.longitude } //val polyLine=Polyline(map) //polyLine.setPoints(pointsList) //save points if(map.overlayManager.contains(polyline)){ map.overlayManager.remove(polyline) } polyline = Polyline(map, false) polyline!!.setPoints(pointsList) //polyline.color = ContextCompat.getColor(context!!,R.color.brown_vd) polyline!!.infoWindow = null val paint = Paint() paint.color = ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) paint.isAntiAlias = true paint.strokeWidth = 16f paint.style = Paint.Style.FILL_AND_STROKE paint.strokeJoin = Paint.Join.ROUND paint.strokeCap = Paint.Cap.ROUND polyline!!.outlinePaintLists.add(MonochromaticPaintList(paint)) map.overlayManager.add(0,polyline!!) stopsOverlay.closeAllInfoWindows() stopsOverlay.items.clear() val stopIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ball) for(s in stops){ val gp = if (showOnTopOfLine) findOptimalPosition(s,pointsList) else GeoPoint(s.latitude!!,s.longitude!!) val marker = MarkerUtils.makeMarker( gp, s.ID, s.stopDefaultName, s.routesThatStopHereToString(), map,stopTouchResponder, stopIcon, R.layout.linedetail_stop_infowindow, R.color.line_drawn_poly ) marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) stopsOverlay.add(marker) } //POINTS LIST IS NOT IN ORDER ANY MORE //if(!map.overlayManager.contains(stopsOverlay)){ // map.overlayManager.add(stopsOverlay) //} polyline!!.setOnClickListener(Polyline.OnClickListener { polyline, mapView, eventPos -> Log.d(DEBUG_TAG, "clicked") true }) //map.controller.zoomToB//#animateTo(pointsList[0]) val del = 0.008 map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), true) //map.invalidate() } private fun initializeRecyclerView(){ val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager } private fun showStopsAsList(stops: List){ Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") val setNewAdapter = true if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } /** * Remove bus marker from overlay associated with tripID */ private fun removeBusMarker(tripID: String){ if(!busPositionMarkersByTrip.containsKey(tripID)){ Log.e(DEBUG_TAG, "Asked to remove veh with tripID $tripID but it's supposedly not shown") return } val marker = busPositionMarkersByTrip[tripID] busPositionsOverlay.remove(marker) busPositionMarkersByTrip.remove(tripID) val animator = tripMarkersAnimators[tripID] animator?.let{ it.cancel() tripMarkersAnimators.remove(tripID) } } private fun showPatternWithStop(patternId: String){ //var index = 0 Log.d(DEBUG_TAG, "Showing pattern with code $patternId ") for (i in currentPatterns.indices){ val pattStop = currentPatterns[i] if(pattStop.pattern.code == patternId){ Log.d(DEBUG_TAG, "Pattern found in position $i") //setPatternAndReqStops(pattStop) patternsSpinner.setSelection(i) break } } } /** * draw the position of the buses in the map. Copied from MapFragment */ private fun updateBusPositionsInMap(tripsPatterns: java.util.HashMap> ) { //Log.d(MapFragment.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 var marker: Marker? = null //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)) { //check if the trip direction ID is the same, if not remove if(tripWithPatternStops?.pattern != null && tripWithPatternStops.pattern.directionId != viewingPattern?.pattern?.directionId){ removeBusMarker(tripID) } else { //need to change the position of the marker marker = busPositionMarkersByTrip.get(tripID)!! BusPositionUtils.updateBusPositionMarker(map, marker, update, tripMarkersAnimators, false) // Set the pattern to add the info if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { val window = marker.infoWindow as BusInfoWindow if (window.pattern == null && 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 (mapView == null) Log.e(MapFragment.DEBUG_TAG, "Creating marker with null map, things will explode") marker = Marker(map) //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); val mdraw = ResourcesCompat.getDrawable(getResources(), R.drawable.map_bus_position_icon, null)!! //mdraw.setBounds(0,0,28,28); marker.icon = mdraw var markerPattern: MatoPattern? = null if (tripWithPatternStops != null) { if (tripWithPatternStops.pattern != null) markerPattern = tripWithPatternStops.pattern } marker.infoWindow = BusInfoWindow(map, update, markerPattern, true) { // set pattern to show if(it!=null) showPatternWithStop(it.code) } //marker.infoWindow as BusInfoWindow marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) BusPositionUtils.updateBusPositionMarker(map,marker, update, tripMarkersAnimators,true) // the overlay is null when it's not attached yet? // 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.put(tripID, marker) } } } if (noPatternsTrips.size > 0) { Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") } } override fun onResume() { super.onResume() Log.d(DEBUG_TAG, "Resetting paused from onResume") pausedFragment = false liveBusViewModel.requestPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ " zoom ${mapViewModel.currentZoom.value}") val controller = map.controller viewLifecycleOwner.lifecycleScope.launch { delay(100) Log.d(DEBUG_TAG, "zooming back to point") controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), mapViewModel.currentZoom.value!!,null,null) //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) //controller.setZoom(mapViewModel.currentZoom.value!!) } //controller.setZoom() } + //initialize GUI here + fragmentListener.readyGUIfor(FragmentKind.LINES) + } override fun onPause() { super.onPause() liveBusViewModel.stopPositionsListening() pausedFragment = true //save map val center = map.mapCenter mapViewModel.currentLat.value = center.latitude mapViewModel.currentLong.value = center.longitude mapViewModel.currentZoom.value = map.zoomLevel.toDouble() } override fun getBaseViewForSnackBar(): View? { return null } companion object { private const val LINEID_KEY="lineID" fun newInstance() = LinesDetailFragment() const val DEBUG_TAG="LinesDetailFragment" fun makeArgs(lineID: String): Bundle{ val b = Bundle() b.putString(LINEID_KEY, lineID) return b } @JvmStatic private fun findOptimalPosition(stop: Stop, pointsList: MutableList): GeoPoint{ if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty()) throw IllegalArgumentException() val sLat = stop.latitude!! val sLong = stop.longitude!! if(pointsList.size < 2) return pointsList[0] pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) } val p1 = pointsList[0] val p2 = pointsList[1] if (p1.longitude == p2.longitude){ //Log.e(DEBUG_TAG, "Same longitude") return GeoPoint(sLat, p1.longitude) } else if (p1.latitude == p2.latitude){ //Log.d(DEBUG_TAG, "Same latitude") return GeoPoint(p2.latitude,sLong) } val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude) val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude) val cR = p1.latitude - p1.longitude * m val longNew = (minv * sLong + sLat -cR ) / (m+minv) val latNew = (m*longNew + cR) //Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)") return GeoPoint(latNew,longNew) } private const val DEFAULT_CENTER_LAT = 45.12 private const val DEFAULT_CENTER_LON = 7.6858 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt index e9dbabf..34a4fb4 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesFragment.kt @@ -1,418 +1,418 @@ /* BusTO - Fragments components Copyright (C) 2022 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.content.Context import android.os.Bundle import android.os.Parcelable import android.util.Log import android.view.* import android.widget.* import android.widget.AdapterView.INVALID_POSITION import android.widget.AdapterView.OnItemSelectedListener import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops -import it.reyboz.bustorino.data.gtfs.PatternStop import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.PatternWithStopsSorter +import it.reyboz.bustorino.viewmodels.LinesViewModel class LinesFragment : ScreenBaseFragment() { companion object { fun newInstance(){ LinesFragment() } private const val DEBUG_TAG="BusTO-LinesFragment" const val FRAGMENT_TAG="LinesFragment" val patternStopsComparator = PatternWithStopsSorter() } private lateinit var viewModel: LinesViewModel private lateinit var linesSpinner: Spinner private lateinit var patternsSpinner: Spinner private lateinit var currentRoutes: List private lateinit var selectedPatterns: List private lateinit var routeDescriptionTextView: TextView private lateinit var stopsRecyclerView: RecyclerView private var linesAdapter: ArrayAdapter? = null private var patternsAdapter: ArrayAdapter? = null private var mListener: CommonFragmentListener? = null private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } private var firstClick = true private var recyclerViewState:Parcelable? = null private var patternsSpinnerState:Parcelable? = null private val adapterListener = object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { //var r = "" //stop?.let { r= it.stopDisplayName.toString() } if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { mListener?.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(mListener == null){ Log.e(DEBUG_TAG, "Listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { Log.d(DEBUG_TAG, "LongPressOnStop") return true } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) Log.d(DEBUG_TAG, "saveInstanceState bundle: $outState") } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.fragment_lines, container, false) linesSpinner = rootView.findViewById(R.id.linesSpinner) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) routeDescriptionTextView = rootView.findViewById(R.id.routeDescriptionTextView) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager //allow the context menu to be opened registerForContextMenu(stopsRecyclerView) Log.d(DEBUG_TAG, "Called onCreateView for LinesFragment") Log.d(DEBUG_TAG, "OnCreateView, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnCreateView, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") //set requests viewModel.routesGTTLiveData.observe(viewLifecycleOwner) { setRoutes(it) } viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> run { selectedPatterns = patterns.sortedBy { p-> p.pattern.code } //patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" } patternsAdapter?.let { it.clear() it.addAll(selectedPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } viewModel.selectedPatternLiveData.value?.let { setSelectedPattern(it) } val pos = patternsSpinner.selectedItemPosition //might be possible that the selectedItem is different (larger than list size) if(pos!= INVALID_POSITION && pos >= 0 && (pos < selectedPatterns.size)){ val p = selectedPatterns[pos] Log.d(DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") setPatternAndReqStops(selectedPatterns[pos]) } } } viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner){stops-> Log.d("BusTO-LinesFragment", "Setting stops from DB") setCurrentStops(stops) } if(context!=null) { patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter linesAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) linesSpinner.adapter = linesAdapter if (linesSpinner.onItemSelectedListener != null){ Log.d(DEBUG_TAG, "linesSpinner listener != null") } //listener linesSpinner.onItemSelectedListener = object: OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, pos: Int, p3: Long) { val selRoute = currentRoutes.get(pos) routeDescriptionTextView.text = selRoute.longName val oldRoute = viewModel.getRouteIDQueried() val resetSpinner = (oldRoute != null) && (oldRoute.trim() != selRoute.gtfsId.trim()) Log.d(DEBUG_TAG, "Selected route: ${selRoute.gtfsId}, reset spinner: $resetSpinner, oldRoute: $oldRoute") //launch query for this gtfsID viewModel.setRouteIDQuery(selRoute.gtfsId) //reset spinner position if(resetSpinner) patternsSpinner.setSelection(0) } override fun onNothingSelected(p0: AdapterView<*>?) { } } patternsSpinner.onItemSelectedListener = object : OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = selectedPatterns.get(position) // setPatternAndReqStops(patternWithStops) //viewModel.currentPositionInPatterns.value = position } override fun onNothingSelected(p0: AdapterView<*>?) { } } } return rootView } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener) mListener = context else throw RuntimeException(context.toString() + " must implement CommonFragmentListener") } override fun onResume() { super.onResume() mListener?.readyGUIfor(FragmentKind.LINES) Log.d(DEBUG_TAG, "Resuming lines fragment") //Log.d(DEBUG_TAG, "OnResume, selected line spinner pos: ${linesSpinner.selectedItemPosition}") //Log.d(DEBUG_TAG, "OnResume, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) Log.d(DEBUG_TAG, "Fragment onCreate") } override fun getBaseViewForSnackBar(): View? { return null } private fun setSelectedPattern(patternWs: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() for(k in selectedPatterns.indices){ if(selectedPatterns[k].pattern.code.trim() == code){ pos = k break } } Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") if(pos>=0){ patternsSpinner.setSelection(pos) } } private fun setRoutes(routes: List){ Log.d(DEBUG_TAG, "Resetting routes") currentRoutes = routes.sortedWith(linesComparator) if (linesAdapter!=null){ var selGtfsRoute = viewModel.getRouteIDQueried() var selRouteIdx = 0 if(selGtfsRoute == null){ selGtfsRoute ="" } Log.d(DEBUG_TAG, "Setting routes, selected route gtfsID: $selGtfsRoute") val adapter = linesAdapter!! if (adapter.isEmpty) { Log.d(DEBUG_TAG, "Lines adapter is empty") } else{ adapter.clear() } adapter.addAll(currentRoutes.map { r -> r.shortName }) adapter.notifyDataSetChanged() for(j in currentRoutes.indices){ val route = currentRoutes[j] if (route.gtfsId == selGtfsRoute) { selRouteIdx = j Log.d(DEBUG_TAG, "Route $selGtfsRoute has index $j") } } linesSpinner.setSelection(selRouteIdx) // } /* linesAdapter?.clear() linesAdapter?.addAll(currentRoutes.map { r -> r.shortName }) linesAdapter?.notifyDataSetChanged() */ } private fun setCurrentStops(stops: List){ Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") var setNewAdapter = true if(stopsRecyclerView.adapter is StopRecyclerAdapter){ val adapter = stopsRecyclerView.adapter as StopRecyclerAdapter if(adapter.stops.size == stopsSorted.size && (adapter.stops.get(0).gtfsID == stopsSorted.get(0).gtfsID) && (adapter.stops.get(numStops-1).gtfsID == stopsSorted.get(numStops-1).gtfsID) ){ Log.d(DEBUG_TAG, "Found same stops on recyclerview") setNewAdapter = false } /*else { Log.d(DEBUG_TAG, "Found adapter on recyclerview, but not the same stops") adapter.stops = stopsSorted adapter.notifyDataSetChanged() }*/ } if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, adapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") //currentPatternStops = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.selectedPatternLiveData.value = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { super.onCreateContextMenu(menu, v, menuInfo) Log.d("BusTO-LinesFragment", "Creating context menu ") if (v.id == R.id.patternStopsRecyclerView) { // if we aren't attached to activity, return null if (activity == null) return val inflater = requireActivity().menuInflater inflater.inflate(R.menu.menu_line_item, menu) } } override fun onContextItemSelected(item: MenuItem): Boolean { if (stopsRecyclerView.getAdapter() !is StopRecyclerAdapter) return false val adapter =stopsRecyclerView.adapter as StopRecyclerAdapter val stop = adapter.stops.get(adapter.getPosition()) val acId = item.itemId if(acId == R.id.action_view_on_map){ // view on the map if ((stop.latitude == null) or (stop.longitude == null) or (mListener == null) ) { Toast.makeText(context, R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show() return true } mListener!!.showMapCenteredOnStop(stop) return true } else if (acId == R.id.action_show_arrivals){ mListener?.requestArrivalsForStopID(stop.ID) return true } return false } override fun onStop() { super.onStop() Log.d(DEBUG_TAG, "Fragment stopped") recyclerViewState = stopsRecyclerView.layoutManager?.onSaveInstanceState() patternsSpinnerState = patternsSpinner.onSaveInstanceState() } override fun onStart() { super.onStart() Log.d(DEBUG_TAG, "OnStart, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnStart, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") if (recyclerViewState!=null){ stopsRecyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState) } if(patternsSpinnerState!=null){ patternsSpinner.onRestoreInstanceState(patternsSpinnerState) } } /* override fun onDestroyView() { super.onDestroyView() Log.d(DEBUG_TAG, "Fragment view destroyed") } override fun onDestroy() { super.onDestroy() Log.d(DEBUG_TAG, "Fragment destroyed") } */ override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) Log.d(DEBUG_TAG, "OnViewStateRes, bundled saveinstancestate: $savedInstanceState") Log.d(DEBUG_TAG, "OnViewStateRes, selected line spinner pos: ${linesSpinner.selectedItemPosition}") Log.d(DEBUG_TAG, "OnViewStateRes, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}") } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt index 0c69a4d..f1cc0cc 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -1,277 +1,278 @@ package it.reyboz.bustorino.fragments import android.content.Context import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.LinearInterpolator import android.view.animation.RotateAnimation import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.viewModels import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.RouteAdapter import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel class LinesGridShowingFragment : ScreenBaseFragment() { private val viewModel: LinesGridShowingViewModel by viewModels() //private lateinit var gridLayoutManager: AutoFitGridLayoutManager private lateinit var urbanRecyclerView: RecyclerView private lateinit var extraurbanRecyclerView: RecyclerView private lateinit var touristRecyclerView: RecyclerView private lateinit var urbanLinesTitle: TextView private lateinit var extrurbanLinesTitle: TextView private lateinit var touristLinesTitle: TextView private var routesByAgency = HashMap>() /*hashMapOf( AG_URBAN to ArrayList(), AG_EXTRAURB to ArrayList(), AG_TOUR to ArrayList() )*/ private lateinit var fragmentListener: CommonFragmentListener private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } private val routeClickListener = RouteAdapter.onItemClick { fragmentListener.showLineOnMap(it.gtfsId) } private val arrows = HashMap() private val durations = HashMap() private var openRecyclerView = "AG_URBAN" override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false) urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView) extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView) touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView) urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView) extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView) touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView) arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb) arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist) arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban) //show urban expanded by default val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView) for (recyView in recViews) { val gridLayoutManager = AutoFitGridLayoutManager( requireContext().applicationContext, (utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat())).toInt() ) recyView.layoutManager = gridLayoutManager } viewModel.routesLiveData.observe(viewLifecycleOwner){ //routesList = ArrayList(it) //routesList.sortWith(linesComparator) routesByAgency.clear() for(route in it){ val agency = route.agencyID if(!routesByAgency.containsKey(agency)){ routesByAgency[agency] = ArrayList() } routesByAgency[agency]?.add(route) } //val adapter = RouteOnlyLineAdapter(routesByAgency.map { route-> route.shortName }) //zip agencies and recyclerviews Companion.AGENCIES.zip(recViews) { ag, recView -> routesByAgency[ag]?.let { routeList -> routeList.sortWith(linesComparator) //val adapter = RouteOnlyLineAdapter(it.map { rt -> rt.shortName }) val adapter = RouteAdapter(routeList,routeClickListener) recView.adapter = adapter durations[ag] = if(routeList.size < 20) ViewUtils.DEF_DURATION else 1000 } } } //onClicks urbanLinesTitle.setOnClickListener { if(openRecyclerView!=""&& openRecyclerView!= AG_URBAN){ openCloseRecyclerView(openRecyclerView) openCloseRecyclerView(AG_URBAN) } } extrurbanLinesTitle.setOnClickListener { if(openRecyclerView!=""&& openRecyclerView!= AG_EXTRAURB){ openCloseRecyclerView(openRecyclerView) openCloseRecyclerView(AG_EXTRAURB) } } touristLinesTitle.setOnClickListener { if(openRecyclerView!="" && openRecyclerView!= AG_TOUR) { openCloseRecyclerView(openRecyclerView) openCloseRecyclerView(AG_TOUR) } } return rootView } private fun openCloseRecyclerView(agency: String){ val recyclerView = when(agency){ AG_TOUR -> touristRecyclerView AG_EXTRAURB -> extraurbanRecyclerView AG_URBAN -> urbanRecyclerView else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val expandedLiveData = when(agency){ AG_TOUR -> viewModel.isTouristExpanded AG_URBAN -> viewModel.isUrbanExpanded AG_EXTRAURB -> viewModel.isExtraUrbanExpanded else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val duration = durations[agency] val arrow = arrows[agency] val durArrow = if(duration == null || duration==ViewUtils.DEF_DURATION) 500 else duration if(duration!=null&&arrow!=null) when (recyclerView.visibility){ View.GONE -> { Log.d(DEBUG_TAG, "Open recyclerview $agency") //val a =ViewUtils.expand(recyclerView, duration, 0) recyclerView.visibility = View.VISIBLE expandedLiveData.value = true Log.d(DEBUG_TAG, "Arrow for $agency has rotation: ${arrow.rotation}") setOpen(arrow, true) //arrow.startAnimation(rotateArrow(true,durArrow)) openRecyclerView = agency } View.VISIBLE -> { Log.d(DEBUG_TAG, "Close recyclerview $agency") //ViewUtils.collapse(recyclerView, duration) recyclerView.visibility = View.GONE expandedLiveData.value = false //arrow.rotation = 90f Log.d(DEBUG_TAG, "Arrow for $agency has rotation ${arrow.rotation} pre-rotate") setOpen(arrow, false) //arrow.startAnimation(rotateArrow(false,durArrow)) openRecyclerView = "" } View.INVISIBLE -> { TODO() } } } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") - fragmentListener.readyGUIfor(FragmentKind.LINES) } override fun getBaseViewForSnackBar(): View? { return null } override fun onResume() { super.onResume() viewModel.isUrbanExpanded.value?.let { if(it) { urbanRecyclerView.visibility = View.VISIBLE arrows[AG_URBAN]?.rotation= 90f openRecyclerView = AG_URBAN Log.d(DEBUG_TAG, "RecyclerView gtt:U is expanded") } else { urbanRecyclerView.visibility = View.GONE arrows[AG_URBAN]?.rotation= 0f } } viewModel.isTouristExpanded.value?.let { val recview = touristRecyclerView if(it) { recview.visibility = View.VISIBLE arrows[AG_TOUR]?.rotation=90f openRecyclerView = AG_TOUR } else { recview.visibility = View.GONE arrows[AG_TOUR]?.rotation= 0f } } viewModel.isExtraUrbanExpanded.value?.let { val recview = extraurbanRecyclerView if(it) { openRecyclerView = AG_EXTRAURB recview.visibility = View.VISIBLE arrows[AG_EXTRAURB]?.rotation=90f } else { recview.visibility = View.GONE arrows[AG_EXTRAURB]?.rotation=0f } } + fragmentListener.readyGUIfor(FragmentKind.LINES) + } companion object { private const val COLUMN_WIDTH_DP=200 private const val AG_URBAN = "gtt:U" private const val AG_EXTRAURB ="gtt:E" private const val AG_TOUR ="gtt:T" private const val DEBUG_TAG ="BusTO-LinesGridFragment" const val FRAGMENT_TAG = "LinesGridShowingFragment" private val AGENCIES = listOf(AG_URBAN, AG_EXTRAURB, AG_TOUR) fun newInstance() = LinesGridShowingFragment() @JvmStatic fun setOpen(imageView: ImageView, value: Boolean){ if(value) imageView.rotation = 90f else imageView.rotation = 0f } @JvmStatic fun rotateArrow(toOpen: Boolean, duration: Long): RotateAnimation{ val start = if (toOpen) 0f else 90f val stop = if(toOpen) 90f else 0f Log.d(DEBUG_TAG, "Rotate arrow from $start to $stop") val rotate = RotateAnimation(start, stop, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) rotate.duration = duration rotate.interpolator = LinearInterpolator() //rotate.fillAfter = true rotate.fillBefore = false return rotate } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index fc1e0d1..78b56bf 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,891 +1,891 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.net.Uri; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.List; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends ScreenBaseFragment implements FragmentListenerMain{ private static final String OPTION_SHOW_LEGEND = "show_legend"; private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public static final String PENDING_STOP_SEARCH="PendingStopSearch"; public final static String FRAGMENT_TAG = "MainScreenFragment"; /// UI ELEMENTS // private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; private boolean instanceStateSaved = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; List fetcherList = utils.getDefaultArrivalsFetchers(getContext()); ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()]; arrivalsFetchers = fetcherList.toArray(arrivalsFetchers); if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); // AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; // private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(), result -> { if(result!=null && result.getContents()!=null) { //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); Uri uri; try { uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); requestArrivalsForStopID(busStopID); } else { //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); } }); /// LOCATION STUFF /// boolean pendingNearbyStopsRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { if(result==null || result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; boolean resCoarse = result.get(Manifest.permission.ACCESS_COARSE_LOCATION); boolean resFine = result.get(Manifest.permission.ACCESS_FINE_LOCATION); Log.d(DEBUG_TAG, "Permissions for location are: "+result); if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if(pendingNearbyStopsRequest){ showNearbyFragmentIfNeeded(cr); pendingNearbyStopsRequest = false; } } if(pendingNearbyStopsRequest) pendingNearbyStopsRequest=false; } }); private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { } @Override public void onLocationStatusChanged(int status) { if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown() && checkLocationPermission()){ //request Stops //pendingNearbyStopsRequest = false; if (getContext()!= null && !isNearbyFragmentShown()) //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfNeeded(cr); } } @Override public long getLastUpdateTimeMillis() { return 50; } @Override public LocationCriteria getLocationCriteria() { return cr; } @Override public void onLocationProviderAvailable() { //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); if(!isNearbyFragmentShown() && getContext()!=null){ // we should have the location permission if(!checkLocationPermission()) Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + "we have no location permission"); pendingNearbyStopsRequest = true; //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfNeeded(cr); } } @Override public void onLocationDisabled() { } }; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { return new MainScreenFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); if (getArguments().getString(PENDING_STOP_SEARCH)!=null) pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); addToFavorites = root.findViewById(R.id.addToFavorites); busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); resultFrameLayout = root.findViewById(R.id.resultFrame); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); hideHintButton.setOnClickListener(this::onHideHint); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff fragMan = getChildFragmentManager(); fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locationManager = AppLocationManager.getInstance(getContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } instanceStateSaved = false; } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); instanceStateSaved = true; } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); if (setupOnStart) { if (pendingStopID==null){ //We want the nearby bus stops! //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsRequest = true; } else { showNearbyFragmentIfNeeded(cr); } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); if (con != null) { if(locationManager==null) locationManager = AppLocationManager.getInstance(con); if(Permissions.locationPermissionGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } //don't request permission } else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { BarcodeScanOptions scanOptions = new BarcodeScanOptions(); Intent intent = scanOptions.createScanIntent(); if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ BarcodeScanUtils.showDownloadDialog(null, this); }else { barcodeLauncher.launch(scanOptions); } } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isVisible()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void actuallyShowNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = fragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing - final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); + final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); FragmentTransaction ft = fragMan.beginTransaction(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing() &&!instanceStateSaved) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because we saved instanceState"); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsRequest) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void showLineOnMap(String routeGtfsId) { //pass to activity mListener.showLineOnMap(routeGtfsId); } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() new AsyncArrivalsSearcher(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); } } else { Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); prepareGUIForBusLines(); new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; return isOldVersion || !noPermission; } private void requestLocationPermission(){ requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfNeeded(Criteria cr){ if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } if(getContext()==null){ Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } AppLocationManager appLocationManager = AppLocationManager.getInstance(getContext()); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); actuallyShowNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /* * Run location requests separately and asynchronously class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } */ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java index 0c9669a..5b02a66 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,836 +1,795 @@ /* 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.graphics.drawable.Drawable; import android.location.Location; import android.location.LocationManager; import android.os.AsyncTask; 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.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; 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.MatoTripsDownloadWorker; 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.StopsMapViewModel; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; 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.BoundingBox; 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; import java.lang.ref.WeakReference; import java.util.*; import kotlin.Pair; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.util.Permissions; public class MapFragment extends ScreenBaseFragment { //private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String BUNDLE_ROUTES_STOPPING = "routesStopping"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; public static final double NO_POSITION_ZOOM = 17.1; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; - //the asynctask used to get the stops from the database - private AsyncStopFetcher stopFetcher = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; private boolean hasMapStartFinished = false; private boolean followingLocation = false; + //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 final HashMap busPositionMarkersByTrip = new HashMap<>(); private FolderOverlay busPositionsOverlay = null; private final HashMap tripMarkersAnimators = new HashMap<>(); protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { @Override public void onActionUp(@NonNull String stopID, @Nullable String stopName) { if (listenerMain!= null){ Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: "+stopID); listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; private final ActivityResultLauncher positionRequestLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { if (result == null){ Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?"); } else if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) && Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ map.getOverlays().remove(mLocationOverlay); startLocationOverlay(true, map); if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) return; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { map.getController().setZoom(POSITION_FOUND_ZOOM); GeoPoint startPoint = new GeoPoint(userLocation); setLocationFollowing(true); map.getController().setCenter(startPoint); } } else Log.w(DEBUG_TAG,"No location permission"); }); public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(@NonNull Stop stop){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stop.getLatitude()); args.putDouble(BUNDLE_LONGIT, stop.getLongitude()); args.putString(BUNDLE_NAME, stop.getStopDisplayName()); args.putString(BUNDLE_ID, stop.ID); args.putString(BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()); fragment.setArguments(args); return fragment; } //public static MapFragment getInstance(@NonNull Stop stop){ // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); //} @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.activity_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); 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.icon_center_map); btFollowMe = root.findViewById(R.id.icon_follow); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); //setup Bus Markers Overlay busPositionsOverlay = new FolderOverlay(); //reset shown bus updates busPositionMarkersByTrip.clear(); tripMarkersAnimators.clear(); //set map not done hasMapStartFinished = false; //Start map from bundle if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); if(Permissions.locationPermissionGranted(getContext())) { final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); if(Permissions.locationPermissionGranted(getContext())) setLocationFollowing(!followingLocation); else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); //gtfsPosViewModel = new ViewModelProvider(this).get(GTFSPositionsViewModel.class); //viewModel - positionsViewModel = new ViewModelProvider(this).get(MQTTPositionsViewModel.class); + ViewModelProvider provider = new ViewModelProvider(this); + positionsViewModel = provider.get(MQTTPositionsViewModel.class); + stopsViewModel = provider.get(StopsMapViewModel.class); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; //stop animations // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { super.onPause(); Log.w(DEBUG_TAG, "On pause called mapfrag"); saveMapState(); for (ObjectAnimator animator : tripMarkersAnimators.values()) { if(animator!=null && animator.isRunning()){ animator.cancel(); } } tripMarkersAnimators.clear(); positionsViewModel.stopPositionsListening(); - if (stopFetcher!= null) - stopFetcher.cancel(true); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new 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 void 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; } final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); } @Override public void onResume() { super.onResume(); if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); if(positionsViewModel !=null) { //gtfsPosViewModel.requestUpdates(); positionsViewModel.requestPosUpdates(MQTTMatoClient.LINES_ALL); //mapViewModel.testCascade(); positionsViewModel.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"); }); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ public void setLocationFollowing(Boolean value){ followingLocation = value; if(mLocationOverlay==null || getContext() == 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 void updateGUIForLocationFollowing(boolean following){ 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 void startLocationOverlay(boolean enableLocation, MapView map){ if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay"); GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); if (enableLocation) overlay.enableMyLocation(); overlay.setOptionsMenuEnabled(true); //map.getOverlays().add(this.mLocationOverlay); this.mLocationOverlay = overlay; map.getOverlays().add(mLocationOverlay); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==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.getOverlays().clear(); //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; String routesStopping = ""; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new 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.locationPermissionGranted(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 = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; startLocationOverlay(Permissions.locationPermissionGranted(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 = new GeoPoint(marker); startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20)); startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20)); //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(new 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"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); //check for permission if (locationManager != null && Permissions.locationPermissionGranted(activity)) { @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { double distan = utils.measuredistanceBetween(userLocation.getLatitude(), userLocation.getLongitude(), DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); if (distan < 100_000.0) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; setLocationFollowing(true); } } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(NO_POSITION_ZOOM); setLocationFollowing(false); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } //add stops overlay //map.getOverlays().add(mLocationOverlay); map.getOverlays().add(this.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 Marker stopMarker = makeMarker(marker, ID , name, routesStopping,true); map.getController().animateTo(marker); } //add the overlays with the bus stops if(busPositionsOverlay == null){ //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"); } + if(stopsViewModel !=null){ + + stopsViewModel.getStopsInBoundingBox().observe(getViewLifecycleOwner(), + this::showStopsMarkers + ); + } else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null"); map.getOverlays().add(this.busPositionsOverlay); //set map as started hasMapStartFinished = true; } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); - double latFrom = bb.getLatSouth(); + Log.d(DEBUG_TAG, "Requesting stops in bounding box, stopViewModel is null "+(stopsViewModel==null)); + if(stopsViewModel!=null){ + stopsViewModel.requestStopsInBoundingBox(bb); + } + /*double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED) stopFetcher.cancel(true); stopFetcher = new AsyncStopFetcher(this); stopFetcher.execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); + + */ } private void updateBusMarker(final Marker marker, final LivePositionUpdate posUpdate, @Nullable boolean justCreated){ GeoPoint position; final String updateID = posUpdate.getTripID(); if(!justCreated){ position = marker.getPosition(); if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){ GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); ObjectAnimator 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 = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); marker.setPosition(position); } if(posUpdate.getBearing()!=null) marker.setRotation(posUpdate.getBearing()*(-1.f)); } private void updateBusPositionsInMap(HashMap> tripsPatterns){ Log.d(DEBUG_TAG, "Updating positions of the buses"); //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); final ArrayList noPatternsTrips = new ArrayList<>(); for(String tripID: tripsPatterns.keySet()) { final Pair pair = tripsPatterns.get(tripID); if (pair == null) continue; final LivePositionUpdate update = pair.getFirst(); final TripAndPatternWithStops tripWithPatternStops = pair.getSecond(); //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)){ //need to change the position of the marker final Marker marker = busPositionMarkersByTrip.get(tripID); assert marker!=null; updateBusMarker(marker, update, false); if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){ BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow(); if(tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.getPattern()); } } } else{ //marker is not there, need to make it if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode"); final Marker marker = new 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()); final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.map_bus_position_icon, null); /*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(), R.drawable.point_heading_icon, R.color.white, route,12); */ assert mdraw != null; //mdraw.setBounds(0,0,28,28); marker.setIcon(mdraw); if(tripWithPatternStops == null){ noPatternsTrips.add(tripID); } MatoPattern markerPattern = null; if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null) markerPattern = tripWithPatternStops.getPattern(); marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , false, (pattern) -> { })); 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.put(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 void showStopsMarkers(List stops){ if (getContext() == null || stops == null){ //we are not attached return; } boolean good = true; for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==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.getRepository() == null){ Log.e(DEBUG_TAG, "Map view repository is null"); } GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().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(); } public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){ return makeMarker(geoPoint,stop.ID, stop.getStopDefaultName(), stop.routesThatStopHereToString(), isStartMarker); } public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, String routesStopping, boolean isStartMarker) { // add a marker final Marker marker = new Marker(map); // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, responder, R.layout.linedetail_stop_infowindow, R.color.red_darker); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, 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.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme())); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(stopID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); //map.getController().animateTo(marker.getPosition()); } return marker; } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return null; } - /** - * Simple asyncTask class to load the stops in the background - * Holds a weak reference to the fragment to do callbacks - */ - static class AsyncStopFetcher extends AsyncTask>{ - - final WeakReference fragmentWeakReference; - - public AsyncStopFetcher(MapFragment fragment) { - this.fragmentWeakReference = new WeakReference<>(fragment); - } - - @Override - protected List doInBackground(BoundingBoxLimit... limits) { - if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){ - Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); - - return null; - - } - final BoundingBoxLimit limit = limits[0]; - //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); - - NextGenDB dbHelper = NextGenDB.getInstance(fragmentWeakReference.get().getContext()); - ArrayList stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo, - limit.longFrom, limit.latitTo); - dbHelper.close(); - return stops; - } - - @Override - protected void onPostExecute(List stops) { - super.onPostExecute(stops); - //Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working"); - if(fragmentWeakReference.get()==null) { - Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); - return; - } - if (stops!=null) - Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size()); - fragmentWeakReference.get().showStopsMarkers(stops); - } - - private static class BoundingBoxLimit{ - final double longFrom, longTo, latitFrom, latitTo; - - public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) { - this.longFrom = longFrom; - this.longTo = longTo; - this.latitFrom = latitFrom; - this.latitTo = latitTo; - } - } - - } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index a42e371..19a44bb 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,636 +1,649 @@ /* BusTO - Fragments components Copyright (C) 2018 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.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.appcompat.widget.AppCompatButton; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.work.WorkInfo; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; import com.android.volley.*; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.FiveTAPIFetcher.QueryType; import it.reyboz.bustorino.backend.mato.MapiArrivalRequest; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import it.reyboz.bustorino.adapters.SquareStopAdapter; import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class NearbyStopsFragment extends Fragment implements LoaderManager.LoaderCallbacks { + public enum FragType{ + STOPS(1), ARRIVALS(2); + private final int num; + FragType(int num){ + this.num = num; + } + public static FragType fromNum(int i){ + switch (i){ + case 1: return STOPS; + case 2: return ARRIVALS; + default: + throw new IllegalArgumentException("type not recognized"); + } + } + } + private FragmentListenerMain mListener; private FragmentLocationListener fragmentLocationListener; private final static String DEBUG_TAG = "NearbyStopsFragment"; private final static String FRAGMENT_TYPE_KEY = "FragmentType"; - public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; - private int fragment_type; + //public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; + private FragType fragment_type = FragType.STOPS; public final static String FRAGMENT_TAG="NearbyStopsFrag"; //data Bundle private final String BUNDLE_LOCATION = "location"; private final int LOADER_ID = 0; private RecyclerView gridRecyclerView; private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; private Location lastReceivedLocation = null; private ProgressBar circlingProgressBar,flatProgressBar; private int distance; protected SharedPreferences globalSharedPref; private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private TextView messageTextView,titleTextView; private CommonScrollListener scrollListener; private AppCompatButton switchButton; private boolean firstLocForStops = true,firstLocForArrivals = true; public static final int COLUMN_WIDTH_DP = 250; private Integer MAX_DISTANCE = -3; private int MIN_NUM_STOPS = -1; private int TIME_INTERVAL_REQUESTS = -1; private AppLocationManager locManager; //These are useful for the case of nearby arrivals private ArrivalsManager arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; private boolean dbUpdateRunning = false; private ArrayList currentNearbyStops = new ArrayList<>(); public NearbyStopsFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ - public static NearbyStopsFragment newInstance(int fragmentType) { - if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) - throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); + public static NearbyStopsFragment newInstance(FragType type) { + //if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) + // throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); NearbyStopsFragment fragment = new NearbyStopsFragment(); final Bundle args = new Bundle(1); - args.putInt(FRAGMENT_TYPE_KEY,fragmentType); + args.putInt(FRAGMENT_TYPE_KEY,type.num); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { - setFragmentType(getArguments().getInt(FRAGMENT_TYPE_KEY)); + setFragmentType(FragType.fromNum(getArguments().getInt(FRAGMENT_TYPE_KEY))); } locManager = AppLocationManager.getInstance(getContext()); fragmentLocationListener = new FragmentLocationListener(this); if (getContext()!=null) { globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment if (getContext() == null) throw new RuntimeException(); View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView); gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), Float.valueOf(utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)).intValue()); gridRecyclerView.setLayoutManager(gridLayoutManager); gridRecyclerView.setHasFixedSize(false); circlingProgressBar = root.findViewById(R.id.loadingBar); flatProgressBar = root.findViewById(R.id.horizontalProgressBar); messageTextView = root.findViewById(R.id.messageTextView); titleTextView = root.findViewById(R.id.titleTextView); switchButton = root.findViewById(R.id.switchButton); scrollListener = new CommonScrollListener(mListener,false); switchButton.setOnClickListener(v -> switchFragmentType()); Log.d(DEBUG_TAG, "onCreateView"); DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, new Observer>() { @Override public void onChanged(List workInfos) { if(workInfos.isEmpty()) return; WorkInfo wi = workInfos.get(0); if (wi.getState() == WorkInfo.State.RUNNING && locManager.isRequesterRegistered(fragmentLocationListener)) { locManager.removeLocationRequestFor(fragmentLocationListener); dbUpdateRunning = true; } else if(!locManager.isRequesterRegistered(fragmentLocationListener)){ locManager.addLocationRequestFor(fragmentLocationListener); dbUpdateRunning = false; } } }); return root; } /** * Use this method to set the fragment type * @param type the type, TYPE_ARRIVALS or TYPE_STOPS */ - private void setFragmentType(int type){ - if(type!=TYPE_ARRIVALS && type !=TYPE_STOPS) - throw new IllegalArgumentException("type not recognized"); + private void setFragmentType(FragType type){ this.fragment_type = type; switch(type){ - case TYPE_ARRIVALS: - + case ARRIVALS: TIME_INTERVAL_REQUESTS = 5*1000; break; - case TYPE_STOPS: + case STOPS: TIME_INTERVAL_REQUESTS = 1000; } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); /// TODO: RISOLVERE PROBLEMA: il context qui e' l'Activity non il Fragment if (context instanceof FragmentListenerMain) { mListener = (FragmentListenerMain) context; } else { throw new RuntimeException(context + " must implement OnFragmentInteractionListener"); } Log.d(DEBUG_TAG, "OnAttach called"); } @Override public void onPause() { super.onPause(); gridRecyclerView.setAdapter(null); locManager.removeLocationRequestFor(fragmentLocationListener); Log.d(DEBUG_TAG,"On paused called"); } @Override public void onResume() { super.onResume(); try{ if(!dbUpdateRunning && !locManager.isRequesterRegistered(fragmentLocationListener)) locManager.addLocationRequestFor(fragmentLocationListener); } catch (SecurityException ex){ //ignored //try another location provider } switch(fragment_type){ - case TYPE_STOPS: + case STOPS: if(dataAdapter!=null){ gridRecyclerView.setAdapter(dataAdapter); circlingProgressBar.setVisibility(View.GONE); } break; - case TYPE_ARRIVALS: + case ARRIVALS: if(arrivalsStopAdapter!=null){ gridRecyclerView.setAdapter(arrivalsStopAdapter); circlingProgressBar.setVisibility(View.GONE); } } mListener.enableRefreshLayout(false); Log.d(DEBUG_TAG,"OnResume called"); if(getContext()==null){ Log.e(DEBUG_TAG, "NULL CONTEXT, everything is going to crash now"); MIN_NUM_STOPS = 5; MAX_DISTANCE = 600; return; } //Re-read preferences SharedPreferences shpr = PreferenceManager.getDefaultSharedPreferences(getContext().getApplicationContext()); //For some reason, they are all saved as strings MAX_DISTANCE = shpr.getInt(getString(R.string.pref_key_radius_recents),600); boolean isMinStopInt = true; try{ MIN_NUM_STOPS = shpr.getInt(getString(R.string.pref_key_num_recents), 5); } catch (ClassCastException ex){ isMinStopInt = false; } if(!isMinStopInt) try { MIN_NUM_STOPS = Integer.parseInt(shpr.getString(getString(R.string.pref_key_num_recents), "5")); } catch (NumberFormatException ex){ MIN_NUM_STOPS = 5; } if(BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Max distance for stops: "+MAX_DISTANCE+ ", Min number of stops: "+MIN_NUM_STOPS); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); gridRecyclerView.setVisibility(View.INVISIBLE); gridRecyclerView.addOnScrollListener(scrollListener); } @Override public void onDetach() { super.onDetach(); mListener = null; if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); } @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { //BUILD URI if (args!=null) lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); Uri.Builder builder = new Uri.Builder(); builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stops").appendPath("location") .appendPath(String.valueOf(lastReceivedLocation.getLatitude())) .appendPath(String.valueOf(lastReceivedLocation.getLongitude())) .appendPath(String.valueOf(distance)); //distance CursorLoader cl = new CursorLoader(getContext(),builder.build(),NextGenDB.QUERY_COLUMN_stops_all,null,null,null); cl.setUpdateThrottle(2000); return cl; } @Override public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { if (0 > MAX_DISTANCE) throw new AssertionError(); //Cursor might be null if (cursor == null) { Log.e(DEBUG_TAG, "Null cursor, something really wrong happened"); return; } Log.d(DEBUG_TAG, "Num stops found: " + cursor.getCount() + ", Current distance: " + distance); if (!dbUpdateRunning && (cursor.getCount() < MIN_NUM_STOPS && distance <= MAX_DISTANCE)) { distance = distance * 2; Bundle d = new Bundle(); d.putParcelable(BUNDLE_LOCATION, lastReceivedLocation); getLoaderManager().restartLoader(LOADER_ID, d, this); //Log.d(DEBUG_TAG, "Doubling distance now!"); return; } Log.d("LoadFromCursor", "Number of nearby stops: " + cursor.getCount()); //////// if(cursor.getCount()>0) currentNearbyStops = NextGenDB.getStopsFromCursorAllFields(cursor); showCurrentStops(); } /** * Display the stops, or run new set of requests for arrivals */ private void showCurrentStops(){ if (currentNearbyStops.isEmpty()) { setNoStopsLayout(); return; } double minDistance = Double.POSITIVE_INFINITY; for(Stop s: currentNearbyStops){ minDistance = Math.min(minDistance, s.getDistanceFromLocation(lastReceivedLocation)); } //quick trial to hopefully always get the stops in the correct order Collections.sort(currentNearbyStops,new StopSorterByDistance(lastReceivedLocation)); switch (fragment_type){ - case TYPE_STOPS: + case STOPS: showStopsInRecycler(currentNearbyStops); break; - case TYPE_ARRIVALS: + case ARRIVALS: arrivalsManager = new ArrivalsManager(currentNearbyStops); flatProgressBar.setVisibility(View.VISIBLE); flatProgressBar.setProgress(0); flatProgressBar.setIndeterminate(false); //for the moment, be satisfied with only one location //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); break; default: } } @Override public void onLoaderReset(@NonNull Loader loader) { } /** * To enable targeting from the Button */ public void switchFragmentType(View v){ switchFragmentType(); } /** * Call when you need to switch the type of fragment */ private void switchFragmentType(){ - if(fragment_type==TYPE_ARRIVALS){ - setFragmentType(TYPE_STOPS); + if(fragment_type==FragType.ARRIVALS){ + setFragmentType(FragType.STOPS); switchButton.setText(getString(R.string.show_arrivals)); titleTextView.setText(getString(R.string.nearby_stops_message)); if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); if(dataAdapter!=null) gridRecyclerView.setAdapter(dataAdapter); - } else if (fragment_type==TYPE_STOPS){ - setFragmentType(TYPE_ARRIVALS); + } else if (fragment_type==FragType.STOPS){ + setFragmentType(FragType.ARRIVALS); titleTextView.setText(getString(R.string.nearby_arrivals_message)); switchButton.setText(getString(R.string.show_stops)); if(arrivalsStopAdapter!=null) gridRecyclerView.setAdapter(arrivalsStopAdapter); } fragmentLocationListener.lastUpdateTime = -1; //locManager.removeLocationRequestFor(fragmentLocationListener); //locManager.addLocationRequestFor(fragmentLocationListener); showCurrentStops(); } //useful methods /////// GUI METHODS //////// private void showStopsInRecycler(List stops){ if(firstLocForStops) { dataAdapter = new SquareStopAdapter(stops, mListener, lastReceivedLocation); gridRecyclerView.setAdapter(dataAdapter); firstLocForStops = false; }else { dataAdapter.setStops(stops); dataAdapter.setUserPosition(lastReceivedLocation); } dataAdapter.notifyDataSetChanged(); //showRecyclerHidingLoadMessage(); if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_STOPS); } private void showArrivalsInRecycler(List palinas){ Collections.sort(palinas,new StopSorterByDistance(lastReceivedLocation)); final ArrayList> routesPairList = new ArrayList<>(10); //int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: palinas){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything if (r.passaggi != null && !r.passaggi.isEmpty()) routesPairList.add(new Pair<>(p,r)); } } if (getContext()==null){ Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached"); return; } if(firstLocForArrivals){ arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastReceivedLocation); gridRecyclerView.setAdapter(arrivalsStopAdapter); firstLocForArrivals = false; } else { arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastReceivedLocation); } //arrivalsStopAdapter.notifyDataSetChanged(); showRecyclerHidingLoadMessage(); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_ARRIVALS); } private void setNoStopsLayout(){ messageTextView.setVisibility(View.VISIBLE); messageTextView.setText(R.string.no_stops_nearby); circlingProgressBar.setVisibility(View.GONE); } /** * Does exactly what is says on the tin */ private void showRecyclerHidingLoadMessage(){ if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); } class ArrivalsManager implements Response.Listener, Response.ErrorListener{ final HashMap palinasDone = new HashMap<>(); //final Map> routesToAdd = new HashMap<>(); final static String REQUEST_TAG = "NearbyArrivals"; final NetworkVolleyManager volleyManager; int activeRequestCount = 0,reqErrorCount = 0, reqSuccessCount=0; ArrivalsManager(List stops){ volleyManager = NetworkVolleyManager.getInstance(getContext()); int MAX_ARRIVAL_STOPS = 35; Date currentDate = new Date(); int timeRange = 3600; int departures = 10; int numreq = 0; for(Stop s: stops.subList(0,Math.min(stops.size(), MAX_ARRIVAL_STOPS))){ final MapiArrivalRequest req = new MapiArrivalRequest(s.ID, currentDate, timeRange, departures, this, this); req.setTag(REQUEST_TAG); volleyManager.addToRequestQueue(req); activeRequestCount++; numreq++; } flatProgressBar.setMax(numreq); } @Override public void onErrorResponse(VolleyError error) { if(error instanceof ParseError){ //TODO Log.w(DEBUG_TAG,"Parsing error for stop request"); } else if (error instanceof NetworkError){ String s; if(error.networkResponse!=null) s = new String(error.networkResponse.data); else s=""; Log.w(DEBUG_TAG,"Network error: "+s); }else { Log.w(DEBUG_TAG,"Volley Error: "+error.getMessage()); } if(error.networkResponse!=null){ Log.w(DEBUG_TAG, "Error status code: "+error.networkResponse.statusCode); } //counters activeRequestCount--; reqErrorCount++; flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); } @Override public void onResponse(Palina result) { //counter for requests activeRequestCount--; reqSuccessCount++; //final Palina palinaInMap = palinasDone.get(result.ID); //palina cannot be null here //sorry for the brutal crash when it happens //if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); //add the palina to the successful one //TODO: Avoid redoing everything every time a new Result arrives palinasDone.put(result.ID, result); final ArrayList outList = new ArrayList<>(); for(Palina p: palinasDone.values()){ final List routes = p.queryAllRoutes(); if(routes!=null && routes.size()>0) outList.add(p); } showArrivalsInRecycler(outList); flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); if(activeRequestCount==0) { flatProgressBar.setIndeterminate(true); flatProgressBar.setVisibility(View.GONE); } } void cancelAllRequests(){ volleyManager.getRequestQueue().cancelAll(REQUEST_TAG); flatProgressBar.setVisibility(View.GONE); } } /** * Local locationListener, to use for the GPS */ class FragmentLocationListener implements AppLocationManager.LocationRequester{ LoaderManager.LoaderCallbacks callbacks; private int oldLocStatus = -2; private LocationCriteria cr; private long lastUpdateTime = -1; public FragmentLocationListener(LoaderManager.LoaderCallbacks callbacks) { this.callbacks = callbacks; } @Override public void onLocationChanged(Location location) { //set adapter float accuracy = location.getAccuracy(); if(accuracy<100 && !dbUpdateRunning) { distance = 20; final Bundle msgBundle = new Bundle(); msgBundle.putParcelable(BUNDLE_LOCATION,location); getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks); } lastUpdateTime = System.currentTimeMillis(); Log.d("BusTO:NearPositListen","can start loader "+ !dbUpdateRunning); } @Override public void onLocationStatusChanged(int status) { switch(status){ case AppLocationManager.LOCATION_GPS_AVAILABLE: messageTextView.setVisibility(View.GONE); break; case AppLocationManager.LOCATION_UNAVAILABLE: messageTextView.setText(R.string.enableGpsText); messageTextView.setVisibility(View.VISIBLE); break; default: Log.e(DEBUG_TAG,"Location status not recognized"); } } @Override public LocationCriteria getLocationCriteria() { return new LocationCriteria(120,TIME_INTERVAL_REQUESTS); } @Override public long getLastUpdateTimeMillis() { return lastUpdateTime; } void resetUpdateTime(){ lastUpdateTime = -1; } @Override public void onLocationProviderAvailable() { } @Override public void onLocationDisabled() { } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java index 3a19b49..4ec5795 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/StopListFragment.java @@ -1,145 +1,150 @@ /* BusTO - Fragments components Copyright (C) 2018 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.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.util.Log; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.StopsTable; import it.reyboz.bustorino.adapters.StopAdapter; +import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.List; public class StopListFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks { private List stopList; private StopAdapter mListAdapter; private static final String[] dataProjection={StopsTable.COL_LINES_STOPPING,StopsTable.COL_PLACE,StopsTable.COL_TYPE,StopsTable.COL_LOCATION}; private static final String KEY_STOP_ID = "stopID"; private static final String WORDS_SEARCHED= "query"; private static final int EXTRA_ID=160; private String searchedWords; public StopListFragment(){ //required empty constructor } public static StopListFragment newInstance(String searchQuery) { Bundle args = new Bundle(); //TODO: search stops inside the DB args.putString(WORDS_SEARCHED,searchQuery); StopListFragment fragment = new StopListFragment(); args.putSerializable(LIST_TYPE,FragmentKind.STOPS); fragment.setArguments(args); return fragment; } public void setStopList(List stopList){ this.stopList = stopList; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); + mListener.readyGUIfor(FragmentKind.STOPS); if(stopList!=null) { mListAdapter = new StopAdapter(getContext(),stopList); resetListAdapter(mListAdapter); for (int i = 0; i < stopList.size(); i++) { final Bundle b = new Bundle(); b.putString(KEY_STOP_ID, stopList.get(i).ID); loaderManager.restartLoader(i, b, this); } } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); searchedWords = getArguments().getString(WORDS_SEARCHED); } @Override public Loader onCreateLoader(int id, Bundle args) { //The id will be the position of the element in the list Uri.Builder builder = new Uri.Builder(); String stopID = args.getString(KEY_STOP_ID); //Log.d("StopListLoader","Creating loader for stop "+stopID+" in position: "+id); if(stopID!=null) { builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stop").appendPath(stopID); CursorLoader cursorLoader = new CursorLoader(getContext(),builder.build(),dataProjection,null,null,null); return cursorLoader; } else return null; } @Override public void onLoadFinished(Loader loader, Cursor data) { //check that we have valid data if(data==null) return; final int numRows = data.getCount(); final int elementIdx = loader.getId(); if (numRows==0) { Log.w(this.getClass().getName(),"No info for stop in position "+elementIdx); return; } else if(numRows>1){ Log.d("StopLoading","we have "+numRows+" rows, should only have 1. Taking the first..."); } final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); data.moveToFirst(); Stop stopToModify = stopList.get(elementIdx); final String linesStopping = data.getString(linesIndex); stopToModify.setRoutesThatStopHere(Arrays.asList(linesStopping.split(","))); try { final String possibleLocation = data.getString(data.getColumnIndexOrThrow(StopsTable.COL_LOCATION)); if (stopToModify.location == null && possibleLocation != null && !possibleLocation.isEmpty() && !possibleLocation.equals("_")) { stopToModify.location = possibleLocation; } if (stopToModify.type == null) { stopToModify.type = Route.Type.fromCode(data.getInt(data.getColumnIndex(StopsTable.COL_TYPE))); } }catch (IllegalArgumentException arg){ if(arg.getMessage().contains("'location' does not exist")) Log.w("StopLoading","stop with no location found"); } //Log.d("StopListFragmentLoader","Finished parsing data for stop in position "+elementIdx); mListAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader loader) { loader.abandon(); } + } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/GTFSPositionsViewModel.kt similarity index 99% rename from app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt rename to app/src/main/java/it/reyboz/bustorino/viewmodels/GTFSPositionsViewModel.kt index 3dcbb76..66110d8 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/GTFSPositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/GTFSPositionsViewModel.kt @@ -1,206 +1,206 @@ /* BusTO - View Models 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.fragments +package it.reyboz.bustorino.viewmodels 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.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest import it.reyboz.bustorino.data.* import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import kotlinx.coroutines.delay import kotlinx.coroutines.launch /** * View Model for the map. For containing the stops, the trips and whatever */ class GTFSPositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) private val netVolleyManager = NetworkVolleyManager.getInstance(application) val positionsLiveData = MutableLiveData>() private val positionsRequestRunning = MutableLiveData() private val positionRequestListener = 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 server") return } else { //Log.i(DEBUG_TI, "Posting value to positionsLiveData") viewModelScope.launch { positionsLiveData.postValue(it) } } } //whatever the result, launch again the update TODO } } 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(){ if(positionsRequestRunning.value == null || !positionsRequestRunning.value!!) { val request = GtfsRtPositionsRequest(positionRequestErrorListener, positionRequestListener) netVolleyManager.requestQueue.add(request) Log.i(DEBUG_TI, "Requested GTFS realtime position updates") positionsRequestRunning.value = true } } /*suspend fun requestDelayedUpdates(timems: Long){ delay(timems) requestUpdates() } */ fun requestDelayedUpdates(timems: Long){ viewModelScope.launch { delay(timems) requestUpdates() } } // TRIPS IDS that have to be queried to the DB val tripsIDsInUpdates : LiveData> = positionsLiveData.map { Log.i(DEBUG_TI, "positionsLiveData changed") //allow new requests for the positions of buses positionsRequestRunning.value = false //add "gtt:" prefix because it's implicit in GTFS Realtime API return@map it.map { pos -> "gtt:"+pos.tripID } } //this holds the trips that have been downloaded but for which we have no pattern /*private val gtfsTripsInDBMissingPattern = tripsIDsInUpdates.map { tripsIDs -> val tripsInDB = gtfsRepo.gtfsDao.getTripsFromIDs(tripsIDs) val tripsPatternCodes = tripsInDB.map { tr -> tr.patternId } val codesInDB = gtfsRepo.gtfsDao.getPatternsCodesInTheDB(tripsPatternCodes) tripsInDB.filter { !(codesInDB.contains(it.patternId)) } }*/ //private val patternsCodesInDB = tripsDBPatterns.map { gtfsRepo.gtfsDao.getPatternsCodesInTheDB(it) } // trips that are in the DB, together with the pattern. If the pattern is not present in the DB, it's null val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap { //Log.i(DEBUG_TI, "tripsIds in updates changed: ${it.size}") gtfsRepo.gtfsDao.getTripPatternStops(it) } //trip IDs to query, which are not present in the DB 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) } else { Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") return@map ArrayList() } } val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> Log.i(DEBUG_TI, "Mapping trips and patterns") val mdict = HashMap>() //missing patterns val routesToDownload = HashSet() if(positionsLiveData.value!=null) for(update in positionsLiveData.value!!){ val trID = 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)) } return@map mdict } /* There are two strategies for the queries, since we cannot query a bunch of tripIDs all together to Mato API -> we need to query each separately: 1 -> wait until they are all queried to insert in the DB 2 -> after each request finishes, insert it into the DB Keep in mind that trips DO CHANGE often, and so do the Patterns */ fun downloadTripsFromMato(trips: List): Boolean{ return MatoTripsDownloadWorker.downloadTripsFromMato(trips,getApplication(), DEBUG_TI) } private fun downloadMissingPatterns(routeIds: List): Boolean{ return MatoPatternsDownloadWorker.downloadPatternsForRoutes(routeIds, getApplication()) } init { Log.d(DEBUG_TI, "MapViewModel created") Log.d(DEBUG_TI, "Observers of positionsLiveData ${positionsLiveData.hasActiveObservers()}") positionsRequestRunning.value = false; } fun testCascade(){ val n = ArrayList() n.add(LivePositionUpdate("22920721U","lala","lalal","lol","ASD", 1000.0,1000.0, 9000.0f, 21838191, null )) positionsLiveData.value = n } /** * Start downloading missing GtfsTrips and Insert them in the DB */ companion object{ private const val DEBUG_TI="BusTO-GTFSRTViewModel" const val DEFAULT_DELAY_REQUESTS: Long=4000 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt similarity index 97% rename from app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt rename to app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt index 94442f9..38949b1 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt @@ -1,107 +1,108 @@ -package it.reyboz.bustorino.fragments +package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.* +import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.data.gtfs.GtfsDatabase import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop import java.util.concurrent.Executors class LinesViewModel(application: Application) : AndroidViewModel(application) { private val gtfsRepo: GtfsRepository private val oldRepo: OldDataRepository //val patternsByRouteLiveData: LiveData> private val routeIDToSearch = MutableLiveData() private var lastShownPatternStops = ArrayList() val currentPatternStops = MutableLiveData>() val selectedPatternLiveData = MutableLiveData() val stopsForPatternLiveData = MutableLiveData>() private val executor = Executors.newFixedThreadPool(2) val mapShowing = MutableLiveData(true) fun setMapShowing(yes: Boolean){ mapShowing.value = yes //retrigger redraw stopsForPatternLiveData.postValue(stopsForPatternLiveData.value) } init { val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() gtfsRepo = GtfsRepository(gtfsDao) oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) } val routesGTTLiveData: LiveData> by lazy{ gtfsRepo.getLinesLiveDataForFeed("gtt") } val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap { gtfsRepo.getPatternsWithStopsForRouteID(it) } fun setRouteIDQuery(routeID: String){ routeIDToSearch.value = routeID } fun getRouteIDQueried(): String?{ return routeIDToSearch.value } var shouldShowMessage = true fun setPatternToDisplay(patternStops: MatoPatternWithStops){ selectedPatternLiveData.value = patternStops } /** * Find the */ private fun requestStopsForGTFSIDs(gtfsIDs: List){ if (gtfsIDs.equals(lastShownPatternStops)){ //nothing to do return } oldRepo.requestStopsWithGtfsIDs(gtfsIDs) { if (it.isSuccess) { stopsForPatternLiveData.postValue(it.result) } else { Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID") it.exception?.printStackTrace() } } lastShownPatternStops.clear() for(id in gtfsIDs) lastShownPatternStops.add(id) } fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){ val gtfsIDs = ArrayList() for(pat in patternStops.stopsIndices){ gtfsIDs.add(pat.stopGtfsId) } requestStopsForGTFSIDs(gtfsIDs) } /*fun getLinesGTT(): MutableLiveData> { val routesData = MutableLiveData>() viewModelScope.launch { val routes=gtfsRepo.getLinesForFeed("gtt") routesData.postValue(routes) } return routesData }*/ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt index 618784e..0b3f477 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/MQTTPositionsViewModel.kt @@ -1,165 +1,164 @@ /* 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 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 it.reyboz.bustorino.fragments.GTFSPositionsViewModel import kotlinx.coroutines.launch typealias UpdatesMap = HashMap class MQTTPositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) //private val updates = UpdatesMap() private val updatesLiveData = MutableLiveData>() private var mqttClient = MQTTMatoClient.getInstance() private var lineListening = "" private var lastTimeReceived: Long = 0 private val positionListener = 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.value = (mupds) lastTimeReceived = time } } //find the trip IDs in the updates private val tripsIDsInUpdates = updatesLiveData.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 changed: ${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 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!!){ 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 requestPosUpdates(line: String){ lineListening = line viewModelScope.launch { mqttClient.startAndSubscribe(line,positionListener, getApplication()) } //updatePositions(1000) } fun stopPositionsListening(){ viewModelScope.launch { val tt = System.currentTimeMillis() mqttClient.desubscribe(positionListener) val time = System.currentTimeMillis() -tt Log.d(DEBUG_TI, "Took $time ms to unsubscribe") } } fun retriggerPositionUpdate(){ if(updatesLiveData.value!=null){ updatesLiveData.postValue(updatesLiveData.value) } } companion object{ private const val DEBUG_TI = "BusTO-MQTTLiveData" } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt new file mode 100644 index 0000000..fced032 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt @@ -0,0 +1,55 @@ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map +import it.reyboz.bustorino.backend.Result +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.data.GtfsRepository +import it.reyboz.bustorino.data.NextGenDB +import it.reyboz.bustorino.data.OldDataRepository +import it.reyboz.bustorino.data.gtfs.GtfsDatabase +import org.osmdroid.util.BoundingBox +import java.util.ArrayList +import java.util.concurrent.Executors + +class StopsMapViewModel(application: Application): AndroidViewModel(application) { + + + private val executor = Executors.newFixedThreadPool(2) + private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) + /* + private val boundingBoxLiveData = MutableLiveData() + + fun setStopBoundingBox(bb: BoundingBox){ + boundingBoxLiveData.value = bb + } + + */ + + val stopsInBoundingBox = MutableLiveData>() + + private val callback = + OldDataRepository.Callback> { result -> + result.let { + if(it.isSuccess){ + stopsInBoundingBox.postValue(it.result) + Log.d(DEBUG_TAG, "Setting value of stops in bounding box") + } + + } + } + + fun requestStopsInBoundingBox(bb: BoundingBox) { + bb.let { + Log.d(DEBUG_TAG, "Launching stop request") + oldRepo.requestStopsInArea(it.latSouth, it.latNorth, it.lonWest, it.lonEast, callback) + } + } + companion object{ + private const val DEBUG_TAG = "BusTOStopMapViewModel" + } +} \ No newline at end of file