diff --git a/app/build.gradle b/app/build.gradle --- a/app/build.gradle +++ b/app/build.gradle @@ -134,7 +134,7 @@ implementation "androidx.coordinatorlayout:coordinatorlayout:1.3.0" - implementation 'org.jsoup:jsoup:1.21.2' + implementation 'org.jsoup:jsoup:1.22.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' //maplibre @@ -149,14 +149,13 @@ implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime - implementation 'com.google.protobuf:protoc:4.33.0' - - implementation 'com.google.protobuf:protobuf-javalite:4.33.0' + implementation 'com.google.protobuf:protoc:4.34.1' + implementation 'com.google.protobuf:protobuf-javalite:4.34.1' // mqtt library //implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' //implementation 'com.github.hannesa2:paho.mqtt.android:4.4' - implementation("com.hivemq:hivemq-mqtt-client:1.3.10") - implementation(platform("com.hivemq:hivemq-mqtt-client-websocket:1.3.10")) + implementation("com.hivemq:hivemq-mqtt-client:1.3.13") + implementation(platform("com.hivemq:hivemq-mqtt-client-websocket:1.3.13")) // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData @@ -173,15 +172,9 @@ //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" - implementation 'de.siegmar:fastcsv:2.2.2' + implementation 'de.siegmar:fastcsv:4.2.0' testImplementation 'junit:junit:4.13.2' - implementation 'junit:junit:4.13.2' - - implementation "androidx.test.ext:junit:1.3.0" - implementation "androidx.test:core:$androidXTestVersion" - implementation "androidx.test:runner:$androidXTestVersion" - implementation "androidx.room:room-testing:$room_version" androidTestImplementation "androidx.test.ext:junit:1.3.0" androidTestImplementation "androidx.test:core:$androidXTestVersion" diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -80,7 +80,7 @@ } @Override - public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom){ + public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); @@ -89,6 +89,16 @@ tr.addToBackStack("LineonMap-"+routeGtfsId); tr.commit(); + } + @Override + public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { + readyGUIfor(FragmentKind.LINES); + FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); + tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgsPattern(routeGtfsId, optionalPatternId, args)); + tr.addToBackStack("Line-"+routeGtfsId); + tr.commit(); } + } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -24,18 +24,15 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.*; -import android.widget.FrameLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; -import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.Insets; import androidx.core.view.*; import androidx.drawerlayout.widget.DrawerLayout; @@ -729,17 +726,26 @@ mNavView.setCheckedItem(R.id.nav_arrivals); } @Override - public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom){ + public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); - tr.addToBackStack("LineonMap-"+routeGtfsId); + tr.addToBackStack("LineFromStop-"+routeGtfsId); tr.commit(); + } + @Override + public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { + readyGUIfor(FragmentKind.LINES); + FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); + tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgsPattern(routeGtfsId, optionalPatternId, args)); + tr.addToBackStack("LineFromOther-"+routeGtfsId); + tr.commit(); } @Override diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java --- a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -286,7 +286,7 @@ final String name = r.getName(); final String destination = r.destinazione; if (name!= null && destination!=null) - myMap.put(new Pair<>(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); + myMap.put(new Pair<>(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); } return myMap; } diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java --- a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -264,7 +264,7 @@ void showRouteFullDirection(Route route); /** - * Show the line with all the stops in the app + * Show the line with all the stops in the line screen * @param route partial line info */ void requestShowingRoute(Route route); diff --git a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java --- a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java @@ -344,6 +344,11 @@ return name.replace(" ",""); } + /** + * Create the line name in GTFS format (e.g., "gtt:10U") from a more human readable name ("10") + * @param route the route object + * @return the code for the line in GTFS format + */ public static String getGtfsRouteID(Route route){ String routeName = route.getName(); String cutName = routeName.replace("\\s", ""); diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java @@ -22,8 +22,8 @@ import androidx.annotation.NonNull; import de.siegmar.fastcsv.reader.CloseableIterator; -import de.siegmar.fastcsv.reader.NamedCsvReader; -import de.siegmar.fastcsv.reader.NamedCsvRow; +import de.siegmar.fastcsv.reader.CsvReader; +import de.siegmar.fastcsv.reader.NamedCsvRecord; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.networkTools; import it.reyboz.bustorino.data.gtfs.CsvTableInserter; @@ -195,8 +195,8 @@ //System.out.println(Arrays.toString(elements)); //lineElements = readCsvLine(header); - NamedCsvReader csvReader = NamedCsvReader.builder().build(reader); - CloseableIterator iterator = csvReader.iterator(); + CsvReader csvReader = CsvReader.builder().ofNamedCsvRecord(reader); + CloseableIterator iterator = csvReader.iterator(); final CsvTableInserter inserter = new CsvTableInserter(tableName,con); @@ -225,7 +225,12 @@ int c = 0; while (iterator.hasNext()){ - final Map rowsMap = iterator.next().getFields(); + //final Map rowsMap = iterator.next().getFields(); + final NamedCsvRecord record = iterator.next(); + final Map rowsMap = new HashMap<>(); + for (String col: record.getHeader()){ + rowsMap.put(col, record.getField(col)); + } if (c < 1){ Log.d(DEBUG_TAG, " in map:"+rowsMap); c++; diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt @@ -19,11 +19,15 @@ import com.google.transit.realtime.GtfsRealtime.VehiclePosition +/** + * General data class for the live position update + * Used in both the GTFS and MaTO services + */ data class LivePositionUpdate( val tripID: String, //tripID WITHOUT THE "gtt:" prefix val startTime: String?, val startDate: String?, - val routeID: String, + val routeID: String, // routeID DOES NOT HAVE THE "gtt:" PREFIX val vehicle: String, var latitude: Double, @@ -53,23 +57,10 @@ position.timestamp, null ) - /*data class VehicleInfo( - val id: String, - val label:String - ) - - */ - /*fun withNewPositionAndBearing(latitude: Double, longitude: Double, bearing: Float) = - LivePositionUpdate(this.tripID, this.startTime, this.startTime, - this.routeID, this.vehicle, latitude, longitude, bearing, - this.timestamp,this.nextStop) - fun withNewPosition(latitude: Double, longitude: Double) = - LivePositionUpdate(this.tripID, this.startTime, this.startTime, - this.routeID, this.vehicle, latitude, longitude, this.bearing, - this.timestamp,this.nextStop) - - */ + fun getLineGTFSFormat(): String{ + return "gtt:$routeID" + } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/utils.java b/app/src/main/java/it/reyboz/bustorino/backend/utils.java --- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java @@ -32,12 +32,8 @@ import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; +import java.text.SimpleDateFormat; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -390,4 +386,23 @@ return Html.fromHtml(text); } } + /** + * Convert an integer (long) timestamp into a String + * @param timestamp the timestamp in seconds (NOT milliseconds) + * @return the formatted String + */ + public static String unixTimestampToLocalTime(long timestamp){ + return unixTimestampToLocalTime(timestamp, "dd/MM/yyyy HH:mm:ss"); + } + /** + * Convert an integer (long) timestamp into a String + * @param timestamp the timestamp in seconds (NOT milliseconds) + * @param patternFormat the format to convert it to + * @return the formatted String + */ + public static String unixTimestampToLocalTime(long timestamp, String patternFormat) { + Date date = new Date(timestamp * 1000L); // seconds to milliseconds + SimpleDateFormat format = new SimpleDateFormat(patternFormat, Locale.getDefault()); + return format.format(date); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java --- a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java +++ b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java @@ -33,7 +33,7 @@ import androidx.annotation.Nullable; import de.siegmar.fastcsv.reader.CloseableIterator; import de.siegmar.fastcsv.reader.CsvReader; -import de.siegmar.fastcsv.reader.CsvRow; +import de.siegmar.fastcsv.reader.CsvRecord; import de.siegmar.fastcsv.writer.CsvWriter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.StopsDBInterface; @@ -342,27 +342,27 @@ Cursor cursor = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray,null,null,null,null, sortOrder); final int nCols = 2;//cursor.getColumnCount(); - writer.writeRow(cursor.getColumnNames()); + writer.writeRecord(cursor.getColumnNames()); while (cursor.moveToNext()){ String[] arr = {cursor.getString(0), cursor.getString(1)}; - writer.writeRow(arr); + writer.writeRecord(arr); } cursor.close(); return true; } - public int insertRowsFromCSV(CsvReader reader){ + public int insertRowsFromCSV(CsvReader reader){ SQLiteDatabase db = this.getWritableDatabase(); boolean firstrow = true; final HashMap colIndexByRows = new HashMap<>(); - final CloseableIterator rowsIter = reader.iterator(); + final CloseableIterator rowsIter = reader.iterator(); if (!rowsIter.hasNext()){ //nothing to do, it's an empty file return -1; } - final CsvRow firstRow = rowsIter.next(); + final CsvRecord firstRow = rowsIter.next(); // close if there isn't another rows if(!rowsIter.hasNext()) return -2; for (int i =0; i - loadZipData(uri,loadFavorites, loadPreferences) } } @@ -198,7 +196,7 @@ FAVORITES_NAME -> if (loadFavorites) { val reader = InputStreamReader(zipstream) - val csvReader = CsvReader.builder().build(reader) + val csvReader = CsvReader.builder().ofCsvRecord(reader) val userDB = UserDB(context) val updated = userDB.insertRowsFromCSV(csvReader) @@ -258,7 +256,7 @@ val contentResolver = context.contentResolver contentResolver.openInputStream(uri)?.use { InputStreamReader(it).use { stream -> - val csvReader = CsvReader.builder().build(stream) + val csvReader = CsvReader.builder().ofCsvRecord(stream) val userDB = UserDB(context) val updated = userDB.insertRowsFromCSV(csvReader) diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java @@ -1,5 +1,6 @@ package it.reyboz.bustorino.fragments; +import android.os.Bundle; import androidx.annotation.Nullable; import it.reyboz.bustorino.backend.Stop; @@ -37,8 +38,16 @@ void showMapCenteredOnStop(Stop stop); /** - * We want to show the line in detail for route + * We want to show the line in detail for route coming from a stop * @param routeGtfsId the route gtfsID (eg, "gtt:10U") */ - void showLineOnMap(String routeGtfsId,@Nullable String fromStopID); + void openLineFromStop(String routeGtfsId, @Nullable String fromStopID); + + /** + * Open the line screen on the line, from a live vehicle (optional pattern) + * @param routeGtfsId the route gtfsID (eg, "gtt:10U") + * @param optionalPatternId the pattern name (can be null) + * @param args extra arguments given as Bundle + */ + void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args); } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt @@ -15,27 +15,36 @@ import android.view.View import android.view.ViewGroup import android.view.animation.LinearInterpolator +import android.widget.ImageButton import android.widget.ImageView import android.widget.RelativeLayout import android.widget.TextView import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.ViewCompat +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.gson.JsonObject import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.FiveTNormalizer import it.reyboz.bustorino.backend.LivePositionTripPattern +import it.reyboz.bustorino.backend.LivePositionsServiceStatus 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.utils import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.MapLibreUtils import it.reyboz.bustorino.util.ViewUtils +import it.reyboz.bustorino.viewmodels.LivePositionsViewModel +import it.reyboz.bustorino.viewmodels.MapStateViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.maplibre.android.MapLibre -import org.maplibre.android.camera.CameraPosition import org.maplibre.android.geometry.LatLng import org.maplibre.android.location.LocationComponent import org.maplibre.android.location.LocationComponentOptions @@ -61,7 +70,7 @@ abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback { protected var map: MapLibreMap? = null protected var shownStopInBottomSheet : Stop? = null - protected var savedMapStateOnPause : Bundle? = null + //protected var savedMapStateOnPause : Bundle? = null protected var fragmentListener: CommonFragmentListener? = null @@ -95,11 +104,13 @@ protected lateinit var stopTitleTextView: TextView protected lateinit var stopNumberTextView: TextView protected lateinit var linesPassingTextView: TextView + protected lateinit var extraBottomTextView: TextView protected lateinit var arrivalsCard: CardView protected lateinit var directionsCard: CardView protected lateinit var bottomrightImage: ImageView - protected lateinit var locationComponent: LocationComponent + protected lateinit var busPositionsIconButton: ImageButton + protected var lastLocation : Location? = null @@ -115,9 +126,13 @@ //extra items to use the LibreMap - protected lateinit var symbolManager : SymbolManager + protected var symbolManager : SymbolManager? = null protected var stopActiveSymbol: Symbol? = null protected var stopsLayerStarted = false + protected val livePositionsViewModel : LivePositionsViewModel by activityViewModels() + + //private lateinit var symbolManager: SymbolManager + protected val mapStateViewModel: MapStateViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -150,6 +165,9 @@ directionsCard = view.findViewById(R.id.directionsCardButton) bottomrightImage = view.findViewById(R.id.rightmostImageView) bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + extraBottomTextView = view.findViewById(R.id.extraBottomTextView) + + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN } override fun onResume() { @@ -211,6 +229,7 @@ } else throw RuntimeException("$context must implement CommonFragmentListener") } + /* protected fun restoreMapStateFromBundle(bundle: Bundle): Boolean{ val nullDouble = -10_000.0 var boundsRestored =false @@ -266,6 +285,8 @@ return b } + */ + protected fun stopToGeoJsonFeature(s: Stop): Feature{ return Feature.fromGeometry( Point.fromLngLat(s.longitude!!, s.latitude!!), @@ -299,15 +320,15 @@ } } if (vehShowing==v){ - hideStopBottomSheet() + hideStopOrBusBottomSheet() } } } // Hide the bottom sheet and remove extra symbol - protected fun hideStopBottomSheet(){ + protected fun hideStopOrBusBottomSheet(){ if (stopActiveSymbol!=null){ - symbolManager.delete(stopActiveSymbol) + symbolManager?.delete(stopActiveSymbol) stopActiveSymbol = null } if(!showOpenStopWithSymbolLayer()){ @@ -323,22 +344,35 @@ vehShowing = "" updatePositionsIcons(true) } + extraBottomTextView.visibility = View.GONE } protected fun initSymbolManager(mapReady: MapLibreMap , style: Style){ - symbolManager = SymbolManager(mapView,mapReady,style) - symbolManager.iconAllowOverlap = true - symbolManager.textAllowOverlap = false - - symbolManager.addClickListener{ _ -> - if (stopActiveSymbol!=null){ - hideStopBottomSheet() - + val sm = SymbolManager(mapView, mapReady, style) + sm.iconAllowOverlap = true + sm.textAllowOverlap = false + sm.addClickListener { _ -> + if (stopActiveSymbol != null) { + hideStopOrBusBottomSheet() return@addClickListener true } else return@addClickListener false } + symbolManager = sm + } + + /** + * Change the icon indicating the status of the live Positions + */ + protected fun setBusPositionsIcon(enabled: Boolean, error: Boolean){ + val ctx = requireContext() + if(!enabled) + busPositionsIconButton.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.bus_pos_circle_inactive)) + else if(error) + busPositionsIconButton.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.bus_pos_circle_notworking)) + else + busPositionsIconButton.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.bus_pos_circle_active)) } @@ -493,6 +527,53 @@ updatePositionsIcons(false) } + /** + * Shared bottom sheet setup. The [onDirectionsClick] lambda is called when + * directionsCard is tapped; it receives the pattern code (empty string when + * no pattern is available) so each subclass can navigate as it sees fit. + */ + protected fun showVehicleTripInBottomSheet( + veh: String, + onDirectionsClick: (patternCode: String) -> Unit + ) { + val data = updatesByVehDict[veh] ?: run { + Log.w(DEBUG_TAG, "Asked to show vehicle $veh, but it's not present in the updates") + return + } + bottomLayout?.let { + val lineName = FiveTNormalizer.fixShortNameForDisplay( + GtfsUtils.getLineNameFromGtfsID(data.posUpdate.routeID), true + ) + val pat = data.pattern + if (pat != null) { + stopTitleTextView.text = pat.headsign + stopTitleTextView.visibility = View.VISIBLE + stopNumberTextView.text = getString(R.string.line_fill_towards, lineName) + } else { + stopTitleTextView.visibility = View.GONE + stopNumberTextView.text = getString(R.string.line_fill, lineName) + } + directionsCard.setOnClickListener { + onDirectionsClick(pat?.code ?: "") + } + directionsCard.visibility = View.VISIBLE + bottomrightImage.setImageDrawable( + ResourcesCompat.getDrawable(resources, R.drawable.ic_magnifying_glass, activity?.theme) + ) + val colorBlue = ResourcesCompat.getColor(resources, R.color.blue_500, activity?.theme) + ViewCompat.setBackgroundTintList(directionsCard, ColorStateList.valueOf(colorBlue)) + linesPassingTextView.text = getString(R.string.vehicle_fill, data.posUpdate.vehicle) + arrivalsCard.visibility = View.GONE + + extraBottomTextView.text = getString(R.string.updated_fill, utils.unixTimestampToLocalTime(data.posUpdate.timestamp)) + extraBottomTextView.visibility = View.VISIBLE + } + vehShowing = veh + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + updatePositionsIcons(true) + Log.d(DEBUG_TAG, "Shown vehicle $veh in bottom sheet") + } + /** * Update the bus positions displayed on the map, from the existing data * @@ -644,14 +725,13 @@ Log.d(DEBUG_TAG, "Showing stop: ${stop.ID}") if (showOpenStopWithSymbolLayer()) { - stopActiveSymbol = symbolManager.create( + stopActiveSymbol = symbolManager?.create( SymbolOptions() .withLatLng(LatLng(stop.latitude!!, stop.longitude!!)) .withIconImage(STOP_ACTIVE_IMG) .withIconAnchor(ICON_ANCHOR_CENTER) - ) - } else{ + } else { val list = ArrayList() list.add(stopToGeoJsonFeature(stop)) selectedStopSource.setGeoJson( @@ -769,15 +849,26 @@ } style.addLayerAbove(busesLayer, STOPS_LAYER_ID) - val selectedBusLayer = SymbolLayer(SEL_BUS_LAYER, SEL_BUS_SOURCE).withProperties( + val selectedBusLayer = SymbolLayer(SEL_BUS_LAYER, SEL_BUS_SOURCE).apply { + withProperties( PropertyFactory.iconImage(BUS_SEL_IMAGE_ID), PropertyFactory.iconSize(busIconsScale), PropertyFactory.iconAllowOverlap(true), PropertyFactory.iconIgnorePlacement(true), PropertyFactory.iconRotate(Expression.get("bearing")), PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP) + ) + if (withLabels){ + withProperties(PropertyFactory.textAnchor(TEXT_ANCHOR_CENTER), + PropertyFactory.textAllowOverlap(true), + PropertyFactory.textField(Expression.get("line")), + PropertyFactory.textColor(Color.WHITE), + PropertyFactory.textRotationAlignment(TEXT_ROTATION_ALIGNMENT_VIEWPORT), + PropertyFactory.textSize(12f), + PropertyFactory.textFont(arrayOf("noto_sans_regular"))) + } + } - ) style.addLayerAbove(selectedBusLayer, BUSES_LAYER_ID) } @@ -791,6 +882,35 @@ return locManager.allProviders.contains(LocationManager.GPS_PROVIDER) } + /** + * Update automatically the icon when the live position service changes status + */ + protected fun observeStatusLivePositions(){ + livePositionsViewModel.serviceStatus.observe(viewLifecycleOwner){ status -> + //if service is active, update the bus positions icon + when(status) { + LivePositionsServiceStatus.OK -> + setBusPositionsIcon(true, error = false) + + LivePositionsServiceStatus.NO_POSITIONS -> setBusPositionsIcon(true, error = true) + + else -> setBusPositionsIcon( true, error = true) + } + } + } + + /** + * Clear all buses from the map + */ + protected fun clearAllBusPositionsInMap(){ + for ((k, anim) in animatorsByVeh){ + anim.cancel() + } + animatorsByVeh.clear() + updatesByVehDict.clear() + updatePositionsIcons(forced = false) + } + companion object{ private const val DEBUG_TAG="GeneralMapLibreFragment" diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -36,7 +36,6 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.ViewCompat -import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager @@ -59,7 +58,7 @@ import it.reyboz.bustorino.middleware.LocationUtils import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.viewmodels.LinesViewModel -import it.reyboz.bustorino.viewmodels.LivePositionsViewModel +import it.reyboz.bustorino.viewmodels.MapStateViewModel import kotlinx.coroutines.Runnable import org.maplibre.android.camera.CameraPosition import org.maplibre.android.camera.CameraUpdateFactory @@ -102,7 +101,6 @@ private var patternShown: 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 @@ -136,7 +134,8 @@ private lateinit var stopsRecyclerView: RecyclerView private lateinit var descripTextView: TextView - private var stopIDFromToShow: String? = null + private var stopIDFromToShow = "" + private var patternIdToShow = "" //adapter for recyclerView private val stopAdapterListener= object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { @@ -205,7 +204,8 @@ private var showOnTopOfLine = false private var recyclerInitDone = false - private var useMQTTPositions = true + private var usingMQTTPositions = true + private var restoredCameraInMap = false @@ -213,15 +213,14 @@ private val tripMarkersAnimators = HashMap() - private val liveBusViewModel: LivePositionsViewModel by activityViewModels() - //extra items to use the LibreMap override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val args = requireArguments() lineID = args.getString(LINEID_KEY,"") - stopIDFromToShow = args.getString(STOPID_FROM_KEY) + stopIDFromToShow = args.getString(STOPID_FROM_KEY, "") //can be null + patternIdToShow = args.getString(PATTERN_SHOW_KEY, "") } @SuppressLint("SetTextI18n") @@ -239,9 +238,15 @@ //lineID = requireArguments().getString(LINEID_KEY, "") arguments?.let { lineID = it.getString(LINEID_KEY, "") + stopIDFromToShow = it.getString(STOPID_FROM_KEY, "") //can be null + patternIdToShow = it.getString(PATTERN_SHOW_KEY, "") + Log.d(DEBUG_TAG, "LineID selected: $lineID, stopIDFromToShow: $stopIDFromToShow, patternIdToShow: $patternIdToShow") } + switchButton = rootView.findViewById(R.id.switchImageButton) locationIcon = rootView.findViewById(R.id.locationEnableIcon) + busPositionsIconButton = rootView.findViewById(R.id.busPositionsImageButton) + favoritesButton = rootView.findViewById(R.id.favoritesButton) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) descripTextView = rootView.findViewById(R.id.lineDescripTextView) @@ -254,7 +259,7 @@ // Setup close button rootView.findViewById(R.id.btnClose).setOnClickListener { - hideStopBottomSheet() + hideStopOrBusBottomSheet() } val titleTextView = rootView.findViewById(R.id.titleTextView) @@ -295,13 +300,46 @@ //set click Listener view.setOnClickListener(this::onPositionIconButtonClick) } + busPositionsIconButton.setOnClickListener { + LivePositionsDialogFragment().show(parentFragmentManager, "LivePositionsDialog") + } //set //INITIALIZE VIEW MODELS viewModel.setRouteIDQuery(lineID) - liveBusViewModel.setGtfsLineToFilterPos(lineID, null) + livePositionsViewModel.setGtfsLineToFilterPos(lineID, null) + //observe the change, clear buses when switching position + livePositionsViewModel.useMQTTPositionsLiveData.observe(viewLifecycleOwner){ useMQTT-> + //Log.d(DEBUG_TAG, "Changed MQTT positions, now have to use MQTT: $useMQTT") + if (isResumed) { + //Log.d(DEBUG_TAG, "Deciding to switch, the current source is using MQTT: $usingMQTTPositions") + if(useMQTT!=usingMQTTPositions){ + // we have to switch + val clearPos = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("positions_clear_on_switch_pref", true) + livePositionsViewModel.clearOldPositionsUpdates() + if(useMQTT){ + //switching to MQTT, the GTFS positions are disabled automatically + livePositionsViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + } else{ + //switching to GTFS RT: stop Mato, launch first request + livePositionsViewModel.stopMatoUpdates() + livePositionsViewModel.requestGTFSUpdates() + } + Log.d(DEBUG_TAG, "Should clear positions: $clearPos") + if (clearPos) { + livePositionsViewModel.clearAllPositions() + //force clear of the viewed data + if(vehShowing.isNotEmpty()) hideStopOrBusBottomSheet() + clearAllBusPositionsInMap() + } + + } + } + usingMQTTPositions = useMQTT + + } val keySourcePositions = getString(R.string.pref_positions_source) - useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) + usingMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ @@ -313,6 +351,8 @@ if(mapView.visibility ==View.VISIBLE) patternShown?.let{ // We have the pattern and the stops here, time to display them + //TODO: Decide if we should follow the camera view given by the previous screen (probably the map fragment) + // use !restoredCameraInMap to do so displayPatternWithStopsOnMap(it,stops, true) } ?:{ Log.w(DEBUG_TAG, "The viewingPattern is null!") @@ -353,11 +393,11 @@ stopAnimations() updatesByVehDict.clear() updatePositionsIcons(true) - liveBusViewModel.retriggerPositionUpdate() + livePositionsViewModel.retriggerPositionUpdate() } } } - liveBusViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern) + livePositionsViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern) } @@ -366,6 +406,8 @@ } Log.d(DEBUG_TAG, "Views created!") + observeStatusLivePositions() + return rootView } @@ -375,14 +417,15 @@ mapView.visibility = View.GONE stopsRecyclerView.visibility = View.VISIBLE locationIcon?.visibility = View.GONE + busPositionsIconButton?.visibility = View.GONE viewModel.setMapShowing(false) - if(useMQTTPositions) liveBusViewModel.stopMatoUpdates() + if(usingMQTTPositions) livePositionsViewModel.stopMatoUpdates() //map.overlayManager.remove(busPositionsOverlay) switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) - hideStopBottomSheet() + hideStopOrBusBottomSheet() if(locationComponent.isLocationComponentEnabled){ locationComponent.isLocationComponentEnabled = false @@ -395,14 +438,16 @@ stopsRecyclerView.visibility = View.GONE mapView.visibility = View.VISIBLE locationIcon?.visibility = View.VISIBLE + busPositionsIconButton.visibility = View.VISIBLE + viewModel.setMapShowing(true) //map.overlayManager.add(busPositionsOverlay) //map. - if(useMQTTPositions) - liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + if(usingMQTTPositions) + livePositionsViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else - liveBusViewModel.requestGTFSUpdates() + livePositionsViewModel.requestGTFSUpdates() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) @@ -469,6 +514,7 @@ */ override fun onMapReady(mapReady: MapLibreMap) { this.map = mapReady + var setViewAlready = false val context = requireContext() val mjson = MapLibreStyles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) //ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json") @@ -491,11 +537,6 @@ setupBusLayer(style) initSymbolManager(mapReady, style) - - mapViewModel.stopShowing?.let { - openStopInBottomSheet(it) - } - mapViewModel.stopShowing = null toRunWhenMapReady?.run() toRunWhenMapReady = null mapInitialized.set(true) @@ -504,9 +545,33 @@ viewModel.stopsForPatternLiveData.value?.let { Log.d(DEBUG_TAG, "Show stops from the cache") displayPatternWithStopsOnMap(patternShown!!, it, true) + //Show stop from cache + mapStateViewModel.lastOpenStopID.value?.let{ sID-> + val s= it.filter { stop -> stop.ID==sID } + if (s.isEmpty()) { + if(sID.isNotEmpty()) + Log.w(DEBUG_TAG,"Wanted to open stop $sID in map but it was not loaded!") + } + else openStopInBottomSheet(s[0]) + + } } + + } + var restoredMapState = mapStateViewModel.restoreMapState(mapReady) + arguments?.let { args -> + // if there is a Camera State in the arguments, set it for the new camera (doesn't work yet!) + if (!restoredMapState && MapCameraState.checkInBundle(args)) { + val initCamState = MapCameraState.fromBundle(args) + //map?.let{ + MapStateViewModel.restoreMapState(mapReady, initCamState) + setViewAlready = true + restoredMapState = true + } + } + restoredCameraInMap = restoredMapState } mapReady.addOnMapClickListener { point -> @@ -521,7 +586,7 @@ val stop = viewModel.getStopByID(id) stop?.let { if (isBottomSheetShowing() || vehShowing.isNotEmpty()){ - hideStopBottomSheet() + hideStopOrBusBottomSheet() } openStopInBottomSheet(it) @@ -535,7 +600,7 @@ val vehid = feature.getStringProperty("veh") val route = feature.getStringProperty("line") if(isBottomSheetShowing()) - hideStopBottomSheet() + hideStopOrBusBottomSheet() //if(context!=null){ // Toast.makeText(context, "Veh $vehid on route ${route.slice(0..route.length-2)}", Toast.LENGTH_SHORT).show() //} @@ -556,18 +621,11 @@ // we start requesting the bus positions now observeBusPositionUpdates() - } - /*savedMapStateOnPause?.let{ - restoreMapStateFromBundle(it) - pendingLocationActivation = false - Log.d(DEBUG_TAG, "Restored map state from the saved bundle") } - */ - val zoom = 12.0 val latlngTarget = LatLng(MapLibreFragment.DEFAULT_CENTER_LAT, MapLibreFragment.DEFAULT_CENTER_LON) - + if(!setViewAlready) mapReady.cameraPosition = savedCameraPosition ?:CameraPosition.Builder().target(latlngTarget).zoom(zoom).build() savedCameraPosition = null @@ -580,8 +638,10 @@ } private fun observeBusPositionUpdates(){ + + //live bus positions - liveBusViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ pair -> + livePositionsViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ pair -> //Log.d(DEBUG_TAG, "Received ${updates.size} updates for the positions") val updates = pair.first val vehiclesNotOnCorrectDir = pair.second @@ -596,13 +656,13 @@ showVehicleTripInBottomSheet(veh) } //if not using MQTT positions - if(!useMQTTPositions){ - liveBusViewModel.requestDelayedGTFSUpdates(2000) + if(!usingMQTTPositions){ + livePositionsViewModel.requestDelayedGTFSUpdates(2000) } } //download missing tripIDs - liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ + livePositionsViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.requestMatoTripsDownload( it, requireContext().applicationContext, @@ -611,63 +671,18 @@ } } - - private fun showVehicleTripInBottomSheet(veh: String){ - val data = updatesByVehDict[veh] - if(data==null) { - Log.w(DEBUG_TAG,"Asked to show vehicle $veh, but it's not present in the updates") - return - } - - bottomLayout?.let { - val lineName = FiveTNormalizer.fixShortNameForDisplay( - GtfsUtils.getLineNameFromGtfsID(data.posUpdate.routeID), true) - val pat = data.pattern - if (pat!=null){ - //WE HAVE THE DIRECTIONS DATA - stopTitleTextView.text = pat.headsign - stopTitleTextView.visibility = View.VISIBLE - Log.d(DEBUG_TAG, "Showing headsign ${pat.headsign} for vehicle $veh") - stopNumberTextView.text = requireContext().getString(R.string.line_fill_towards, lineName) - - bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_magnifying_glass, activity?.theme)) - directionsCard.setOnClickListener { - data.pattern?.let { - - if(patternShown?.pattern?.code == it.code){ - context?.let { c->Toast.makeText(c, R.string.showing_same_direction, Toast.LENGTH_SHORT).show() } - }else - showPatternWithCode(it.code) - } //TODO - // ?: { - // context?.let { ctx -> Toast.makeText(ctx,"") } - //} - } - //set color - val colorBlue = ResourcesCompat.getColor(resources,R.color.blue_500,activity?.theme) - ViewCompat.setBackgroundTintList(directionsCard, ColorStateList.valueOf(colorBlue)) - directionsCard.visibility = View.VISIBLE + private fun showVehicleTripInBottomSheet(veh: String) { + super.showVehicleTripInBottomSheet(veh) { patternCode -> + //this is checked in @GeneralMapLibreFragment + //val data = updatesByVehDict[veh] ?: return@showVehicleTripInBottomSheet + if (patternCode.isEmpty()) return@showVehicleTripInBottomSheet + if (patternShown?.pattern?.code == patternCode) { + Toast.makeText(context, R.string.showing_same_direction, Toast.LENGTH_SHORT).show() } else { - //stopTitleTextView.text = "NN" - stopTitleTextView.visibility = View.GONE - stopNumberTextView.text = requireContext().getString(R.string.line_fill, lineName) - directionsCard.visibility = View.GONE - + showPatternWithCode(patternCode) } - linesPassingTextView.text = requireContext().getString(R.string.vehicle_fill, data.posUpdate.vehicle) } - - arrivalsCard.visibility=View.GONE - - vehShowing = veh - bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED - - //call update position to color the bus special - updatePositionsIcons(true) - //isBottomSheetShowing = true - Log.d(DEBUG_TAG, "Shown vehicle $veh in bottom layout") } - // ------- MAP LAYERS INITIALIZE ---- /** * Initialize the map layers for the stops @@ -730,29 +745,29 @@ initStopsLayer(style, null, POLY_ARROWS_LAYER) } + private fun filterPatternFromArgs(patterns: List): MatoPatternWithStops?{ + var p: MatoPatternWithStops? = null - /** - * Save the loaded pattern data, without the stops! - */ - private fun savePatternsToShow(patterns: List){ - - currentPatterns = patterns.sortedWith(patternsSorter) - - patternsAdapter?.let { - it.clear() - it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) - it.notifyDataSetChanged() + if (patternIdToShow.isNotEmpty()){ + for (patt in currentPatterns) { + if (patt.pattern.code == patternIdToShow){ + p = patt + } + } + if(p==null) + Log.w(DEBUG_TAG, "We had to show the pattern with code $patternIdToShow, but we didn't find it") + else + Log.d(DEBUG_TAG, "Requesting to show pattern with code $patternIdToShow, found pattern ${p.pattern.code}") } // if we are loading from a stop, find it - val patternToShow = stopIDFromToShow?.let { sID -> - val stopGtfsID = "gtt:$sID" - var p: MatoPatternWithStops? = null + else if(stopIDFromToShow.isNotEmpty()) { + val stopGtfsID = "gtt:$stopIDFromToShow" var pLength = 0 - for(patt in currentPatterns){ - for(pstop in patt.stopsIndices){ - if(pstop.stopGtfsId == stopGtfsID){ + for (patt in currentPatterns) { + for (pstop in patt.stopsIndices) { + if (pstop.stopGtfsId == stopGtfsID) { //found - if (patt.stopsIndices.size>pLength){ + if (patt.stopsIndices.size > pLength) { p = patt pLength = patt.stopsIndices.size } @@ -761,20 +776,32 @@ } } } - p - } - if(stopIDFromToShow!=null){ - if(patternToShow==null) + if(p==null) Log.w(DEBUG_TAG, "We had to show the pattern from stop $stopIDFromToShow, but we didn't find it") else - Log.d(DEBUG_TAG, "Requesting to show pattern from stop $stopIDFromToShow, found pattern ${patternToShow.pattern.code}") + Log.d(DEBUG_TAG, "Requesting to show pattern from stop $stopIDFromToShow, found pattern ${p.pattern.code}") } - //unset the stopID to show - if(patternToShow!=null) { + stopIDFromToShow = "" + patternIdToShow = "" + return p + } + /** + * Save the loaded pattern data, without the stops! + */ + private fun savePatternsToShow(patterns: List){ + + currentPatterns = patterns.sortedWith(patternsSorter) + + patternsAdapter?.let { + it.clear() + it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) + it.notifyDataSetChanged() + } + val patternToShow = filterPatternFromArgs(patterns) + if(patternToShow!=null) { //showPattern(patternToShow) patternShown = patternToShow - stopIDFromToShow = null } patternShown?.let { showPattern(it) @@ -812,6 +839,9 @@ //setPatternAndReqStops(patternWs) } + /** + * Zoom on the map to get the pattern + */ private fun zoomToCurrentPattern(){ if(polyline==null) return val NULL_VALUE = -4000.0 @@ -927,27 +957,12 @@ Log.e(DEBUG_TAG, "Stops layer is not started!!") } - /* OLD CODE - for(s in stops){ - val gp = - 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) //} if(zoomToPattern) zoomToCurrentPattern() - //map.invalidate() } private fun initializeRecyclerView(){ @@ -999,31 +1014,16 @@ pausedFragment = false val keySourcePositions = getString(R.string.pref_positions_source) - useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) + usingMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") //separate paths - if(useMQTTPositions) - liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + if(usingMQTTPositions) + livePositionsViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else - liveBusViewModel.requestGTFSUpdates() - - - if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { - Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ - " zoom ${mapViewModel.currentZoom.value}") - //THIS WAS A FIX FOR THE OLD OSMDROID MAP - /*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!!) - } - */ - } + livePositionsViewModel.requestGTFSUpdates() + + //initialize GUI here fragmentListener?.readyGUIfor(FragmentKind.LINES) @@ -1032,24 +1032,19 @@ override fun onPause() { super.onPause() mapView.onPause() - if(useMQTTPositions) liveBusViewModel.stopMatoUpdates() + if(usingMQTTPositions) livePositionsViewModel.stopMatoUpdates() pausedFragment = true //save map - val camera = map?.cameraPosition - camera?.let {cam-> - mapViewModel.currentLat.value = cam.target?.latitude ?: -400.0 - mapViewModel.currentLong.value = cam.target?.longitude ?: -400.0 - mapViewModel.currentZoom.value = cam.zoom + map?.let{ + //if map is initialized + mapStateViewModel.saveMapState(it) } - + mapStateViewModel.lastOpenStopID.postValue(shownStopInBottomSheet?.ID) } override fun onStop() { super.onStop() mapView.onStop() - shownStopInBottomSheet?.let { - mapViewModel.stopShowing = it - } shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled } @@ -1093,6 +1088,7 @@ companion object { private const val LINEID_KEY="lineID" private const val STOPID_FROM_KEY="stopID" + private const val PATTERN_SHOW_KEY ="patternIDShow" private const val DEBUG_TAG="BusTO-LineDetalFragment" @@ -1103,6 +1099,14 @@ b.putString(STOPID_FROM_KEY, stopIDFrom) return b } + + fun makeArgsPattern(lineID: String, patternShow: String?, extraArgs: Bundle?): Bundle { + + val b= extraArgs ?: Bundle() + b.putString(LINEID_KEY, lineID) + b.putString(PATTERN_SHOW_KEY, patternShow) + return b + } fun newInstance(lineID: String?, stopIDFrom: String?) = LinesDetailFragment().apply { lineID?.let { arguments = makeArgs(it, stopIDFrom) } } @@ -1139,8 +1143,4 @@ private const val DEFAULT_CENTER_LAT = 45.12 private const val DEFAULT_CENTER_LON = 7.6858 } - - enum class BottomShowing{ - STOP, VEHICLE - } } \ 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 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -65,7 +65,7 @@ } private val routeClickListener = RouteAdapter.ItemClicker { - fragmentListener.showLineOnMap(it.gtfsId, null) + fragmentListener.openLineFromStop(it.gtfsId, null) } private val arrows = HashMap() private val durations = HashMap() @@ -158,7 +158,7 @@ //create new item click listener every time val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ -> val r = routes[pos] - fragmentListener.showLineOnMap(r.gtfsId, null) + fragmentListener.openLineFromStop(r.gtfsId, null) } favoritesRecyclerView.adapter = adapter } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -725,9 +725,14 @@ } @Override - public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom) { + public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom) { //pass to activity - mListener.showLineOnMap(routeGtfsId, stopIDFrom); + mListener.openLineFromStop(routeGtfsId, stopIDFrom); + } + + @Override + public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { + mListener.openLineFromVehicle(routeGtfsId, optionalPatternId, args); } @Override diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -4,6 +4,7 @@ import android.Manifest import android.annotation.SuppressLint import android.content.Context +import android.content.res.ColorStateList import android.location.Location import android.location.LocationListener import android.location.LocationManager @@ -15,16 +16,19 @@ import android.widget.ImageButton import android.widget.RelativeLayout import android.widget.Toast +import it.reyboz.bustorino.backend.FiveTNormalizer +import it.reyboz.bustorino.backend.gtfs.GtfsUtils import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager +import androidx.room.concurrent.AtomicBoolean import com.google.android.material.bottomsheet.BottomSheetBehavior import it.reyboz.bustorino.R -import it.reyboz.bustorino.backend.LivePositionsServiceStatus import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.mato.MQTTMatoClient @@ -32,7 +36,6 @@ import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.MapLibreStyles import it.reyboz.bustorino.util.Permissions -import it.reyboz.bustorino.viewmodels.LivePositionsViewModel import it.reyboz.bustorino.viewmodels.StopsMapViewModel import org.maplibre.android.camera.CameraPosition import org.maplibre.android.camera.CameraUpdateFactory @@ -60,8 +63,6 @@ private val stopsViewModel: StopsMapViewModel by viewModels() private var stopsShowing = ArrayList(0) - //private lateinit var symbolManager: SymbolManager - // Sources for stops and buses are in GeneralMapLibreFragment private var isUserMovingCamera = false @@ -78,10 +79,15 @@ private lateinit var showUserPositionButton: ImageButton private lateinit var centerUserButton: ImageButton private lateinit var followUserButton: ImageButton + private var followingUserLocation = false private var pendingLocationActivation = false private var ignoreCameraMovementForFollowing = true private var enablingPositionFromClick = false + private var restoredMapCamera = AtomicBoolean() + private var permissionsGranted = false + + //TODO: Rewrite this mess using LocationEngineProvider in MapLibre private val positionRequestLauncher = registerForActivityResult, Map>( ActivityResultContracts.RequestMultiplePermissions(), ActivityResultCallback { result -> if (result == null) { @@ -92,9 +98,9 @@ } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { // We can use the position, restart location overlay - Log.d(DEBUG_TAG, "HAVE THE PERMISSIONS") + permissionsGranted = true if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) - return@ActivityResultCallback ///@registerForActivityResult + return@ActivityResultCallback val locationManager = requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager var lastLoc = stopsViewModel.lastUserLocation @SuppressLint("MissingPermission") @@ -107,6 +113,7 @@ } else if (lastLoc != null) { + if(LatLng(lastLoc.latitude, lastLoc.longitude).distanceTo(DEFAULT_LATLNG) <= MAX_DIST_KM*1000){ Log.d(DEBUG_TAG, "Showing the user position") setMapLocationEnabled(true, true, false) @@ -138,10 +145,7 @@ //BUS POSITIONS private var usingMQTTPositions = true // THIS IS INSIDE VIEW MODEL NOW - private val livePositionsViewModel : LivePositionsViewModel by activityViewModels() - private lateinit var busPositionsIconButton: ImageButton - //private var busLabelSymbolsByVeh = HashMap() private val symbolsToUpdate = ArrayList() private var initialStopToShow : Stop? = null @@ -178,13 +182,8 @@ // Init the MapView mapView = rootView.findViewById(R.id.libreMapView) - val restoreBundle = stopsViewModel.savedState - if(restoreBundle!=null){ - mapView.onCreate(restoreBundle) - } else mapView.onCreate(savedInstanceState) - mapView.getMapAsync(this) //{ //map -> - //map.setStyle("https://demotiles.maplibre.org/style.json") } - + mapView.onCreate(savedInstanceState) + mapView.getMapAsync(this) //init bottom sheet val bottomSheet = rootView.findViewById(R.id.bottom_sheet) @@ -243,24 +242,16 @@ Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show() } + // PERMISSIONS REQUESTED AFTER MAP SETUP } // Setup close button rootView.findViewById(R.id.btnClose).setOnClickListener { - hideStopBottomSheet() - } - livePositionsViewModel.serviceStatus.observe(viewLifecycleOwner){ status -> - //if service is active, update the bus positions icon - when(status) { - LivePositionsServiceStatus.OK -> - setBusPositionsIcon(true, error = false) - - LivePositionsServiceStatus.NO_POSITIONS -> setBusPositionsIcon(true, error = true) - - else -> setBusPositionsIcon(true, error = true) - } + hideStopOrBusBottomSheet() } + observeStatusLivePositions() + //observe change in source of the live positions livePositionsViewModel.useMQTTPositionsLiveData.observe(viewLifecycleOwner){ useMQTT-> //Log.d(DEBUG_TAG, "Changed MQTT positions, now have to use MQTT: $useMQTT") if (showBusLayer && isResumed) { @@ -281,6 +272,7 @@ if (clearPos) { livePositionsViewModel.clearAllPositions() //force clear of the viewed data + if(vehShowing.isNotEmpty()) hideStopOrBusBottomSheet() clearAllBusPositionsInMap() } @@ -303,8 +295,6 @@ this.map = mapReady val context = requireContext() val mjson = MapLibreStyles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) - //ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json") - val builder = Style.Builder().fromJson(mjson!!) @@ -323,8 +313,6 @@ displayStops(stopsInCache) if(showBusLayer) setupBusLayer(style, withLabels = true, busIconsScale = 1.2f) - initSymbolManager(mapReady, style) - // Start observing data now that everything is set up observeStops() } @@ -350,9 +338,6 @@ //the user is moving the map isUserMovingCamera = true } - map?.let { setFollowingUser(it.locationComponent.cameraMode == CameraMode.TRACKING) } - //setFollowingUser() - } mapReady.addOnMapClickListener { point -> @@ -364,28 +349,39 @@ observeBusPositionUpdates() //Restoring data - var boundsRestored = false - pendingLocationActivation = true - stopsViewModel.savedState?.let{ - boundsRestored = restoreMapStateFromBundle(it) - //why are we disabling it? - pendingLocationActivation = it.getBoolean(KEY_LOCATION_ENABLED,true) - Log.d(DEBUG_TAG, "Restored map state from the saved bundle: ") - } - if(pendingLocationActivation) - positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) - - //reset saved State at the end - if((!boundsRestored)) { - //set initial position - //center position - val latlngTarget = initialStopToShow?.let { - LatLng(it.latitude!!, it.longitude!!) - } ?: LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) - mapReady.cameraPosition = CameraPosition.Builder().target(latlngTarget).zoom(DEFAULT_ZOOM).build() + + if (initialStopToShow!=null){ + val s = initialStopToShow!! + mapReady.cameraPosition = CameraPosition.Builder().target( + LatLng(s.latitude!!, s.longitude!!) + ).zoom(DEFAULT_ZOOM).build() + restoredMapCamera.set(true) + } else{ + var boundsRestored = false + //restore the map state here + map?.let{ + boundsRestored = mapStateViewModel.restoreMapState(it) + mapStateViewModel.lastOpenStopID.value?.let{ sID-> + val s= stopsViewModel.getStopByID(sID) + if (s==null) { + if(sID.isNotEmpty()) + Log.w(DEBUG_TAG,"Wanted to open stop $sID in map but it was not loaded!") + } + else{ + openStopInBottomSheet(s) } + } + + } + if(!boundsRestored){ + mapReady.cameraPosition = CameraPosition.Builder().target( + LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) + ).zoom(DEFAULT_ZOOM).build() + } + restoredMapCamera.set(boundsRestored) } - //reset saved state - stopsViewModel.savedState = null + + pendingLocationActivation = true + positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) } private fun onMapClickReact(point: LatLng): Boolean{ @@ -405,7 +401,7 @@ val sameStopClicked = shownStopInBottomSheet?.let { newstop.ID==it.ID } ?: false Log.d(DEBUG_TAG, "Hiding clicked stop: $sameStopClicked") if (isBottomSheetShowing()) { - hideStopBottomSheet() + hideStopOrBusBottomSheet() } if(!sameStopClicked){ openStopInBottomSheet(newstop) @@ -424,9 +420,14 @@ } else if (busNearby.isNotEmpty()) { val feature = busNearby[0] val vehid = feature.getStringProperty("veh") - val route = feature.getStringProperty("line") - - Toast.makeText(context, "Veh $vehid on route $route", Toast.LENGTH_SHORT).show() + if (isBottomSheetShowing()) hideStopOrBusBottomSheet() + showVehicleTripInBottomSheet(vehid) + //move camera to center on vehicle + updatesByVehDict[vehid]?.let { dat -> + mapReady.animateCamera( + CameraUpdateFactory.newLatLng(LatLng(dat.posUpdate.latitude, dat.posUpdate.longitude)), 750 + ) + } return true } } @@ -455,19 +456,12 @@ override fun onStart() { super.onStart() - //restore state from viewModel - stopsViewModel.savedState?.let { - restoreMapStateFromBundle(it) - //reset state - stopsViewModel.savedState = null - } } override fun onResume() { super.onResume() //mapView.onResume() handled in GeneralMapLibreFragment - //val keySourcePositions = getString(R.string.pref_positions_source) if(showBusLayer) { //first, clean up all the old positions livePositionsViewModel.clearOldPositionsUpdates() @@ -480,7 +474,7 @@ livePositionsViewModel.requestGTFSUpdates() usingMQTTPositions = false } - //mapViewModel.testCascade(); + livePositionsViewModel.isLastWorkResultGood.observe(this) { d: Boolean -> Log.d( DEBUG_TAG, "Last trip download result is $d" @@ -493,8 +487,6 @@ } fragmentListener?.readyGUIfor(FragmentKind.MAP) - //restore saved state - savedMapStateOnPause?.let { restoreMapStateFromBundle(it) } } override fun onPause() { @@ -502,7 +494,11 @@ mapView.onPause() Log.d(DEBUG_TAG, "Fragment paused") - savedMapStateOnPause = saveMapStateInBundle() + map?.let{ + //if map is initialized + mapStateViewModel.saveMapState(it) + } + mapStateViewModel.lastOpenStopID.postValue(shownStopInBottomSheet?.ID) if (livePositionsViewModel.useMQTTPositionsLiveData.value!!) livePositionsViewModel.stopMatoUpdates() } @@ -511,10 +507,11 @@ super.onStop() mapView.onStop() Log.d(DEBUG_TAG, "Fragment stopped!") - stopsViewModel.savedState = Bundle().let { + /* stopsViewModel.savedState = Bundle().let { mapView.onSaveInstanceState(it) it } + */ //save last location map?.locationComponent?.lastKnownLocation?.let{ stopsViewModel.lastUserLocation = it @@ -537,6 +534,17 @@ return mapView } + private fun showVehicleTripInBottomSheet(veh: String) { + val data = updatesByVehDict[veh] ?: return + super.showVehicleTripInBottomSheet(veh) { patternCode -> + map?.let { mapStateViewModel.saveMapState(it) } + fragmentListener?.openLineFromVehicle( + data.posUpdate.getLineGTFSFormat(), + patternCode, + mapStateViewModel.savedCameraState?.toBundle() + ) + } + } private fun observeStops() { // Observe stops stopsViewModel.stopsToShow.observe(viewLifecycleOwner) { stops -> @@ -606,7 +614,9 @@ DEBUG_TAG, "Have " + data.size + " trip updates, has Map start finished: " + mapInitCompleted ) - if (mapInitCompleted) updateBusPositionsInMap(data) + if (mapInitCompleted) updateBusPositionsInMap(data, hasVehicleTracking = true) { veh -> + showVehicleTripInBottomSheet(veh) + } if (!isDetached && !livePositionsViewModel.useMQTTPositionsLiveData.value!!) livePositionsViewModel.requestDelayedGTFSUpdates( 3000 ) @@ -614,30 +624,6 @@ } - /*private fun createLabelForVehicle(positionUpdate: LivePositionUpdate){ - val symOpt = SymbolOptions() - .withLatLng(LatLng(positionUpdate.latitude, positionUpdate.longitude)) - .withTextColor("#ffffff") - .withTextField(positionUpdate.routeID.substringBeforeLast('U')) - .withTextSize(13f) - .withTextAnchor(TEXT_ANCHOR_CENTER) - .withTextFont(arrayOf( "noto_sans_regular"))//"noto_sans_regular", "sans-serif")) //"noto_sans_regular")) - - val newSymbol = symbolManager.create(symOpt - ) - Log.d(DEBUG_TAG, "Symbol for veh ${positionUpdate.vehicle}: $newSymbol") - busLabelSymbolsByVeh[positionUpdate.vehicle] = newSymbol - } - private fun removeVehicleLabel(vehicle: String){ - busLabelSymbolsByVeh[vehicle]?.let { - symbolManager.delete(it) - busLabelSymbolsByVeh.remove(vehicle) - } - } - - */ - - // ------ LOCATION STUFF ----- @SuppressLint("MissingPermission") private fun requestInitialUserLocation() { @@ -672,19 +658,6 @@ } - /** - * Clear all buses from the map - */ - private fun clearAllBusPositionsInMap(){ - for ((k, anim) in animatorsByVeh){ - anim.cancel() - } - animatorsByVeh.clear() - updatesByVehDict.clear() - updatePositionsIcons(forced = false) - } - - /** * Handles logic of enabling the user location on the map @@ -697,7 +670,7 @@ if (permissionOk) { Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions, fromClick: $fromClick") locationComponent.isLocationComponentEnabled = true - if (initialStopToShow==null) { + if (!restoredMapCamera.get()) { locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING setFollowingUser(true) } @@ -711,7 +684,6 @@ Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show() } Log.d(DEBUG_TAG, "Requesting permission to show user location") - enablingPositionFromClick = fromClick showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) } } else{ @@ -727,6 +699,7 @@ } + private fun setLocationIconEnabled(enabled: Boolean){ if (enabled) showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) @@ -735,20 +708,6 @@ } - /** - * Helper method for GUI - */ - private fun setBusPositionsIcon(enabled: Boolean, error: Boolean){ - val ctx = requireContext() - if(!enabled) - busPositionsIconButton.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.bus_pos_circle_inactive)) - else if(error) - busPositionsIconButton.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.bus_pos_circle_notworking)) - else - busPositionsIconButton.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.bus_pos_circle_active)) - - } - private fun updateFollowingIcon(enabled: Boolean){ if(enabled) followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.walk_circle_active)) diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapCameraState.kt b/app/src/main/java/it/reyboz/bustorino/map/MapCameraState.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/MapCameraState.kt @@ -0,0 +1,41 @@ +package it.reyboz.bustorino.map + +import android.os.Bundle + + +data class MapCameraState( + val latitude: Double, + val longitude: Double, + val zoom: Double, + val bearing: Double, + val tilt: Double +){ + fun toBundle(): Bundle = Bundle().apply { + putDouble(KEY_LATITUDE, latitude) + putDouble(KEY_LONGITUDE, longitude) + putDouble(KEY_ZOOM, zoom) + putDouble(KEY_BEARING, bearing) + putDouble(KEY_TILT, tilt) + } + + companion object { + private const val KEY_LATITUDE = "cam-latitude" + private const val KEY_LONGITUDE = "cam-longitude" + private const val KEY_ZOOM = "cam-zoom" + private const val KEY_BEARING = "cam-bearing" + private const val KEY_TILT = "cam-tilt" + + fun fromBundle(bundle: Bundle): MapCameraState = MapCameraState( + latitude = bundle.getDouble(KEY_LATITUDE), + longitude = bundle.getDouble(KEY_LONGITUDE), + zoom = bundle.getDouble(KEY_ZOOM), + bearing = bundle.getDouble(KEY_BEARING), + tilt = bundle.getDouble(KEY_TILT) + ) + + fun checkInBundle(bundle: Bundle): Boolean { + val chck = bundle.containsKey(KEY_LATITUDE) && bundle.containsKey(KEY_LONGITUDE) && bundle.containsKey(KEY_ZOOM) + return chck + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt deleted file mode 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt +++ /dev/null @@ -1,18 +0,0 @@ -package it.reyboz.bustorino.map - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import it.reyboz.bustorino.backend.Stop - -class MapViewModel : ViewModel() { - - val currentLat = MutableLiveData(INVALID) - val currentLong = MutableLiveData(INVALID) - val currentZoom = MutableLiveData(-10.0) - - var stopShowing: Stop? = null - - companion object{ - const val INVALID = -1000.0 - } -} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt @@ -120,9 +120,6 @@ Log.d(DEBUG_TI, "Switched positions source in ViewModel, now using MQTT: ${!usingMQTT}") serviceStatus.value = LivePositionsServiceStatus.CONNECTING } - fun setGtfsLineToFilterPos(line: String, pattern: MatoPattern?){ - gtfsLineToFilterPos.value = Pair(line, pattern) - } var isLastWorkResultGood = workManager .getWorkInfosForUniqueWorkLiveData(MatoTripsDownloadWorker.TAG_TRIPS).map { it -> @@ -274,7 +271,7 @@ } filteredLocationUpdates.addSource(gtfsLineToFilterPos){ - //Log.d(DEBUG_TI, "line to filter change to: ${gtfsLineToFilterPos.value}") + Log.d(DEBUG_TI, "line to filter change to: ${gtfsLineToFilterPos.value}") updatesWithTripAndPatterns.value?.let{ ups-> filteredLocationUpdates.postValue(filterUpdatesForGtfsLine(ups, it)) //Log.d(DEBUG_TI, "Set ${ups.size} updates as new value for filteredLocation") @@ -282,6 +279,13 @@ } } + private fun clearFilteredPositions(){ + filteredLocationUpdates.postValue(Pair(HashMap(), ArrayList())) + } + fun setGtfsLineToFilterPos(line: String, pattern: MatoPattern?){ + clearFilteredPositions() + gtfsLineToFilterPos.value = Pair(line, pattern) + } private fun filterUpdatesForGtfsLine(updates: FullPositionUpdatesMap, linePatt: Pair): @@ -322,6 +326,7 @@ if (dir == directionId) { //add the trip updsForTripId[tripId] = pair + Log.d(DEBUG_TI, "Add vehicle ${pair.first.vehicle}, route ${pair.first.routeID}") } else { vehicleOnWrongDirection.add(vehicle) } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/MapStateViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/MapStateViewModel.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/MapStateViewModel.kt @@ -0,0 +1,48 @@ +package it.reyboz.bustorino.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import it.reyboz.bustorino.map.MapCameraState +import org.maplibre.android.camera.CameraPosition +import org.maplibre.android.geometry.LatLng +import org.maplibre.android.geometry.LatLngBounds +import org.maplibre.android.maps.MapLibreMap + +class MapStateViewModel : ViewModel() { + + var savedCameraState: MapCameraState? = null + private set + + val lastOpenStopID = MutableLiveData() + + fun saveMapState(map: MapLibreMap){ + val cp = map.cameraPosition + val newBbox = map.projection.visibleRegion.latLngBounds + + val cameraState = MapCameraState( + latitude = newBbox.center.latitude, + longitude = newBbox.center.longitude, + zoom = cp.zoom, + bearing = cp.bearing, + tilt = cp.tilt + ) + + savedCameraState = cameraState + } + fun restoreMapState(map: MapLibreMap): Boolean { + return restoreMapState(map, this.savedCameraState) + } + + companion object{ + fun restoreMapState(map: MapLibreMap, savedCameraState: MapCameraState?): Boolean { + val state = savedCameraState ?: return false + map.cameraPosition = CameraPosition.Builder() + .target(LatLng(state.latitude, state.longitude)) + .zoom(state.zoom) + .bearing(state.bearing) + .tilt(state.tilt) + .build() + return true + } + } +} \ 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 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt @@ -81,11 +81,9 @@ addStopsCallback) } } - - var savedState: Bundle? = null var lastUserLocation: Location? = null companion object{ private const val DEBUG_TAG = "BusTOStopMapViewModel" } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_lines_detail.xml b/app/src/main/res/layout/fragment_lines_detail.xml --- a/app/src/main/res/layout/fragment_lines_detail.xml +++ b/app/src/main/res/layout/fragment_lines_detail.xml @@ -98,27 +98,24 @@ android:src="@drawable/location_circlew_red" android:layout_marginTop="54dp" - android:layout_marginEnd="8dp" + android:layout_marginEnd="5dp" android:background="#00ffffff" android:contentDescription="@string/enable_position" app:layout_constraintTop_toTopOf="@id/lineMap" app:layout_constraintEnd_toEndOf="@id/lineMap" android:cropToPadding="true" /> - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map_libre.xml b/app/src/main/res/layout/fragment_map_libre.xml --- a/app/src/main/res/layout/fragment_map_libre.xml +++ b/app/src/main/res/layout/fragment_map_libre.xml @@ -15,7 +15,7 @@ /> - + + Fonte posizioni in tempo reale: Cambia fonte Rimuovi posizioni sulla mappa quando si cambia fonte delle posizioni in tempo reale + Aggiornato: %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -373,4 +373,6 @@ Switch source Clear bus positions when switching live positions source + Updated: %1$s + diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ ext.coroutines_version = "1.10.2" dependencies { classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.5' // or latest - classpath 'com.android.tools.build:gradle:8.12.3' + classpath 'com.android.tools.build:gradle:8.13.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-2.0.4" @@ -28,15 +28,15 @@ multidex_version = "2.0.1" //libraries versions fragment_version = "1.8.9" - activity_version = "1.11.0" + activity_version = "1.13.0" appcompat_version = "1.7.1" preference_version = "1.2.1" - work_version = "2.11.0" + work_version = "2.11.2" acra_version = "5.13.1" - lifecycle_version = "2.9.4" + lifecycle_version = "2.10.0" arch_version = "2.1.0" - room_version = "2.8.3" + room_version = "2.8.4" }