Page Menu
Home
GitPull.it
Search
Configure Global Search
Log In
Files
F11448443
D213.1773637980.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
101 KB
Referenced Files
None
Subscribers
None
D213.1773637980.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D213: Move code to general fragment, attempt bug fix
Attached
Detach File
Event Timeline
Log In to Comment