Page MenuHomeGitPull.it

D213.1773637980.diff
No OneTemporary

Size
101 KB
Referenced Files
None
Subscribers
None

D213.1773637980.diff

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
@@ -1,23 +1,59 @@
package it.reyboz.bustorino.fragments
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.content.Context
import android.content.SharedPreferences
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.location.Location
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.LinearInterpolator
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.cardview.widget.CardView
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.ViewCompat
+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.LivePositionTripPattern
import it.reyboz.bustorino.backend.Stop
+import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
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 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
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.OnMapReadyCallback
import org.maplibre.android.maps.Style
+import org.maplibre.android.plugins.annotation.Symbol
+import org.maplibre.android.plugins.annotation.SymbolManager
+import org.maplibre.android.plugins.annotation.SymbolOptions
+import org.maplibre.android.style.expressions.Expression
+import org.maplibre.android.style.layers.Property.ICON_ANCHOR_CENTER
+import org.maplibre.android.style.layers.Property.ICON_ROTATION_ALIGNMENT_MAP
+import org.maplibre.android.style.layers.Property.TEXT_ANCHOR_CENTER
+import org.maplibre.android.style.layers.Property.TEXT_ROTATION_ALIGNMENT_VIEWPORT
+import org.maplibre.android.style.layers.PropertyFactory
+import org.maplibre.android.style.layers.SymbolLayer
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
+import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback {
@@ -25,14 +61,20 @@
protected var shownStopInBottomSheet : Stop? = null
protected var savedMapStateOnPause : Bundle? = null
+
+ protected var fragmentListener: CommonFragmentListener? = null
+
// Declare a variable for MapView
protected lateinit var mapView: MapView
protected lateinit var mapStyle: Style
protected lateinit var stopsSource: GeoJsonSource
protected lateinit var busesSource: GeoJsonSource
protected lateinit var selectedStopSource: GeoJsonSource
+ protected lateinit var selectedBusSource: GeoJsonSource //= GeoJsonSource(SEL_BUS_SOURCE)
protected lateinit var sharedPreferences: SharedPreferences
+ protected lateinit var bottomSheetBehavior: BottomSheetBehavior<RelativeLayout>
+
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener(){ pref, key ->
/*when(key){
@@ -46,9 +88,36 @@
reloadMap()
}
}
+ //Bottom sheet behavior in GeneralMapLibreFragment
+ protected var bottomLayout: RelativeLayout? = null
+ protected lateinit var stopTitleTextView: TextView
+ protected lateinit var stopNumberTextView: TextView
+ protected lateinit var linesPassingTextView: TextView
+ protected lateinit var arrivalsCard: CardView
+ protected lateinit var directionsCard: CardView
+ protected lateinit var bottomrightImage: ImageView
+
+ protected lateinit var locationComponent: LocationComponent
+ protected var lastLocation : Location? = null
+
private var lastMapStyle =""
+ //BUS POSITIONS
+ protected val updatesByVehDict = HashMap<String, LivePositionTripPattern>(5)
+ protected val animatorsByVeh = HashMap<String, ValueAnimator>()
+ protected var vehShowing = ""
+ protected var lastUpdateTime:Long = -2
+
+ private val lifecycleOwnerLiveData = getViewLifecycleOwnerLiveData()
+
+
+ //extra items to use the LibreMap
+ protected lateinit var symbolManager : SymbolManager
+ protected var stopActiveSymbol: Symbol? = null
+ protected var stopsLayerStarted = false
+
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -66,6 +135,21 @@
return super.onCreateView(inflater, container, savedInstanceState)
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ //init bottom sheet
+ val bottomSheet = view.findViewById<RelativeLayout>(R.id.bottom_sheet)
+ bottomLayout = bottomSheet
+ stopTitleTextView = view.findViewById(R.id.stopTitleTextView)
+ stopNumberTextView = view.findViewById(R.id.stopNumberTextView)
+ linesPassingTextView = view.findViewById(R.id.linesPassingTextView)
+ arrivalsCard = view.findViewById(R.id.arrivalsCardButton)
+ directionsCard = view.findViewById(R.id.directionsCardButton)
+ bottomrightImage = view.findViewById(R.id.rightmostImageView)
+ bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
+ }
+
override fun onResume() {
mapView.onResume()
super.onResume()
@@ -93,6 +177,10 @@
super.onDestroy()
}
+ override fun onDestroyView() {
+ bottomLayout = null
+ super.onDestroyView()
+ }
protected fun reloadMap(){
/*map?.let {
@@ -111,11 +199,16 @@
//TODO figure out how to switch map safely
}
- abstract fun openStopInBottomSheet(stop: Stop)
-
//For extra stuff to do when the map is destroyed
abstract fun onMapDestroy()
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if(context is CommonFragmentListener){
+ fragmentListener = context
+ } else throw RuntimeException("$context must implement CommonFragmentListener")
+
+ }
protected fun restoreMapStateFromBundle(bundle: Bundle): Boolean{
val nullDouble = -10_000.0
var boundsRestored =false
@@ -194,6 +287,496 @@
}
+ protected fun removeVehiclesData(vehs: List<String>){
+ for(v in vehs){
+ if (updatesByVehDict.contains(v)) {
+ updatesByVehDict.remove(v)
+ if (animatorsByVeh.contains(v)){
+ animatorsByVeh[v]?.cancel()
+ animatorsByVeh.remove(v)
+ }
+ }
+ if (vehShowing==v){
+ hideStopBottomSheet()
+ }
+ }
+ }
+
+ // Hide the bottom sheet and remove extra symbol
+ protected fun hideStopBottomSheet(){
+ if (stopActiveSymbol!=null){
+ symbolManager.delete(stopActiveSymbol)
+ stopActiveSymbol = null
+ }
+ if(!showOpenStopWithSymbolLayer()){
+ selectedStopSource.setGeoJson(FeatureCollection.fromFeatures(ArrayList<Feature>()))
+ }
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+ //isBottomSheetShowing = false
+
+ //reset states
+ shownStopInBottomSheet = null
+ if (vehShowing!=""){
+ //we are hiding a vehicle
+ vehShowing = ""
+ updatePositionsIcons(true)
+ }
+
+ }
+
+ protected fun initSymbolManager(mapReady: MapLibreMap , style: Style){
+ symbolManager = SymbolManager(mapView,mapReady,style)
+ symbolManager.iconAllowOverlap = true
+ symbolManager.textAllowOverlap = false
+
+ symbolManager.addClickListener{ _ ->
+ if (stopActiveSymbol!=null){
+ hideStopBottomSheet()
+
+ return@addClickListener true
+ } else
+ return@addClickListener false
+ }
+
+ }
+
+ /**
+ * Initialize the map location, but do not enable the component
+ */
+ @SuppressLint("MissingPermission")
+ protected fun initMapUserLocation(style: Style, map: MapLibreMap, context: Context){
+ locationComponent = map.locationComponent
+ val locationComponentOptions =
+ LocationComponentOptions.builder(context)
+ .pulseEnabled(false)
+ .build()
+ val locationComponentActivationOptions =
+ MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context)
+ locationComponent.activateLocationComponent(locationComponentActivationOptions)
+ locationComponent.isLocationComponentEnabled = false
+
+ lastLocation?.let {
+ if (it.accuracy < 200)
+ locationComponent.forceLocationUpdate(it)
+ }
+ }
+
+
+ /**
+ * Update function for the bus positions
+ * Takes the processed updates and saves them accordingly
+ * Unified version that works with both fragments
+ *
+ * @param incomingData Map of updates with optional trip and pattern information
+ * @param checkCoordinateValidity If true, validates that coordinates are positive (default: false)
+ * @param hasVehicleTracking If true, checks if vehShowing is updated and calls callback (default: true)
+ * @param trackVehicleCallback Optional callback to show vehicle details when vehShowing is updated
+ */
+ protected fun updateBusPositionsInMap(
+ incomingData: HashMap<String, Pair<LivePositionUpdate,TripAndPatternWithStops?>>,
+ hasVehicleTracking: Boolean = false,
+ trackVehicleCallback: ((String) -> Unit)? = null
+ ) {
+ val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle })
+ val vehsOld = HashSet(updatesByVehDict.keys)
+
+ Log.d(DEBUG_TAG, "In fragment, have ${incomingData.size} updates to show")
+
+ var countUpds = 0
+ var createdVehs = 0
+
+ for (upsWithTrp in incomingData.values) {
+ val newPos = upsWithTrp.first
+ val patternStops = upsWithTrp.second
+ val vehID = newPos.vehicle
+
+ // Validate coordinates
+ if (!vehsOld.contains(vehID)) {
+ if (newPos.latitude <= 0 || newPos.longitude <= 0) {
+ Log.w(DEBUG_TAG, "Update ignored for veh $vehID on line ${newPos.routeID}, lat: ${newPos.latitude}, lon ${newPos.longitude}")
+ continue
+ }
+ }
+
+ if (vehsOld.contains(vehID)) {
+ // Changing the location of an existing bus
+ val oldPosData = updatesByVehDict[vehID]!!
+ val oldPos = oldPosData.posUpdate
+ val oldPattern = oldPosData.pattern
+
+ var avoidShowingUpdateBecauseIsImpossible = false
+
+ // Check for impossible route changes
+ if (oldPos.routeID != newPos.routeID) {
+ val dist = LatLng(oldPos.latitude, oldPos.longitude).distanceTo(
+ LatLng(newPos.latitude, newPos.longitude)
+ )
+ val speed = dist * 3.6 / (newPos.timestamp - oldPos.timestamp) // km/h
+ Log.w(DEBUG_TAG, "Vehicle $vehID changed route from ${oldPos.routeID} to ${newPos.routeID}, distance: $dist, speed: $speed")
+ if (speed > 120 || speed < 0) {
+ avoidShowingUpdateBecauseIsImpossible = true
+ }
+ }
+
+ if (avoidShowingUpdateBecauseIsImpossible) {
+ Log.w(DEBUG_TAG, "Update for vehicle $vehID skipped")
+ continue
+ }
+
+ // Check if position actually changed
+ val samePosition = (oldPos.latitude == newPos.latitude) &&
+ (oldPos.longitude == newPos.longitude)
+
+ val setPattern = (oldPattern == null) && (patternStops != null)
+
+ // Copy old bearing if new one is missing
+ if (newPos.bearing == null && oldPos.bearing != null) {
+ newPos.bearing = oldPos.bearing
+ }
+
+ if (!samePosition || setPattern) {
+ val newOrOldPosInBounds = isPointInsideVisibleRegion(
+ newPos.latitude, newPos.longitude, true
+ ) || isPointInsideVisibleRegion(oldPos.latitude, oldPos.longitude, true)
+
+ if (newOrOldPosInBounds) {
+ // Update pattern data if available
+ patternStops?.let {
+ updatesByVehDict[vehID]!!.pattern = it.pattern
+ }
+ // Animate the position change
+ animateNewPositionMove(newPos)
+ } else {
+ // Update position without animation
+ updatesByVehDict[vehID] = LivePositionTripPattern(
+ newPos,
+ patternStops?.pattern
+ )
+ }
+ }
+ countUpds++
+ } else {
+ // New vehicle - create entry
+ updatesByVehDict[vehID] = LivePositionTripPattern(
+ newPos,
+ patternStops?.pattern
+ )
+ createdVehs++
+ }
+
+ // Update vehicle details if this is the shown/tracked vehicle
+ if (hasVehicleTracking && vehShowing.isNotEmpty() && vehID == vehShowing) {
+ trackVehicleCallback?.invoke(vehID)
+ }
+ }
+
+ // Remove old positions
+ Log.d(DEBUG_TAG, "Updated $countUpds vehicles, created $createdVehs vehicles")
+ vehsOld.removeAll(vehsNew)
+
+ // Clean up stale vehicles (not updated for 2 minutes)
+ val currentTimeStamp = System.currentTimeMillis() / 1000
+ for (vehID in vehsOld) {
+ val posData = updatesByVehDict[vehID]!!
+ if (currentTimeStamp - posData.posUpdate.timestamp > 2 * 60) {
+ // Remove the bus
+ updatesByVehDict.remove(vehID)
+ // Cancel and remove animator if exists
+ animatorsByVeh[vehID]?.cancel()
+ animatorsByVeh.remove(vehID)
+ }
+ }
+
+ // Update UI
+ updatePositionsIcons(false)
+ }
+
+ /**
+ * Update the bus positions displayed on the map, from the existing data
+ *
+ * @param forced If true, forces immediate update ignoring the 60ms throttle
+ */
+ protected fun updatePositionsIcons(forced: Boolean) {
+ // Avoid frequent updates - throttle to max once per 60ms
+ val currentTime = System.currentTimeMillis()
+ if (!forced && currentTime - lastUpdateTime < 60) {
+ // Schedule delayed update
+ if(lifecycleOwnerLiveData.value != null)
+ viewLifecycleOwner.lifecycleScope.launch {
+ delay(200)
+ updatePositionsIcons(forced)
+ }
+ return
+ }
+
+ val busFeatures = ArrayList<Feature>()
+ val selectedBusFeatures = ArrayList<Feature>()
+
+ for (dat in updatesByVehDict.values) {
+ val pos = dat.posUpdate
+ val point = Point.fromLngLat(pos.longitude, pos.latitude)
+
+ val newFeature = Feature.fromGeometry(
+ point,
+ JsonObject().apply {
+ addProperty("veh", pos.vehicle)
+ addProperty("trip", pos.tripID)
+ addProperty("bearing", pos.bearing ?: 0.0f)
+ addProperty("line", pos.routeID.substringBeforeLast('U'))
+ }
+ )
+
+ // Separate selected vehicle from others
+ if (vehShowing.isNotEmpty() && vehShowing == dat.posUpdate.vehicle) {
+ selectedBusFeatures.add(newFeature)
+ } else {
+ busFeatures.add(newFeature)
+ }
+ }
+
+ busesSource.setGeoJson(FeatureCollection.fromFeatures(busFeatures))
+ selectedBusSource.setGeoJson(FeatureCollection.fromFeatures(selectedBusFeatures))
+
+ lastUpdateTime = System.currentTimeMillis()
+ }
+
+ /**
+ * Animates the transition of a vehicle from its current position to a new position
+ * This is the tricky part - we need to set the new positions with the data and redraw them all
+ *
+ * @param positionUpdate The new position update to animate to
+ */
+ protected fun animateNewPositionMove(positionUpdate: LivePositionUpdate) {
+ val vehID = positionUpdate.vehicle
+
+ // Check if vehicle exists in our tracking dictionary
+ if (vehID !in updatesByVehDict.keys) {
+ return
+ }
+
+ val currentUpdate = updatesByVehDict[vehID] ?: run {
+ Log.e(DEBUG_TAG, "Have to run animation for veh $vehID but not in the dict")
+ return
+ }
+
+ // Cancel any current animation for this vehicle
+ animatorsByVeh[vehID]?.cancel()
+
+ val posUp = currentUpdate.posUpdate
+ val currentPos = LatLng(posUp.latitude, posUp.longitude)
+ val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
+
+ // Create animator for smooth transition
+ val valueAnimator = ValueAnimator.ofObject(
+ MapLibreUtils.LatLngEvaluator(),
+ currentPos,
+ newPos
+ )
+
+ valueAnimator.addUpdateListener { animation ->
+ val latLng = animation.animatedValue as LatLng
+
+ // Update position during animation
+ updatesByVehDict[vehID]?.let { update ->
+ update.posUpdate.latitude = latLng.latitude
+ update.posUpdate.longitude = latLng.longitude
+ updatePositionsIcons(false)
+ } ?: run {
+ Log.w(DEBUG_TAG, "The bus position to animate has been removed, but the animator is still running!")
+ }
+ }
+
+ // Set the new position as current but keep old coordinates for animation start
+ positionUpdate.latitude = posUp.latitude
+ positionUpdate.longitude = posUp.longitude
+ updatesByVehDict[vehID]!!.posUpdate = positionUpdate
+
+ // Configure and start animation
+ valueAnimator.duration = 300
+ valueAnimator.interpolator = LinearInterpolator()
+ valueAnimator.start()
+
+ // Store animator for potential cancellation
+ animatorsByVeh[vehID] = valueAnimator
+ }
+
+ /// STOP OPENING
+ abstract fun showOpenStopWithSymbolLayer(): Boolean
+ /**
+ * Update the bottom sheet with the stop information
+ */
+ protected fun openStopInBottomSheet(stop: Stop){
+ bottomLayout?.let {
+
+ //lay.findViewById<TextView>(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
+ val stopName = stop.stopUserName ?: stop.stopDefaultName
+ stopTitleTextView.text = stopName//stop.stopDefaultName
+ stopNumberTextView.text = getString(R.string.stop_fill,stop.ID)
+ stopTitleTextView.visibility = View.VISIBLE
+
+ val string_show = if (stop.numRoutesStopping==0) ""
+ else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString())
+ linesPassingTextView.text = string_show
+
+ //SET ON CLICK LISTENER
+ arrivalsCard.setOnClickListener{
+ fragmentListener?.requestArrivalsForStopID(stop.ID)
+ }
+
+ arrivalsCard.visibility = View.VISIBLE
+ directionsCard.visibility = View.VISIBLE
+
+ directionsCard.setOnClickListener {
+ ViewUtils.openStopInOutsideApp(stop, context)
+ }
+ context?.let {
+ val colorIcon = ViewUtils.getColorFromTheme(it, android.R.attr.colorAccent)//ResourcesCompat.getColor(resources,R.attr.colorAccent,activity?.theme)
+ ViewCompat.setBackgroundTintList(directionsCard, ColorStateList.valueOf(colorIcon))
+ }
+
+ bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.navigation_right, activity?.theme))
+
+ }
+ //add stop marker
+ if (stop.latitude!=null && stop.longitude!=null) {
+ Log.d(DEBUG_TAG, "Showing stop: ${stop.ID}")
+
+ if (showOpenStopWithSymbolLayer()) {
+ stopActiveSymbol = symbolManager.create(
+ SymbolOptions()
+ .withLatLng(LatLng(stop.latitude!!, stop.longitude!!))
+ .withIconImage(STOP_ACTIVE_IMG)
+ .withIconAnchor(ICON_ANCHOR_CENTER)
+
+ )
+ } else{
+ val list = ArrayList<Feature>()
+ list.add(stopToGeoJsonFeature(stop))
+ selectedStopSource.setGeoJson(
+ FeatureCollection.fromFeatures(list)
+ )
+ }
+
+ }
+ Log.d(DEBUG_TAG, "Shown stop $stop in bottom sheet")
+ shownStopInBottomSheet = stop
+
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+
+
+
+ protected fun stopAnimations(){
+ for(anim in animatorsByVeh.values){
+ anim.cancel()
+ }
+ }
+
+ protected fun addImagesStyle(style: Style){
+ style.addImage(
+ STOP_IMAGE_ID,
+ ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!)
+
+ style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!)
+ style.addImage("ball",ResourcesCompat.getDrawable(resources, R.drawable.ball, activity?.theme)!!)
+ style.addImage(BUS_IMAGE_ID,ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!)
+ style.addImage(BUS_SEL_IMAGE_ID, ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon_sel, activity?.theme)!!)
+ val polyIconArrow = ResourcesCompat.getDrawable(resources, R.drawable.arrow_up_box_fill, activity?.theme)!!
+ style.addImage(POLY_ARROW, polyIconArrow)
+
+ }
+
+ protected fun initStopsLayer(style: Style, stopsFeatures: FeatureCollection?){
+ initStopsLayer(style, stopsFeatures,"symbol-transit-airfield" )
+ }
+
+ protected fun initStopsLayer(style: Style, stopsFeatures: FeatureCollection?, stopsLayerAbove: String){
+
+
+ stopsSource = GeoJsonSource(STOPS_SOURCE_ID,stopsFeatures ?: FeatureCollection.fromFeatures(ArrayList<Feature>()))
+ style.addSource(stopsSource)
+
+
+ // Stops layer
+ val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID)
+ stopsLayer.withProperties(
+ PropertyFactory.iconImage(STOP_IMAGE_ID),
+ PropertyFactory.iconAnchor(ICON_ANCHOR_CENTER),
+ PropertyFactory.iconAllowOverlap(true),
+ PropertyFactory.iconIgnorePlacement(true)
+ )
+
+ style.addLayerAbove(stopsLayer, stopsLayerAbove ) //"label_country_1") this with OSM Bright
+
+
+ selectedStopSource = GeoJsonSource(SEL_STOP_SOURCE, FeatureCollection.fromFeatures(ArrayList<Feature>()))
+ style.addSource(selectedStopSource)
+
+ val selStopLayer = SymbolLayer(SEL_STOP_LAYER, SEL_STOP_SOURCE)
+ selStopLayer.withProperties(
+ PropertyFactory.iconImage(STOP_ACTIVE_IMG),
+ PropertyFactory.iconAllowOverlap(true),
+ PropertyFactory.iconIgnorePlacement(true),
+ PropertyFactory.iconAnchor(ICON_ANCHOR_CENTER),
+
+ )
+ style.addLayerAbove(selStopLayer, STOPS_LAYER_ID)
+
+ stopsLayerStarted = true
+ }
+
+ /**
+ * Setup the Map Layers
+ */
+ protected fun setupBusLayer(style: Style, withLabels: Boolean =false, busIconsScale: Float = 1.0f) {
+ // Buses source
+ busesSource = GeoJsonSource(BUSES_SOURCE_ID)
+ style.addSource(busesSource)
+ //style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!)
+
+ selectedBusSource = GeoJsonSource(SEL_BUS_SOURCE)
+ style.addSource(selectedBusSource)
+
+ // Buses layer
+ val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply {
+ withProperties(
+ PropertyFactory.iconImage(BUS_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(busesLayer, STOPS_LAYER_ID)
+
+ val selectedBusLayer = SymbolLayer(SEL_BUS_LAYER, SEL_BUS_SOURCE).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)
+
+ )
+ style.addLayerAbove(selectedBusLayer, BUSES_LAYER_ID)
+
+ }
+
+ protected fun isBottomSheetShowing(): Boolean {
+ return bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED
+ }
+
companion object{
private const val DEBUG_TAG="GeneralMapLibreFragment"
@@ -204,6 +787,26 @@
const val SEL_STOP_SOURCE="selected-stop-source"
const val SEL_STOP_LAYER = "selected-stop-layer"
+ const val SEL_BUS_SOURCE = "sel_bus_source"
+ const val SEL_BUS_LAYER = "sel_bus_layer"
+
const val KEY_LOCATION_ENABLED="location_enabled"
+
+
+ protected const val STOPS_SOURCE_ID = "stops-source"
+ protected const val STOPS_LAYER_ID = "stops-layer"
+
+ protected const val STOP_IMAGE_ID = "stop-img"
+ protected const val STOP_ACTIVE_IMG = "stop_active_img"
+ protected const val BUS_IMAGE_ID = "bus_symbol"
+ protected const val BUS_SEL_IMAGE_ID = "sel_bus_symbol"
+
+ protected const val POLYLINE_LAYER = "polyline-layer"
+ protected const val POLYLINE_SOURCE = "polyline-source"
+
+ protected const val POLY_ARROWS_LAYER = "arrows-layer"
+ protected const val POLY_ARROWS_SOURCE = "arrows-source"
+ protected const val POLY_ARROW ="poly-arrow-img"
+
}
}
\ No newline at end of file
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
@@ -20,29 +20,24 @@
import android.Manifest
import android.animation.ObjectAnimator
-import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.ColorStateList
-import android.location.Location
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.view.animation.LinearInterpolator
import android.widget.*
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
-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 androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -53,40 +48,28 @@
import it.reyboz.bustorino.adapters.StopAdapterListener
import it.reyboz.bustorino.adapters.StopRecyclerAdapter
import it.reyboz.bustorino.backend.FiveTNormalizer
-import it.reyboz.bustorino.backend.LivePositionTripPattern
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.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops
-import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import it.reyboz.bustorino.map.*
import it.reyboz.bustorino.middleware.LocationUtils
import it.reyboz.bustorino.util.Permissions
-import it.reyboz.bustorino.util.ViewUtils
import it.reyboz.bustorino.viewmodels.LinesViewModel
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import kotlinx.coroutines.Runnable
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
-import org.maplibre.android.location.LocationComponent
-import org.maplibre.android.location.LocationComponentOptions
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
-import org.maplibre.android.plugins.annotation.Symbol
-import org.maplibre.android.plugins.annotation.SymbolManager
-import org.maplibre.android.plugins.annotation.SymbolOptions
import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.layers.LineLayer
import org.maplibre.android.style.layers.Property
-import org.maplibre.android.style.layers.Property.ICON_ANCHOR_CENTER
import org.maplibre.android.style.layers.Property.ICON_ROTATION_ALIGNMENT_MAP
import org.maplibre.android.style.layers.PropertyFactory
import org.maplibre.android.style.layers.SymbolLayer
@@ -104,15 +87,6 @@
private lateinit var patternsSpinner: Spinner
private var patternsAdapter: ArrayAdapter<String>? = null
- //Bottom sheet behavior
- private lateinit var bottomSheetBehavior: BottomSheetBehavior<RelativeLayout>
- private var bottomLayout: RelativeLayout? = null
- private lateinit var stopTitleTextView: TextView
- private lateinit var stopNumberTextView: TextView
- private lateinit var linesPassingTextView: TextView
- private lateinit var arrivalsCard: CardView
- private lateinit var directionsCard: CardView
- private lateinit var bottomrightImage: ImageView
//private var isBottomSheetShowing = false
private var shouldMapLocationBeReactivated = true
@@ -172,7 +146,7 @@
viewModel.shouldShowMessage=false
}
stop?.let {
- fragmentListener.requestArrivalsForStopID(it.ID)
+ fragmentListener?.requestArrivalsForStopID(it.ID)
}
if(stop == null){
Log.e(DEBUG_TAG,"Passed wrong stop")
@@ -197,23 +171,15 @@
//map data
//style and sources are in GeneralMapLibreFragment
- private lateinit var locationComponent: LocationComponent
private lateinit var polylineSource: GeoJsonSource
private lateinit var polyArrowSource: GeoJsonSource
- private lateinit var selectedBusSource: GeoJsonSource
private var savedCameraPosition: CameraPosition? = null
- private var vehShowing = ""
- private var stopsLayerStarted = false
private var lastStopsSizeShown = 0
- private var lastUpdateTime:Long = -2
//BUS POSITIONS
- private val updatesByVehDict = HashMap<String, LivePositionTripPattern>(5)
- private val animatorsByVeh = HashMap<String, ValueAnimator>()
- private var lastLocation : Location? = null
private var enablingPositionFromClick = false
private var polyline: LineString? = null
@@ -235,7 +201,6 @@
//private var stopPosList = ArrayList<GeoPoint>()
//fragment actions
- private lateinit var fragmentListener: CommonFragmentListener
private var showOnTopOfLine = false
private var recyclerInitDone = false
@@ -251,8 +216,6 @@
private val liveBusViewModel: LivePositionsViewModel by activityViewModels()
//extra items to use the LibreMap
- private lateinit var symbolManager : SymbolManager
- private var stopActiveSymbol: Symbol? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -289,17 +252,6 @@
mapView.getMapAsync(this)
- //init bottom sheet
- val bottomSheet = rootView.findViewById<RelativeLayout>(R.id.bottom_sheet)
- bottomLayout = bottomSheet
- stopTitleTextView = bottomSheet.findViewById(R.id.stopTitleTextView)
- stopNumberTextView = bottomSheet.findViewById(R.id.stopNumberTextView)
- linesPassingTextView = bottomSheet.findViewById(R.id.linesPassingTextView)
- arrivalsCard = bottomSheet.findViewById(R.id.arrivalsCardButton)
- directionsCard = bottomSheet.findViewById(R.id.directionsCardButton)
- bottomrightImage = bottomSheet.findViewById(R.id.rightmostImageView)
- bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
-
// Setup close button
rootView.findViewById<View>(R.id.btnClose).setOnClickListener {
hideStopBottomSheet()
@@ -525,6 +477,7 @@
val builder = Style.Builder().fromJson(mjson!!)
mapReady.setStyle(builder) { style ->
+ addImagesStyle(style)
mapStyle = style
//setupLayers(style)
@@ -533,22 +486,11 @@
initMapUserLocation(style, mapReady, requireContext())
//if(!stopsLayerStarted)
- initStopsPolyLineLayers(style, FeatureCollection.fromFeatures(ArrayList<Feature>()), null, null)
+ initPolylineStopsLayers(style, null)
setupBusLayer(style)
- symbolManager = SymbolManager(mapView,mapReady,style)
- symbolManager.iconAllowOverlap = true
- symbolManager.textAllowOverlap = false
-
- symbolManager.addClickListener{ _ ->
- if (stopActiveSymbol!=null){
- hideStopBottomSheet()
-
- return@addClickListener true
- } else
- return@addClickListener false
- }
+ initSymbolManager(mapReady, style)
mapViewModel.stopShowing?.let {
openStopInBottomSheet(it)
@@ -633,6 +575,10 @@
if(shouldMapLocationBeReactivated) setMapUserLocationEnabled(true, false, false)
}
+ override fun showOpenStopWithSymbolLayer(): Boolean {
+ return true
+ }
+
private fun observeBusPositionUpdates(){
//live bus positions
liveBusViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ pair ->
@@ -646,7 +592,9 @@
}
//remove vehicles not on this direction
removeVehiclesData(vehiclesNotOnCorrectDir)
- updateBusPositionsInMap(updates)
+ updateBusPositionsInMap(updates, hasVehicleTracking = true) { veh->
+ showVehicleTripInBottomSheet(veh)
+ }
//if not using MQTT positions
if(!useMQTTPositions){
liveBusViewModel.requestDelayedGTFSUpdates(2000)
@@ -663,100 +611,6 @@
}
}
- private fun isBottomSheetShowing(): Boolean{
- return bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED
- }
-
- /**
- * Initialize the map location, but do not enable the component
- */
- @SuppressLint("MissingPermission")
- private fun initMapUserLocation(style: Style, map: MapLibreMap, context: Context){
- locationComponent = map.locationComponent
- val locationComponentOptions =
- LocationComponentOptions.builder(context)
- .pulseEnabled(false)
- .build()
- val locationComponentActivationOptions =
- MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context)
- locationComponent.activateLocationComponent(locationComponentActivationOptions)
- locationComponent.isLocationComponentEnabled = false
-
- lastLocation?.let {
- if (it.accuracy < 200)
- locationComponent.forceLocationUpdate(it)
- }
- }
- /**
- * Update the bottom sheet with the stop information
- */
- override fun openStopInBottomSheet(stop: Stop){
- bottomLayout?.let {
-
- //lay.findViewById<TextView>(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
- val stopName = stop.stopUserName ?: stop.stopDefaultName
- stopTitleTextView.text = stopName//stop.stopDefaultName
- stopNumberTextView.text = getString(R.string.stop_fill,stop.ID)
- stopTitleTextView.visibility = View.VISIBLE
-
- val string_show = if (stop.numRoutesStopping==0) ""
- else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString())
- linesPassingTextView.text = string_show
-
- //SET ON CLICK LISTENER
- arrivalsCard.setOnClickListener{
- fragmentListener?.requestArrivalsForStopID(stop.ID)
- }
-
- arrivalsCard.visibility = View.VISIBLE
- directionsCard.visibility = View.VISIBLE
-
- directionsCard.setOnClickListener {
- ViewUtils.openStopInOutsideApp(stop, context)
- }
- context?.let {
- val colorIcon = ViewUtils.getColorFromTheme(it, android.R.attr.colorAccent)//ResourcesCompat.getColor(resources,R.attr.colorAccent,activity?.theme)
- ViewCompat.setBackgroundTintList(directionsCard, ColorStateList.valueOf(colorIcon))
- }
-
- bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.navigation_right, activity?.theme))
-
- }
- //add stop marker
- if (stop.latitude!=null && stop.longitude!=null) {
- stopActiveSymbol = symbolManager.create(
- SymbolOptions()
- .withLatLng(LatLng(stop.latitude!!, stop.longitude!!))
- .withIconImage(STOP_ACTIVE_IMG)
- .withIconAnchor(ICON_ANCHOR_CENTER)
-
- )
-
- }
- Log.d(DEBUG_TAG, "Shown stop $stop in bottom sheet")
- shownStopInBottomSheet = stop
-
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
- //isBottomSheetShowing = true
- }
- // Hide the bottom sheet and remove extra symbol
- private fun hideStopBottomSheet(){
- if (stopActiveSymbol!=null){
- symbolManager.delete(stopActiveSymbol)
- stopActiveSymbol = null
- }
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
- //isBottomSheetShowing = false
-
- //reset states
- shownStopInBottomSheet = null
- if (vehShowing!=""){
- //we are hiding a vehicle
- vehShowing = ""
- updatePositionsIcons(true)
- }
-
- }
private fun showVehicleTripInBottomSheet(veh: String){
val data = updatesByVehDict[veh]
@@ -818,11 +672,10 @@
/**
* Initialize the map layers for the stops
*/
- private fun initStopsPolyLineLayers(style: Style, stopFeatures:FeatureCollection, lineFeature: Feature?, arrowFeatures: FeatureCollection?){
+ private fun initPolylineStopsLayers(style: Style, arrowFeatures: FeatureCollection?){
Log.d(DEBUG_TAG, "INIT STOPS CALLED")
stopsSource = GeoJsonSource(STOPS_SOURCE_ID)
- style.addSource(stopsSource)
//val context = requireContext()
val stopIcon = ResourcesCompat.getDrawable(resources,R.drawable.ball, activity?.theme)!!
@@ -831,17 +684,11 @@
//set the image tint
//DrawableCompat.setTint(imgBus,ContextCompat.getColor(context,R.color.line_drawn_poly))
- // add icon
+ // add icons
style.addImage(STOP_IMAGE_ID,stopIcon)
style.addImage(POLY_ARROW, polyIconArrow)
style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!)
- // Stops layer
- val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID)
- stopsLayer.withProperties(
- PropertyFactory.iconImage(STOP_IMAGE_ID),
- PropertyFactory.iconAllowOverlap(true),
- PropertyFactory.iconIgnorePlacement(true)
- )
+
polylineSource = GeoJsonSource(POLYLINE_SOURCE) //lineFeature?.let { GeoJsonSource(POLYLINE_SOURCE, it) } ?: GeoJsonSource(POLYLINE_SOURCE)
style.addSource(polylineSource)
@@ -875,67 +722,14 @@
style.addLayerAbove(lineLayer,lastLayers[0].id)
else
style.addLayerBelow(lineLayer,"label_country_1")
- style.addLayerAbove(stopsLayer, POLYLINE_LAYER)
+ //style.addLayerAbove(stopsLayer, POLYLINE_LAYER)
style.addLayerAbove(arrowsLayer, POLYLINE_LAYER)
stopsLayerStarted = true
- }
-
-
- /**
- * Setup the Map Layers
- */
- private fun setupBusLayer(style: Style) {
- // Buses source
- busesSource = GeoJsonSource(BUSES_SOURCE_ID)
- style.addSource(busesSource)
- style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!)
-
- selectedBusSource = GeoJsonSource("sel_bus_source")
- style.addSource(selectedBusSource)
- style.addImage("sel_bus_symbol", ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon_sel, activity?.theme)!!)
-
- // Buses layer
- val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply {
- withProperties(
- PropertyFactory.iconImage("bus_symbol"),
- //PropertyFactory.iconSize(1.2f),
- PropertyFactory.iconAllowOverlap(true),
- PropertyFactory.iconIgnorePlacement(true),
- PropertyFactory.iconRotate(Expression.get("bearing")),
- PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP)
-
- )
- }
- style.addLayerAbove(busesLayer, STOPS_LAYER_ID)
-
- val selectedBusLayer = SymbolLayer("sel_bus_layer", "sel_bus_source").withProperties(
- PropertyFactory.iconImage("sel_bus_symbol"),
- //PropertyFactory.iconSize(1.2f),
- PropertyFactory.iconAllowOverlap(true),
- PropertyFactory.iconIgnorePlacement(true),
- PropertyFactory.iconRotate(Expression.get("bearing")),
- PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP)
-
- )
- style.addLayerAbove(selectedBusLayer, BUSES_LAYER_ID)
+ initStopsLayer(style, null, POLY_ARROWS_LAYER)
}
- override fun onAttach(context: Context) {
- super.onAttach(context)
- if(context is CommonFragmentListener){
- fragmentListener = context
- } else throw RuntimeException("$context must implement CommonFragmentListener")
-
- }
-
-
- private fun stopAnimations(){
- for(anim in animatorsByVeh.values){
- anim.cancel()
- }
- }
/**
* Save the loaded pattern data, without the stops!
@@ -1125,7 +919,7 @@
} else
map?.let {
Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer")
- initStopsPolyLineLayers(mapStyle, FeatureCollection.fromFeatures(features),lineFeature, FeatureCollection.fromFeatures(arrowFeatures))
+ initPolylineStopsLayers(mapStyle, FeatureCollection.fromFeatures(arrowFeatures))
Log.d(DEBUG_TAG,"Started stops layer on map")
lastStopsSizeShown = features.size
stopsLayerStarted = true
@@ -1198,260 +992,6 @@
}
}
- private fun removeVehiclesData(vehs: List<String>){
- for(v in vehs){
- if (updatesByVehDict.contains(v)) {
- updatesByVehDict.remove(v)
- if (animatorsByVeh.contains(v)){
- animatorsByVeh[v]?.cancel()
- animatorsByVeh.remove(v)
- }
- }
- if (vehShowing==v){
- hideStopBottomSheet()
- }
- }
- }
-
- /**
- * Update function for the bus positions
- * Takes the processed updates and saves them accordingly
- * Copied from MapLibreFragment, removing the labels
- */
- private fun updateBusPositionsInMap(incomingData: HashMap<String, Pair<LivePositionUpdate,TripAndPatternWithStops?>>){
- val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle })
- val vehsOld = HashSet(updatesByVehDict.keys)
- Log.d(DEBUG_TAG, "In fragment, have ${incomingData.size} updates to show")
-
- var countUpds = 0
- var createdVehs = 0
- //val symbolsToUpdate = ArrayList<Symbol>()
- for (upsWithTrp in incomingData.values){
- val newPos = upsWithTrp.first
- val patternStops = upsWithTrp.second
- val vehID = newPos.vehicle
- var animate = false
- if (vehsOld.contains(vehID)){
- //changing the location of an existing bus
- //update position only if the starting or the stopping position of the animation are in the view
- val oldPos = updatesByVehDict[vehID]?.posUpdate
- val oldPattern = updatesByVehDict[vehID]?.pattern
- var avoidShowingUpdateBecauseIsImpossible = false
- oldPos?.let{
-
- if(it.routeID!=newPos.routeID) {
- val dist = LatLng(it.latitude, it.longitude).distanceTo(LatLng(newPos.latitude, newPos.longitude))
- val speed = dist*3.6 / (newPos.timestamp - it.timestamp) //this should be in km/h
- Log.w(DEBUG_TAG, "Vehicle $vehID changed route from ${oldPos.routeID} to ${newPos.routeID}, distance: $dist, speed: $speed")
- if (speed > 120 || speed < 0){
- avoidShowingUpdateBecauseIsImpossible = true
- }
- }
- }
- if (avoidShowingUpdateBecauseIsImpossible){
- // DO NOT SHOW THIS SHIT
- Log.w(DEBUG_TAG, "Update for vehicle $vehID skipped")
- continue
- }
-
- val samePosition = oldPos?.let { (it.latitude==newPos.latitude)&&(it.longitude == newPos.longitude) }?:false
- val setPattern = (oldPattern==null) && (patternStops!=null)
- if(newPos.bearing == null && oldPos?.bearing != null){
- //copy old bearing
- newPos.bearing = oldPos.bearing
- }
- if((!samePosition)|| setPattern) {
-
- val newOrOldPosInBounds = isPointInsideVisibleRegion(
- newPos.latitude, newPos.longitude, true
- ) || (oldPos?.let { isPointInsideVisibleRegion(it.latitude,it.longitude,true) } ?: false)
-
-
- //val skip = true
- if (newOrOldPosInBounds) {
- // update the pattern data, the position will be updated with the animation
- patternStops?.let { updatesByVehDict[vehID]!!.pattern = it.pattern}
- //this moves both the icon and the label
- animateNewPositionMove(newPos)
-
- } else {
- //update
- updatesByVehDict[vehID] = LivePositionTripPattern(newPos,patternStops?.pattern)
- /*busLabelSymbolsByVeh[vehID]?.let {
- it.latLng = LatLng(pos.latitude, pos.longitude)
- symbolsToUpdate.add(it)
- }*/
- //if(vehShowing==vehID)
- // map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500)
- //TODO: Follow the vehicle
- }
- }
- countUpds++
- }
- else{
- //not inside
- // update it simply
- updatesByVehDict[vehID] = LivePositionTripPattern(newPos, patternStops?.pattern)
- //createLabelForVehicle(pos)
- //if(vehShowing==vehID)
- // map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500)
- createdVehs +=1
- }
- if (vehID == vehShowing){
- //update the data
- showVehicleTripInBottomSheet(vehID)
- }
- }
- //symbolManager.update(symbolsToUpdate)
- //remove old positions
- Log.d(DEBUG_TAG, "Updated $countUpds vehicles, created $createdVehs vehicles")
- vehsOld.removeAll(vehsNew)
- //now vehsOld contains the vehicles id for those that have NOT been updated
- val currentTimeStamp = System.currentTimeMillis() /1000
- for(vehID in vehsOld){
- //remove after 2 minutes of inactivity
- if (updatesByVehDict[vehID]!!.posUpdate.timestamp - currentTimeStamp > 2*60){
- //remove the bus
- updatesByVehDict.remove(vehID)
- if(vehID in animatorsByVeh){
- animatorsByVeh[vehID]?.cancel()
- animatorsByVeh.remove(vehID)
- }
- //removeVehicleLabel(vehID)
- }
- }
- //update UI
- updatePositionsIcons(false)
- }
-
- /**
- * This is the tricky part, animating the transitions
- * Basically, we need to set the new positions with the data and redraw them all
- */
- private fun animateNewPositionMove(positionUpdate: LivePositionUpdate){
- if (positionUpdate.vehicle !in updatesByVehDict.keys)
- return
- val vehID = positionUpdate.vehicle
- val currentUpdate = updatesByVehDict[positionUpdate.vehicle]
- currentUpdate?.let { it ->
- //cancel current animation on vehicle
- animatorsByVeh[vehID]?.cancel()
- val posUp = it.posUpdate
-
- val currentPos = LatLng(posUp.latitude, posUp.longitude)
- val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
- val valueAnimator = ValueAnimator.ofObject(MapLibreUtils.LatLngEvaluator(), currentPos, newPos)
- valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
- private var latLng: LatLng? = null
- override fun onAnimationUpdate(animation: ValueAnimator) {
- latLng = animation.animatedValue as LatLng
- //update position on animation
- val update = updatesByVehDict[positionUpdate.vehicle]
- if(update!=null){ latLng?.let { ll ->
- update.posUpdate.latitude = ll.latitude
- update.posUpdate.longitude = ll.longitude
- updatePositionsIcons(false)
- }
- } else{
- //The update is null
- Log.w(DEBUG_TAG, "The bus position to animate has been removed, but the animator is still running!")
- }
- }
- })
- /*valueAnimator.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationStart(animation: Animator) {
- super.onAnimationStart(animation)
- //val update = positionsByVehDict[positionUpdate.vehicle]!!
- //remove the label at the start of the animation
- //removeVehicleLabel(vehID)
- val annot = busLabelSymbolsByVeh[vehID]
- annot?.let { sym ->
- sym.textOpacity = 0.0f
- symbolsToUpdate.add(sym)
- }
- }
-
- override fun onAnimationEnd(animation: Animator) {
- super.onAnimationEnd(animation)
- /*val annot = busLabelSymbolsByVeh[vehID]
- annot?.let { sym ->
- sym.textOpacity = 1.0f
- sym.latLng = newPos //LatLng(newPos)
- symbolsToUpdate.add(sym)
- }
-
- */
- }
- })
- */
- animatorsByVeh[vehID]?.cancel()
- //set the new position as the current one but with the old lat and lng
- positionUpdate.latitude = posUp.latitude
- positionUpdate.longitude = posUp.longitude
- //this might be null if the updates dict does not contain the vehID
- updatesByVehDict[vehID]!!.posUpdate = positionUpdate
- valueAnimator.duration = 300
- valueAnimator.interpolator = LinearInterpolator()
- valueAnimator.start()
-
- animatorsByVeh[vehID] = valueAnimator
-
- } ?: {
- Log.e(DEBUG_TAG, "Have to run animation for veh ${positionUpdate.vehicle} but not in the dict, adding")
- //updatesByVehDict[positionUpdate.vehicle] = positionUpdate
- }
- }
- //TODO: MERGE THIS CODE WITH MapLibreFragment ONE
- /**
- * Update the bus positions displayed on the map, from the existing data
- */
- private fun updatePositionsIcons(forced: Boolean){
- //avoid frequent updates
- val currentTime = System.currentTimeMillis()
- if(!forced && currentTime - lastUpdateTime < 60){
- //DO NOT UPDATE THE MAP
- viewLifecycleOwner.lifecycleScope.launch {
- delay(200)
- updatePositionsIcons(forced)
- }
- return
- }
-
- val busFeatures = ArrayList<Feature>()
- val selectedBusFeatures = ArrayList<Feature>()
- for (dat in updatesByVehDict.values){
- //if (s.latitude!=null && s.longitude!=null)
- val pos = dat.posUpdate
- val point = Point.fromLngLat(pos.longitude, pos.latitude)
-
- val newFeature = Feature.fromGeometry(
- point,
- JsonObject().apply {
- addProperty("veh", pos.vehicle)
- addProperty("trip", pos.tripID)
- addProperty("bearing", pos.bearing ?:0.0f)
- addProperty("line", pos.routeID)
- }
- )
- if (vehShowing == dat.posUpdate.vehicle)
- selectedBusFeatures.add(newFeature)
- else
- busFeatures.add(newFeature)
- /*busLabelSymbolsByVeh[pos.vehicle]?.let {
- it.latLng = LatLng(pos.latitude, pos.longitude)
- symbolsToUpdate.add(it)
- }
-
- */
- }
- busesSource.setGeoJson(FeatureCollection.fromFeatures(busFeatures))
- selectedBusSource.setGeoJson(FeatureCollection.fromFeatures(selectedBusFeatures))
- //update labels, clear cache to be used
- //symbolManager.update(symbolsToUpdate)
- //symbolsToUpdate.clear()
- lastUpdateTime = System.currentTimeMillis()
- }
-
override fun onResume() {
super.onResume()
@@ -1485,7 +1025,7 @@
*/
}
//initialize GUI here
- fragmentListener.readyGUIfor(FragmentKind.LINES)
+ fragmentListener?.readyGUIfor(FragmentKind.LINES)
}
@@ -1553,16 +1093,7 @@
companion object {
private const val LINEID_KEY="lineID"
private const val STOPID_FROM_KEY="stopID"
- private const val STOPS_SOURCE_ID = "stops-source"
- private const val STOPS_LAYER_ID = "stops-layer"
- private const val STOP_ACTIVE_IMG = "stop_active_img"
- private const val STOP_IMAGE_ID = "stop-img"
- private const val POLYLINE_LAYER = "polyline-layer"
- private const val POLYLINE_SOURCE = "polyline-source"
-
- private const val POLY_ARROWS_LAYER = "arrows-layer"
- private const val POLY_ARROWS_SOURCE = "arrows-source"
- private const val POLY_ARROW ="poly-arrow-img"
+
private const val DEBUG_TAG="BusTO-LineDetalFragment"
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
@@ -2,12 +2,8 @@
import android.Manifest
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
-import android.graphics.Color
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
@@ -16,23 +12,17 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.view.animation.LinearInterpolator
import android.widget.ImageButton
import android.widget.RelativeLayout
-import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
-import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.gson.JsonObject
import it.reyboz.bustorino.R
import it.reyboz.bustorino.backend.LivePositionsServiceStatus
import it.reyboz.bustorino.backend.Stop
@@ -40,32 +30,20 @@
import it.reyboz.bustorino.backend.mato.MQTTMatoClient
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
-import it.reyboz.bustorino.map.MapLibreUtils
import it.reyboz.bustorino.map.MapLibreStyles
import it.reyboz.bustorino.util.Permissions
-import it.reyboz.bustorino.util.ViewUtils
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import it.reyboz.bustorino.viewmodels.StopsMapViewModel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
-import org.maplibre.android.location.LocationComponent
-import org.maplibre.android.location.LocationComponentOptions
import org.maplibre.android.location.modes.CameraMode
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.plugins.annotation.Symbol
-import org.maplibre.android.style.expressions.Expression
-import org.maplibre.android.style.layers.Property.*
-import org.maplibre.android.style.layers.PropertyFactory
-import org.maplibre.android.style.layers.SymbolLayer
-import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
-import org.maplibre.geojson.Point
// TODO: Rename parameter arguments, choose names that match
@@ -80,32 +58,19 @@
class MapLibreFragment : GeneralMapLibreFragment() {
- protected var fragmentListener: CommonFragmentListener? = null
- private lateinit var locationComponent: LocationComponent
- private var lastLocation: Location? = null
private val stopsViewModel: StopsMapViewModel by viewModels()
private var stopsShowing = ArrayList<Stop>(0)
- private var isBottomSheetShowing = false
//private lateinit var symbolManager: SymbolManager
// Sources for stops and buses are in GeneralMapLibreFragment
private var isUserMovingCamera = false
- private var stopsLayerStarted = false
private var lastStopsSizeShown = 0
private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0)
private var mapInitCompleted =false
private var stopsRedrawnTimes = 0
- //bottom Sheet behavior
- private lateinit var bottomSheetBehavior: BottomSheetBehavior<RelativeLayout>
- private var bottomLayout: RelativeLayout? = null
- private lateinit var stopTitleTextView: TextView
- private lateinit var stopNumberTextView: TextView
- private lateinit var linesPassingTextView: TextView
- private lateinit var arrivalsCard: CardView
- private lateinit var directionsCard: CardView
-
+ //bottom Sheet behavior in GeneralMapLibreFragment
//private var stopActiveSymbol: Symbol? = null
// Location stuff
@@ -171,9 +136,6 @@
private val livePositionsViewModel : LivePositionsViewModel by activityViewModels()
private lateinit var busPositionsIconButton: ImageButton
- private val positionsByVehDict = HashMap<String, LivePositionUpdate>(5)
- private val animatorsByVeh = HashMap<String, ValueAnimator>()
- private var lastUpdateTime : Long = -1
//private var busLabelSymbolsByVeh = HashMap<String,Symbol>()
private val symbolsToUpdate = ArrayList<Symbol>()
@@ -295,7 +257,9 @@
}
}
livePositionsViewModel.useMQTTPositionsLiveData.observe(viewLifecycleOwner){ useMQTT->
+ //Log.d(DEBUG_TAG, "Changed MQTT positions, now have to use MQTT: $useMQTT")
if (showBusLayer && 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)
@@ -308,6 +272,7 @@
livePositionsViewModel.stopMatoUpdates()
livePositionsViewModel.requestGTFSUpdates()
}
+ Log.d(DEBUG_TAG, "Should clear positions: $clearPos")
if (clearPos) {
livePositionsViewModel.clearAllPositions()
//force clear of the viewed data
@@ -342,15 +307,18 @@
mapStyle = style
//setupLayers(style)
+ addImagesStyle(style)
- initMapLocation(style, mapReady, requireContext())
+ initMapUserLocation(style, mapReady, requireContext())
//init stop layer with this
val stopsInCache = stopsViewModel.getAllStopsLoaded()
if(stopsInCache.isEmpty())
- initStopsLayer(style, FeatureCollection.fromFeatures(ArrayList<Feature>()))
+ initStopsLayer(style, null)
else
displayStops(stopsInCache)
- if(showBusLayer) setupBusLayer(style)
+ if(showBusLayer) setupBusLayer(style, withLabels = true, busIconsScale = 1.2f)
+
+ initSymbolManager(mapReady, style)
// Start observing data now that everything is set up
observeStops()
@@ -418,17 +386,20 @@
private fun onMapClickReact(point: LatLng): Boolean{
map?.let { mapReady ->
val screenPoint = mapReady.projection.toScreenLocation(point)
- val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID)
+ val stopsFeatures = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID)
val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_LAYER_ID)
- if (features.isNotEmpty()) {
- val feature = features[0]
+ Log.d(DEBUG_TAG, "Clicked on stops: $stopsFeatures \n and buses: $busNearby")
+ if (stopsFeatures.isNotEmpty()) {
+ val feature = stopsFeatures[0]
val id = feature.getStringProperty("id")
val name = feature.getStringProperty("name")
//Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show()
val stop = stopsViewModel.getStopByID(id)
+ Log.d(DEBUG_TAG, "Decided click is on stop with id $id : $stop")
stop?.let { newstop ->
val sameStopClicked = shownStopInBottomSheet?.let { newstop.ID==it.ID } ?: false
- if (isBottomSheetShowing) {
+ Log.d(DEBUG_TAG, "Hiding clicked stop: $sameStopClicked")
+ if (isBottomSheetShowing()) {
hideStopBottomSheet()
}
if(!sameStopClicked){
@@ -457,147 +428,10 @@
return false
}
-
- private fun initStopsLayer(style: Style, features:FeatureCollection){
-
- stopsSource = GeoJsonSource(STOPS_SOURCE_ID,features)
- style.addSource(stopsSource)
-
- // add icon
- style.addImage(STOP_IMAGE_ID,
- ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!)
-
- style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!)
- style.addImage("ball",ResourcesCompat.getDrawable(resources, R.drawable.ball, activity?.theme)!!)
- // Stops layer
- val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID)
- stopsLayer.withProperties(
- PropertyFactory.iconImage(STOP_IMAGE_ID),
- PropertyFactory.iconAnchor(ICON_ANCHOR_CENTER),
- PropertyFactory.iconAllowOverlap(true),
- PropertyFactory.iconIgnorePlacement(true)
- )
-
- style.addLayerBelow(stopsLayer, "symbol-transit-airfield") //"label_country_1") this with OSM Bright
-
-
- selectedStopSource = GeoJsonSource(SEL_STOP_SOURCE, FeatureCollection.fromFeatures(ArrayList<Feature>()))
- style.addSource(selectedStopSource)
-
- val selStopLayer = SymbolLayer(SEL_STOP_LAYER, SEL_STOP_SOURCE)
- selStopLayer.withProperties(
- PropertyFactory.iconImage(STOP_ACTIVE_IMG),
- PropertyFactory.iconAllowOverlap(true),
- PropertyFactory.iconIgnorePlacement(true),
- PropertyFactory.iconAnchor(ICON_ANCHOR_CENTER),
-
- )
- style.addLayerAbove(selStopLayer, STOPS_LAYER_ID)
-
- stopsLayerStarted = true
- }
-
- /**
- * Setup the Map Layers
- */
- private fun setupBusLayer(style: Style) {
- // Buses source
- busesSource = GeoJsonSource(BUSES_SOURCE_ID)
- style.addSource(busesSource)
- style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!)
-
- // Buses layer
- val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply {
- withProperties(
- PropertyFactory.iconImage("bus_symbol"),
- PropertyFactory.iconSize(1.2f),
- PropertyFactory.iconAllowOverlap(true),
- PropertyFactory.iconIgnorePlacement(true),
- PropertyFactory.iconRotate(Expression.get("bearing")),
- PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP),
-
- 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(busesLayer, STOPS_LAYER_ID)
-
- //Line names layer
- /*vehiclesLabelsSource = GeoJsonSource(LABELS_SOURCE)
- style.addSource(vehiclesLabelsSource)
- val textLayer = SymbolLayer(LABELS_LAYER_ID, LABELS_SOURCE).apply {
- withProperties(
- PropertyFactory.textField("label"),
- PropertyFactory.textSize(30f),
- //PropertyFactory.textHaloColor(Color.BLACK),
- //PropertyFactory.textHaloWidth(1f),
-
- 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)
-
-
- )
- }
- style.addLayerAbove(textLayer, BUSES_LAYER_ID)
-
- */
-
+ override fun showOpenStopWithSymbolLayer(): Boolean {
+ return false
}
- /**
- * Update the bottom sheet with the stop information
- */
- override fun openStopInBottomSheet(stop: Stop){
- bottomLayout?.let {
-
- //lay.findViewById<TextView>(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
- val stopName = stop.stopUserName ?: stop.stopDefaultName
- stopTitleTextView.text = stopName//stop.stopDefaultName
- stopNumberTextView.text = getString(R.string.stop_fill,stop.ID)
- val string_show = if (stop.numRoutesStopping==0) ""
- else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString())
- linesPassingTextView.text = string_show
-
- //SET ON CLICK LISTENER
- arrivalsCard.setOnClickListener{
- fragmentListener?.requestArrivalsForStopID(stop.ID)
- }
-
- directionsCard.setOnClickListener {
- ViewUtils.openStopInOutsideApp(stop, context)
- }
-
-
- }
- //add stop marker
- if (stop.latitude!=null && stop.longitude!=null) {
- /*stopActiveSymbol = symbolManager.create(
- SymbolOptions()
- .withLatLng(LatLng(stop.latitude!!, stop.longitude!!))
- .withIconImage(STOP_ACTIVE_IMG)
- .withIconAnchor(ICON_ANCHOR_CENTER)
- //.withTextFont(arrayOf("noto_sans_regular")))
- */
- Log.d(DEBUG_TAG, "Showing stop: ${stop.ID}")
- val list = ArrayList<Feature>()
- list.add(stopToGeoJsonFeature(stop))
- selectedStopSource.setGeoJson(
- FeatureCollection.fromFeatures(list)
- )
- }
- shownStopInBottomSheet = stop
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
- isBottomSheetShowing = true
- }
override fun onAttach(context: Context) {
super.onAttach(context)
fragmentListener = if (context is CommonFragmentListener) {
@@ -756,25 +590,7 @@
stopsLayerStarted = true
}
}
- // Hide the bottom sheet and remove extra symbol
- private fun hideStopBottomSheet(){
- /*if (stopActiveSymbol!=null){
- symbolManager.delete(stopActiveSymbol)
- stopActiveSymbol = null
- }
- */
- //empty the source
- selectedStopSource.setGeoJson(FeatureCollection.fromFeatures(ArrayList<Feature>()))
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
- //remove initial stop
- if(initialStopToShow!=null){
- initialStopToShow = null
- }
- //set showing
- isBottomSheetShowing = false
- shownStopInBottomSheet = null
- }
// --------------- BUS LOCATIONS STUFF --------------------------
/**
* Start requesting position updates
@@ -791,15 +607,7 @@
)
}
}
- private fun isInsideVisibleRegion(latitude: Double, longitude: Double, nullValue: Boolean): Boolean{
- var isInside = nullValue
- val visibleRegion = map?.projection?.visibleRegion
- visibleRegion?.let {
- val bounds = it.latLngBounds
- isInside = bounds.contains(LatLng(latitude, longitude))
- }
- return isInside
- }
+
/*private fun createLabelForVehicle(positionUpdate: LivePositionUpdate){
val symOpt = SymbolOptions()
@@ -824,214 +632,6 @@
*/
- /**
- * Update function for the bus positions
- * Takes the processed updates and saves them accordingly
- */
- private fun updateBusPositionsInMap(incomingData: HashMap<String, Pair<LivePositionUpdate,TripAndPatternWithStops?>>){
- val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle })
- val vehsOld = HashSet(positionsByVehDict.keys)
-
- val symbolsToUpdate = ArrayList<Symbol>()
- for (upsWithTrp in incomingData.values){
- val newPos = upsWithTrp.first
- val vehID = newPos.vehicle
- //var animate = false
- if (vehsOld.contains(vehID)){
- //update position only if the starting or the stopping position of the animation are in the view
- val oldPos = positionsByVehDict[vehID]
- var avoidShowingUpdateBecauseIsImpossible = false
- oldPos?.let{
- if(oldPos.routeID!=newPos.routeID) {
- val dist = LatLng(it.latitude, it.longitude).distanceTo(LatLng(newPos.latitude, newPos.longitude))
- val speed = dist*3.6 / (newPos.timestamp - it.timestamp) //this should be in km/h
- Log.w(DEBUG_TAG, "Vehicle $vehID changed route from ${oldPos.routeID} to ${newPos.routeID}, distance: $dist, speed: $speed")
- if (speed > 120 || speed < 0){
- avoidShowingUpdateBecauseIsImpossible = true
- }
- }
- }
- if (avoidShowingUpdateBecauseIsImpossible){
- // DO NOT SHOW THIS SHIT
- Log.w(DEBUG_TAG, "Update for vehicle $vehID skipped")
- continue
- }
-
- val samePosition = oldPos?.let { (oldPos.latitude==newPos.latitude)&&(oldPos.longitude == newPos.longitude) }?:false
-
- if(!samePosition) {
- val isPositionInBounds = isInsideVisibleRegion(
- newPos.latitude, newPos.longitude, false
- ) || (oldPos?.let { isInsideVisibleRegion(it.latitude,it.longitude, false) } ?: false)
- if ((newPos.bearing==null && oldPos?.bearing!=null)){
- //copy old bearing
- newPos.bearing = oldPos.bearing
- }
- if (isPositionInBounds) {
- //animate = true
- //this moves both the icon and the label
- moveVehicleToNewPosition(newPos)
- } else {
-
- positionsByVehDict[vehID] = newPos
- /*busLabelSymbolsByVeh[vehID]?.let {
- it.latLng = LatLng(pos.latitude, pos.longitude)
- symbolsToUpdate.add(it)
- }
-
- */
- }
- }
- }
- else if(newPos.latitude>0 && newPos.longitude>0) {
- //we should not have to check for this
- // update it simply
- positionsByVehDict[vehID] = newPos
- //createLabelForVehicle(pos)
- }else{
- Log.w(DEBUG_TAG, "Update ignored for veh $vehID on line ${newPos.routeID}, lat: ${newPos.latitude}, lon ${newPos.longitude}")
- }
-
- }
- // symbolManager.update(symbolsToUpdate)
- //remove old positions
- vehsOld.removeAll(vehsNew)
- //now vehsOld contains the vehicles id for those that have NOT been updated
- val currentTimeStamp = System.currentTimeMillis() /1000
- for(vehID in vehsOld){
- //remove after 2 minutes of inactivity
- if (positionsByVehDict[vehID]!!.timestamp - currentTimeStamp > 2*60){
- positionsByVehDict.remove(vehID)
- //removeVehicleLabel(vehID)
- }
- }
- //finally, update UI
- updatePositionsIcons()
- }
-
- /**
- * This is the tricky part, animating the transitions
- * Basically, we need to set the new positions with the data and redraw them all
- */
- private fun moveVehicleToNewPosition(positionUpdate: LivePositionUpdate){
- if (positionUpdate.vehicle !in positionsByVehDict.keys)
- return
- val vehID = positionUpdate.vehicle
- val currentUpdate = positionsByVehDict[positionUpdate.vehicle]
- currentUpdate?.let { it ->
- //cancel current animation on vehicle
- animatorsByVeh[vehID]?.cancel()
-
- val currentPos = LatLng(it.latitude, it.longitude)
- val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
- val valueAnimator = ValueAnimator.ofObject(MapLibreUtils.LatLngEvaluator(), currentPos, newPos)
- valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
- private var latLng: LatLng? = null
- override fun onAnimationUpdate(animation: ValueAnimator) {
- latLng = animation.animatedValue as LatLng
- //update position on animation
- val update = positionsByVehDict[positionUpdate.vehicle]!!
- latLng?.let { ll->
- update.latitude = ll.latitude
- update.longitude = ll.longitude
- updatePositionsIcons()
- }
- }
- })
- valueAnimator.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationStart(animation: Animator) {
- super.onAnimationStart(animation)
- //val update = positionsByVehDict[positionUpdate.vehicle]!!
- //remove the label at the start of the animation
- /*val annot = busLabelSymbolsByVeh[vehID]
- annot?.let { sym ->
- sym.textOpacity = 0.0f
- symbolsToUpdate.add(sym)
- }
-
- */
-
- }
-
- override fun onAnimationEnd(animation: Animator) {
- super.onAnimationEnd(animation)
- //recreate the label at the end of the animation
- //createLabelForVehicle(positionUpdate)
- /*val annot = busLabelSymbolsByVeh[vehID]
- annot?.let { sym ->
- sym.textOpacity = 1.0f
- sym.latLng = newPos //LatLng(newPos)
- symbolsToUpdate.add(sym)
- }
-
- */
- }
- })
-
- //set the new position as the current one but with the old lat and lng
- positionUpdate.latitude = currentUpdate.latitude
- positionUpdate.longitude = currentUpdate.longitude
- positionsByVehDict[vehID] = positionUpdate
- valueAnimator.duration = 300
- valueAnimator.interpolator = LinearInterpolator()
- valueAnimator.start()
-
- animatorsByVeh[vehID] = valueAnimator
-
- } ?: {
- Log.e(DEBUG_TAG, "Have to run animation for veh ${positionUpdate.vehicle} but not in the dict, adding")
- positionsByVehDict[positionUpdate.vehicle] = positionUpdate
- }
- }
-
- /**
- * Update the bus positions displayed on the map, from the existing data
- */
- private fun updatePositionsIcons(){
- //avoid frequent updates
- val currentTime = System.currentTimeMillis()
- //throttle updates when user is moving camera
- val interval = if(isUserMovingCamera) 150 else 60
- val shouldDelayUpdateDraw = currentTime - lastUpdateTime < interval
- if(shouldDelayUpdateDraw){
- //Defer map update
- viewLifecycleOwner.lifecycleScope.launch {
- delay(200)
- updatePositionsIcons()
- }
- return
- }
- val features = ArrayList<Feature>()//stops.mapNotNull { stop ->
- //stop.latitude?.let { lat ->
- // stop.longitude?.let { lon ->
- for (pos in positionsByVehDict.values){
- //if (s.latitude!=null && s.longitude!=null)
- val point = Point.fromLngLat(pos.longitude, pos.latitude)
- features.add(
- Feature.fromGeometry(
- point,
- JsonObject().apply {
- addProperty("veh", pos.vehicle)
- addProperty("trip", pos.tripID)
- addProperty("bearing", pos.bearing ?:0.0f)
- addProperty("line", pos.routeID.substringBeforeLast('U'))
- }
- )
- )
- /*busLabelSymbolsByVeh[pos.vehicle]?.let {
- it.latLng = LatLng(pos.latitude, pos.longitude)
- symbolsToUpdate.add(it)
- }
-
- */
- }
- //this updates the positions
- busesSource.setGeoJson(FeatureCollection.fromFeatures(features))
- //update labels, clear cache to be used
- //symbolManager.update(symbolsToUpdate)
- symbolsToUpdate.clear()
- lastUpdateTime = System.currentTimeMillis()
- }
// ------ LOCATION STUFF -----
@SuppressLint("MissingPermission")
@@ -1075,29 +675,8 @@
anim.cancel()
}
animatorsByVeh.clear()
- positionsByVehDict.clear()
- updatePositionsIcons()
- }
-
- /**
- * Initialize the map location, but do not enable the component
- */
- @SuppressLint("MissingPermission")
- private fun initMapLocation(style: Style, map: MapLibreMap, context: Context){
- locationComponent = map.locationComponent
- val locationComponentOptions =
- LocationComponentOptions.builder(context)
- .pulseEnabled(true)
- .build()
- val locationComponentActivationOptions =
- MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context)
- locationComponent.activateLocationComponent(locationComponentActivationOptions)
- locationComponent.isLocationComponentEnabled = false
-
- lastLocation?.let {
- if (it.accuracy < 200)
- locationComponent.forceLocationUpdate(it)
- }
+ updatesByVehDict.clear()
+ updatePositionsIcons(forced = false)
}
@@ -1191,10 +770,10 @@
}
}
+
companion object {
private const val STOPS_SOURCE_ID = "stops-source"
private const val STOPS_LAYER_ID = "stops-layer"
- private const val STOPS_LAYER_SEL_ID ="stops-layer-selected"
private const val LABELS_LAYER_ID = "bus-labels-layer"
private const val LABELS_SOURCE = "labels-source"
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
@@ -117,7 +117,7 @@
if (usingMQTT) LIVE_POS_PREF_GTFSRT else LIVE_POS_PREF_MQTT
)
}
- Log.d(DEBUG_TI, "Switched positions source in ViewModel, using MQTT: ${!usingMQTT}")
+ Log.d(DEBUG_TI, "Switched positions source in ViewModel, now using MQTT: ${!usingMQTT}")
serviceStatus.value = LivePositionsServiceStatus.CONNECTING
}
fun setGtfsLineToFilterPos(line: String, pattern: MatoPattern?){
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
@@ -151,130 +151,5 @@
/>
</androidx.constraintlayout.widget.ConstraintLayout>
- <!-- Bottom Sheet for details -->
- <RelativeLayout
- android:id="@+id/bottom_sheet"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:paddingTop="3dp"
- android:orientation="vertical"
- android:background="@drawable/bottom_sheet_background"
- android:elevation="8dp"
- android:padding="13dp"
- app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
- app:behavior_hideable="true"
- app:behavior_peekHeight="4dp"
- android:clickable="true"
- android:focusable="true">
-
- <TextView
- android:id="@+id/stopNumberTextView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="17sp"
- android:layout_alignParentTop="true"
- android:layout_alignParentStart="true"
- android:layout_toStartOf="@id/arrivalsCardButton"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:layout_marginBottom="4dp"
- android:fontFamily="@font/lato_regular"
-
- />
- <TextView
- android:id="@+id/stopTitleTextView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="19sp"
- android:layout_below="@id/stopNumberTextView"
- android:layout_alignParentStart="true"
- android:layout_toStartOf="@id/arrivalsCardButton"
- android:layout_marginBottom="6dp"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:fontFamily="@font/lato_bold"
- />
- <TextView
- android:id="@+id/linesPassingTextView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="15sp"
- android:layout_below="@id/stopTitleTextView"
- android:layout_alignParentStart="true"
- android:layout_toStartOf="@id/arrivalsCardButton"
- android:layout_marginBottom="5dp"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:fontFamily="@font/lato_regular"
- />
- <androidx.cardview.widget.CardView
- android:id="@+id/arrivalsCardButton"
- android:layout_width="50sp"
- android:layout_height="50sp"
- android:layout_marginStart="5sp"
- android:layout_marginEnd="5sp"
- app:cardCornerRadius="25sp"
- app:cardElevation="2dp"
- android:clickable="true"
- android:focusable="true"
- android:layout_alignParentTop="true"
- android:layout_toStartOf="@id/directionsCardButton"
- android:backgroundTint="?android:attr/colorAccent"
- android:foreground="?selectableItemBackground">
-
- <ImageView
- android:layout_width="30sp"
- android:layout_height="30sp"
- android:layout_gravity="center"
- app:tint="?colorOnPrimary"
- app:srcCompat="@drawable/ic_baseline_departure_board_24" />
-
- </androidx.cardview.widget.CardView>
-
- <androidx.cardview.widget.CardView
- android:id="@+id/directionsCardButton"
- android:layout_width="50sp"
- android:layout_height="50sp"
- android:layout_marginStart="5sp"
- android:layout_marginEnd="5sp"
- app:cardCornerRadius="25sp"
- app:cardElevation="2dp"
- android:clickable="true"
- android:focusable="true"
- android:layout_alignParentTop="true"
- android:layout_alignParentEnd="true"
- android:foreground="?selectableItemBackground"
- android:backgroundTint="?android:attr/colorAccent"
- >
-
- <ImageView
- android:id="@+id/rightmostImageView"
- android:layout_width="30sp"
- android:layout_height="30sp"
- android:layout_gravity="center"
- app:srcCompat="@drawable/navigation_right" />
-
- </androidx.cardview.widget.CardView>
-
-
- <!-- Additional details -->
-
- <!-- Close button -->
- <ImageView
- android:layout_width="30sp"
- android:layout_height="30sp"
- app:srcCompat="@drawable/baseline_close_16"
- android:id="@+id/btnClose"
- android:layout_marginTop="10dp"
- android:layout_marginEnd="15dp"
- android:layout_marginBottom="5dp"
- android:layout_marginStart="10dp"
- android:layout_below="@id/directionsCardButton"
- android:layout_alignParentEnd="true"
- app:layout_constraintHorizontal_bias="0.5"
- android:foreground="?selectableItemBackground"
- />
- </RelativeLayout>
+ <include layout="@layout/map_include_bottom_sheet"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ 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,130 +15,7 @@
/>
<!-- Bottom Sheet for details -->
- <RelativeLayout
- android:id="@+id/bottom_sheet"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:paddingTop="3dp"
- android:orientation="vertical"
- android:background="@drawable/bottom_sheet_background"
- android:elevation="8dp"
- android:padding="13dp"
- app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
- app:behavior_hideable="true"
- app:behavior_peekHeight="4dp"
- android:clickable="true"
- android:focusable="true">
-
- <!-- TODO: Deduplicate code from here and fragment_lines_details -->
- <TextView
- android:id="@+id/stopNumberTextView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="17sp"
- android:layout_alignParentTop="true"
- android:layout_alignParentStart="true"
- android:layout_toStartOf="@id/arrivalsCardButton"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:layout_marginBottom="4dp"
- android:fontFamily="@font/lato_regular"
-
- />
- <TextView
- android:id="@+id/stopTitleTextView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="19sp"
- android:layout_below="@id/stopNumberTextView"
- android:layout_alignParentStart="true"
- android:layout_toStartOf="@id/arrivalsCardButton"
- android:layout_marginBottom="6dp"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:fontFamily="@font/lato_bold"
- />
- <TextView
- android:id="@+id/linesPassingTextView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="15sp"
- android:layout_below="@id/stopTitleTextView"
- android:layout_alignParentStart="true"
- android:layout_toStartOf="@id/arrivalsCardButton"
- android:layout_marginBottom="5dp"
- android:layout_marginStart="10dp"
- android:layout_marginEnd="10dp"
- android:fontFamily="@font/lato_regular"
- />
- <androidx.cardview.widget.CardView
- android:id="@+id/arrivalsCardButton"
- android:layout_width="50sp"
- android:layout_height="50sp"
- android:layout_margin="5sp"
- app:cardCornerRadius="25sp"
- app:cardElevation="2dp"
- android:clickable="true"
- android:focusable="true"
- android:layout_alignParentTop="true"
- android:layout_toStartOf="@id/directionsCardButton"
- android:backgroundTint="?android:attr/colorAccent"
- android:foreground="?selectableItemBackground">
-
- <ImageView
- android:layout_width="30sp"
- android:layout_height="30sp"
- android:layout_gravity="center"
- app:tint="?colorOnPrimary"
- app:srcCompat="@drawable/ic_baseline_departure_board_24" />
-
- </androidx.cardview.widget.CardView>
-
- <androidx.cardview.widget.CardView
- android:id="@+id/directionsCardButton"
- android:layout_width="50sp"
- android:layout_height="50sp"
- android:layout_margin="5sp"
- app:cardCornerRadius="25sp"
- app:cardElevation="2dp"
- android:clickable="true"
- android:focusable="true"
- android:layout_alignParentTop="true"
- android:layout_alignParentEnd="true"
- android:foreground="?selectableItemBackground"
- android:backgroundTint="?android:attr/colorAccent"
- >
-
- <ImageView
- android:layout_width="30sp"
- android:layout_height="30sp"
- android:layout_gravity="center"
- app:srcCompat="@drawable/navigation_right" />
-
- </androidx.cardview.widget.CardView>
-
-
- <!-- Additional details -->
-
- <!-- Close button -->
- <ImageView
- android:layout_width="30sp"
- android:layout_height="30sp"
- app:srcCompat="@drawable/baseline_close_16"
- app:tint="@color/red_darker"
- android:id="@+id/btnClose"
- android:layout_marginTop="10dp"
- android:layout_marginEnd="15dp"
- android:layout_marginBottom="5dp"
- android:layout_marginStart="10dp"
- android:layout_below="@id/directionsCardButton"
- android:layout_alignParentEnd="true"
- app:layout_constraintHorizontal_bias="0.5"
- android:foreground="?selectableItemBackground"
- />
- </RelativeLayout>
+ <include layout="@layout/map_include_bottom_sheet"></include>
<FrameLayout
android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/map_include_bottom_sheet.xml b/app/src/main/res/layout/map_include_bottom_sheet.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/layout/map_include_bottom_sheet.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/bottom_sheet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp"
+ android:paddingTop="3dp"
+ android:orientation="vertical"
+ android:background="@drawable/bottom_sheet_background"
+ android:elevation="8dp"
+ android:padding="13dp"
+ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
+ app:behavior_hideable="true"
+ app:behavior_peekHeight="4dp"
+ android:clickable="true"
+ android:focusable="true">
+
+ <TextView
+ android:id="@+id/stopNumberTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="17sp"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/arrivalsCardButton"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp"
+ android:layout_marginBottom="4dp"
+ android:fontFamily="@font/lato_regular"
+
+ />
+ <TextView
+ android:id="@+id/stopTitleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="19sp"
+ android:layout_below="@id/stopNumberTextView"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/arrivalsCardButton"
+ android:layout_marginBottom="6dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp"
+ android:fontFamily="@font/lato_bold"
+ />
+ <TextView
+ android:id="@+id/linesPassingTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="15sp"
+ android:layout_below="@id/stopTitleTextView"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/arrivalsCardButton"
+ android:layout_marginBottom="5dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp"
+ android:fontFamily="@font/lato_regular"
+ />
+ <androidx.cardview.widget.CardView
+ android:id="@+id/arrivalsCardButton"
+ android:layout_width="50sp"
+ android:layout_height="50sp"
+ android:layout_marginStart="5sp"
+ android:layout_marginEnd="5sp"
+ app:cardCornerRadius="25sp"
+ app:cardElevation="2dp"
+ android:clickable="true"
+ android:focusable="true"
+ android:layout_alignParentTop="true"
+ android:layout_toStartOf="@id/directionsCardButton"
+ android:backgroundTint="?android:attr/colorAccent"
+ android:foreground="?selectableItemBackground">
+
+ <ImageView
+ android:layout_width="30sp"
+ android:layout_height="30sp"
+ android:layout_gravity="center"
+ app:tint="?colorOnPrimary"
+ app:srcCompat="@drawable/ic_baseline_departure_board_24" />
+
+ </androidx.cardview.widget.CardView>
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/directionsCardButton"
+ android:layout_width="50sp"
+ android:layout_height="50sp"
+ android:layout_marginStart="5sp"
+ android:layout_marginEnd="5sp"
+ app:cardCornerRadius="25sp"
+ app:cardElevation="2dp"
+ android:clickable="true"
+ android:focusable="true"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ android:foreground="?selectableItemBackground"
+ android:backgroundTint="?android:attr/colorAccent"
+ >
+
+ <ImageView
+ android:id="@+id/rightmostImageView"
+ android:layout_width="30sp"
+ android:layout_height="30sp"
+ android:layout_gravity="center"
+ app:srcCompat="@drawable/navigation_right" />
+
+ </androidx.cardview.widget.CardView>
+
+
+ <!-- Additional details -->
+
+ <!-- Close button -->
+ <ImageView
+ android:layout_width="30sp"
+ android:layout_height="30sp"
+ app:srcCompat="@drawable/baseline_close_16"
+ android:id="@+id/btnClose"
+ android:layout_marginTop="10dp"
+ android:layout_marginEnd="15dp"
+ android:layout_marginBottom="5dp"
+ android:layout_marginStart="10dp"
+ android:layout_below="@id/directionsCardButton"
+ android:layout_alignParentEnd="true"
+ app:layout_constraintHorizontal_bias="0.5"
+ android:foreground="?selectableItemBackground"
+ />
+</RelativeLayout>
\ No newline at end of file

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 16, 06:13 (13 h, 11 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1748565
Default Alt Text
D213.1773637980.diff (101 KB)

Event Timeline