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 + 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(5) + protected val animatorsByVeh = HashMap() + 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(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){ + 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())) + } + 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>, + 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() + val selectedBusFeatures = ArrayList() + + 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) + } + ) + + // 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(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() + 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? = null - //Bottom sheet behavior - private lateinit var bottomSheetBehavior: BottomSheetBehavior - 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(5) - private val animatorsByVeh = HashMap() - private var lastLocation : Location? = null private var enablingPositionFromClick = false private var polyline: LineString? = null @@ -235,7 +202,6 @@ //private var stopPosList = ArrayList() //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(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(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(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){ - 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>){ - 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() - 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() - val selectedBusFeatures = ArrayList() - 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(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 - 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 @@ -172,8 +162,6 @@ private lateinit var busPositionsIconButton: ImageButton private val positionsByVehDict = HashMap(5) - private val animatorsByVeh = HashMap() - private var lastUpdateTime : Long = -1 //private var busLabelSymbolsByVeh = HashMap() private val symbolsToUpdate = ArrayList() @@ -343,7 +331,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 +340,8 @@ displayStops(stopsInCache) if(showBusLayer) setupBusLayer(style) + initSymbolManager(mapReady, style) + // Start observing data now that everything is set up observeStops() } @@ -553,51 +543,10 @@ } - /** - * Update the bottom sheet with the stop information - */ - override fun openStopInBottomSheet(stop: Stop){ - bottomLayout?.let { - - //lay.findViewById(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() - 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 +705,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())) - 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 +722,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 +747,6 @@ */ - /** - * Update function for the bus positions - * Takes the processed updates and saves them accordingly - */ - private fun updateBusPositionsInMap(incomingData: HashMap>){ - val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle }) - val vehsOld = HashSet(positionsByVehDict.keys) - - val symbolsToUpdate = ArrayList() - 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()//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") @@ -1076,28 +791,7 @@ } 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) - } + updatePositionsIcons(forced = false) } @@ -1191,10 +885,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/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 @@ /> - - - - - - - - - - - - - - - - - - - - - - - - + \ 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 @@ /> - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file