Page MenuHomeGitPull.it

D213.1775629334.diff
No OneTemporary

Size
83 KB
Referenced Files
None
Subscribers
None

D213.1775629334.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,52 @@
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.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.layers.Property.ICON_ANCHOR_CENTER
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 +54,21 @@
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 var isSelBusSourceInit = false
protected lateinit var sharedPreferences: SharedPreferences
+ protected lateinit var bottomSheetBehavior: BottomSheetBehavior<RelativeLayout>
+
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener(){ pref, key ->
/*when(key){
@@ -46,9 +82,35 @@
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
+
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -66,6 +128,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 +170,10 @@
super.onDestroy()
}
+ override fun onDestroyView() {
+ bottomLayout = null
+ super.onDestroyView()
+ }
protected fun reloadMap(){
/*map?.let {
@@ -111,11 +192,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
@@ -193,6 +279,397 @@
return isPointInsideVisibleRegion(p, other)
}
+ protected fun initSelBusSource(){
+ selectedBusSource = GeoJsonSource(SEL_BUS_SOURCE)
+ isSelBusSourceInit = true
+ }
+
+ 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 (isSelBusSourceInit && vehShowing.isNotEmpty() && vehShowing == dat.posUpdate.vehicle) {
+ selectedBusFeatures.add(newFeature)
+ } else {
+ busFeatures.add(newFeature)
+ }
+ }
+
+ busesSource.setGeoJson(FeatureCollection.fromFeatures(busFeatures))
+ if (isSelBusSourceInit)
+ 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
+ //isBottomSheetShowing = true
+ }
+
+
+
+ protected fun stopAnimations(){
+ for(anim in animatorsByVeh.values){
+ anim.cancel()
+ }
+ }
companion object{
@@ -204,6 +681,10 @@
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 KEY_LOCATION_ENABLED="location_enabled"
+
+ const val STOP_ACTIVE_IMG = "stop_active_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,16 @@
//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 +202,6 @@
//private var stopPosList = ArrayList<GeoPoint>()
//fragment actions
- private lateinit var fragmentListener: CommonFragmentListener
private var showOnTopOfLine = false
private var recyclerInitDone = false
@@ -251,8 +217,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 +253,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()
@@ -537,18 +490,7 @@
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)
@@ -667,96 +615,8 @@
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]
@@ -831,7 +691,7 @@
//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)!!)
@@ -884,6 +744,7 @@
/**
* Setup the Map Layers
+ * TODO: Move into GeneralMapLibreFragment
*/
private fun setupBusLayer(style: Style) {
// Buses source
@@ -922,20 +783,7 @@
}
- 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!
@@ -1198,260 +1046,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 +1079,7 @@
*/
}
//initialize GUI here
- fragmentListener.readyGUIfor(FragmentKind.LINES)
+ fragmentListener?.readyGUIfor(FragmentKind.LINES)
}
@@ -1555,7 +1149,7 @@
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"
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
@@ -58,6 +58,7 @@
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.style.expressions.Expression
import org.maplibre.android.style.layers.Property.*
import org.maplibre.android.style.layers.PropertyFactory
@@ -80,9 +81,6 @@
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
@@ -97,15 +95,7 @@
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 +161,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 +282,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 +297,7 @@
livePositionsViewModel.stopMatoUpdates()
livePositionsViewModel.requestGTFSUpdates()
}
+ Log.d(DEBUG_TAG, "Should clear positions: $clearPos")
if (clearPos) {
livePositionsViewModel.clearAllPositions()
//force clear of the viewed data
@@ -343,7 +333,7 @@
mapStyle = style
//setupLayers(style)
- initMapLocation(style, mapReady, requireContext())
+ initMapUserLocation(style, mapReady, requireContext())
//init stop layer with this
val stopsInCache = stopsViewModel.getAllStopsLoaded()
if(stopsInCache.isEmpty())
@@ -352,6 +342,8 @@
displayStops(stopsInCache)
if(showBusLayer) setupBusLayer(style)
+ initSymbolManager(mapReady, style)
+
// Start observing data now that everything is set up
observeStops()
}
@@ -553,51 +545,10 @@
}
- /**
- * 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 showOpenStopWithSymbolLayer(): Boolean {
+ return false
}
+
override fun onAttach(context: Context) {
super.onAttach(context)
fragmentListener = if (context is CommonFragmentListener) {
@@ -756,25 +707,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 +724,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 +749,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 +792,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 +887,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
Wed, Apr 8, 08:22 (18 h, 36 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1828594
Default Alt Text
D213.1775629334.diff (83 KB)

Event Timeline