diff --git a/app/src/main/java/it/reyboz/bustorino/backend/LivePositionTripPattern.kt b/app/src/main/java/it/reyboz/bustorino/backend/LivePositionTripPattern.kt
new file mode 100644
index 0000000..3c4c051
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/backend/LivePositionTripPattern.kt
@@ -0,0 +1,9 @@
+package it.reyboz.bustorino.backend
+
+import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
+import it.reyboz.bustorino.data.gtfs.MatoPattern
+
+data class LivePositionTripPattern(
+ var posUpdate: LivePositionUpdate,
+ var pattern: MatoPattern?
+)
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
index 7c67413..31fb240 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
@@ -1,1340 +1,1406 @@
/*
BusTO - Fragments components
Copyright (C) 2023 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments
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.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.fragment.app.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.gson.JsonObject
import it.reyboz.bustorino.R
import it.reyboz.bustorino.adapters.NameCapitalize
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.map.CustomInfoWindow.TouchResponder
import it.reyboz.bustorino.middleware.LocationUtils
import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.viewmodels.LinesViewModel
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import org.maplibre.android.MapLibre
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.MapView
import org.maplibre.android.maps.OnMapReadyCallback
import org.maplibre.android.maps.Style
import org.maplibre.android.plugins.annotation.Symbol
import org.maplibre.android.plugins.annotation.SymbolManager
import org.maplibre.android.plugins.annotation.SymbolOptions
import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.layers.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
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.maplibre.geojson.Point
class LinesDetailFragment() : ScreenBaseFragment(), OnMapReadyCallback {
private var lineID = ""
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 patternsSpinnerState: Parcelable? = null
private lateinit var currentPatterns: List
//private lateinit var map: MapView
private var patternShown: MatoPatternWithStops? = null
private val viewModel: LinesViewModel by viewModels()
private val mapViewModel: MapViewModel by viewModels()
private var firstInit = true
private var pausedFragment = false
private lateinit var switchButton: ImageButton
private var favoritesButton: ImageButton? = null
private var locationIcon: ImageButton? = null
private var isLineInFavorite = false
private var appContext: Context? = null
private var isLocationPermissionOK = false
private val lineSharedPrefMonitor = SharedPreferences.OnSharedPreferenceChangeListener { pref, keychanged ->
if(keychanged!=PreferencesHolder.PREF_FAVORITE_LINES || lineID.isEmpty()) return@OnSharedPreferenceChangeListener
val newFavorites = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet())
newFavorites?.let {favorites->
isLineInFavorite = favorites.contains(lineID)
//if the button has been intialized, change the icon accordingly
favoritesButton?.let { button->
//avoid crashes if fragment not attached
if(context==null) return@let
if(isLineInFavorite) {
button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null))
appContext?.let { Toast.makeText(it,R.string.favorites_line_add,Toast.LENGTH_SHORT).show()}
} else {
button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_outline, null))
appContext?.let {Toast.makeText(it,R.string.favorites_line_remove,Toast.LENGTH_SHORT).show()}
}
}
}
}
private lateinit var stopsRecyclerView: RecyclerView
private lateinit var descripTextView: TextView
//adapter for recyclerView
private val stopAdapterListener= object : StopAdapterListener {
override fun onTappedStop(stop: Stop?) {
if(viewModel.shouldShowMessage) {
Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show()
viewModel.shouldShowMessage=false
}
stop?.let {
fragmentListener.requestArrivalsForStopID(it.ID)
}
if(stop == null){
Log.e(DEBUG_TAG,"Passed wrong stop")
}
if(fragmentListener == null){
Log.e(DEBUG_TAG, "Fragment listener is null")
}
}
override fun onLongPressOnStop(stop: Stop?): Boolean {
TODO("Not yet implemented")
}
}
private val patternsSorter = Comparator{ p1: MatoPatternWithStops, p2: MatoPatternWithStops ->
if(p1.pattern.directionId != p2.pattern.directionId)
return@Comparator p1.pattern.directionId - p2.pattern.directionId
else
return@Comparator -1*(p1.stopsIndices.size - p2.stopsIndices.size)
}
//map data
private lateinit var mapView: MapView
private lateinit var locationComponent: LocationComponent
private lateinit var mapStyle: Style
protected var map: MapLibreMap? = null
- private var mapInitCompleted = false
private lateinit var stopsSource: GeoJsonSource
private lateinit var busesSource: GeoJsonSource
private lateinit var polylineSource: GeoJsonSource
private var stopsLayerStarted = false
private var lastStopsSizeShown = 0
private var lastUpdateTime:Long = -2
//BUS POSITIONS
- private val positionsByVehDict = HashMap(5)
+ private val updatesByVehDict = HashMap(5)
private val animatorsByVeh = HashMap()
private var lastLocation : Location? = null
private var enablingPositionFromClick = false
private var polyline: LineString? = null
private val showUserPositionRequestLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
ActivityResultCallback { result ->
if (result == null) {
Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?")
} else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION]
&& java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) {
// We can use the position, restart location overlay
if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
return@ActivityResultCallback ///@registerForActivityResult
setMapUserLocationEnabled(true, true, enablingPositionFromClick)
} else Log.w(DEBUG_TAG, "No location permission")
})
//private var stopPosList = ArrayList()
//fragment actions
private lateinit var fragmentListener: CommonFragmentListener
private val stopTouchResponder = TouchResponder { stopID, stopName ->
Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID")
fragmentListener.requestArrivalsForStopID(stopID)
}
private var showOnTopOfLine = false
private var recyclerInitDone = false
private var useMQTTPositions = true
//position of live markers
private val tripMarkersAnimators = HashMap()
private val liveBusViewModel: LivePositionsViewModel by viewModels()
//extra items to use the LibreMap
private lateinit var symbolManager : SymbolManager
private var stopActiveSymbol: Symbol? = null
private var shownStopInBottomSheet : Stop? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lineID = requireArguments().getString(LINEID_KEY,"")
MapLibre.getInstance(requireContext())
}
@SuppressLint("SetTextI18n")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//reset statuses
isBottomSheetShowing = false
- mapInitCompleted = false
stopsLayerStarted = false
lastStopsSizeShown = 0
val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false)
//lineID = requireArguments().getString(LINEID_KEY, "")
arguments?.let {
lineID = it.getString(LINEID_KEY, "")
}
switchButton = rootView.findViewById(R.id.switchImageButton)
locationIcon = rootView.findViewById(R.id.locationEnableIcon)
favoritesButton = rootView.findViewById(R.id.favoritesButton)
stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView)
descripTextView = rootView.findViewById(R.id.lineDescripTextView)
descripTextView.visibility = View.INVISIBLE
//map stuff
mapView = rootView.findViewById(R.id.lineMap)
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()
}
val titleTextView = rootView.findViewById(R.id.titleTextView)
titleTextView.text = getString(R.string.line)+" "+FiveTNormalizer.fixShortNameForDisplay(
GtfsUtils.getLineNameFromGtfsID(lineID), true)
favoritesButton?.isClickable = true
favoritesButton?.setOnClickListener {
if(lineID.isNotEmpty())
PreferencesHolder.addOrRemoveLineToFavorites(requireContext(),lineID,!isLineInFavorite)
}
val preferences = PreferencesHolder.getMainSharedPreferences(requireContext())
val favorites = preferences.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet())
if(favorites!=null && favorites.contains(lineID)){
favoritesButton?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null))
isLineInFavorite = true
}
appContext = requireContext().applicationContext
preferences.registerOnSharedPreferenceChangeListener(lineSharedPrefMonitor)
patternsSpinner = rootView.findViewById(R.id.patternsSpinner)
patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList())
patternsSpinner.adapter = patternsAdapter
initializeRecyclerView()
switchButton.setOnClickListener{
if(mapView.visibility == View.VISIBLE){
hideMapAndShowStopList()
} else{
hideStopListAndShowMap()
}
}
locationIcon?.let {view ->
if(!LocationUtils.isLocationEnabled(requireContext()) || !Permissions.anyLocationPermissionsGranted(requireContext()))
setLocationIconEnabled(false)
//set click Listener
view.setOnClickListener(this::onPositionIconButtonClick)
}
//set
//INITIALIZE VIEW MODELS
viewModel.setRouteIDQuery(lineID)
liveBusViewModel.setGtfsLineToFilterPos(lineID, null)
val keySourcePositions = getString(R.string.pref_positions_source)
useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions, "mqtt").contentEquals("mqtt")
viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){
patterns -> savePatternsToShow(patterns)
}
/*
- We have the pattern and the stops here, time to display them
*/
viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops ->
if(mapView.visibility ==View.VISIBLE)
patternShown?.let{
- displayPatternWithStopsOnMap(it,stops)
+ // We have the pattern and the stops here, time to display them
+ displayPatternWithStopsOnMap(it,stops, true)
} ?:{
Log.w(DEBUG_TAG, "The viewingPattern is null!")
}
else{
if(stopsRecyclerView.visibility==View.VISIBLE)
showStopsAsList(stops)
}
}
viewModel.gtfsRoute.observe(viewLifecycleOwner){route->
if(route == null){
//need to close the fragment
activity?.supportFragmentManager?.popBackStack()
return@observe
}
descripTextView.text = route.longName
descripTextView.visibility = View.VISIBLE
}
+ /*
+ TODO: move somewhere else
if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){
val patt = viewModel.selectedPatternLiveData.value!!
Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}")
showPattern(patt)
pausedFragment = false
}
+ */
+
Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}")
//listeners
patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
val currentShownPattern = patternShown?.pattern
val patternWithStops = currentPatterns.get(position)
//viewModel.setPatternToDisplay(patternWithStops)
setPatternAndReqStops(patternWithStops)
Log.d(DEBUG_TAG, "item Selected, cleaning bus markers")
if(mapView.visibility == View.VISIBLE) {
//Clear buses if we are changing direction
currentShownPattern?.let { patt ->
if(patt.directionId != patternWithStops.pattern.directionId){
stopAnimations()
- positionsByVehDict.clear()
+ updatesByVehDict.clear()
updatePositionsIcons(true)
liveBusViewModel.retriggerPositionUpdate()
}
}
}
liveBusViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern)
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
Log.d(DEBUG_TAG, "Views created!")
return rootView
}
// ------------- UI switch stuff ---------
private fun hideMapAndShowStopList(){
mapView.visibility = View.GONE
stopsRecyclerView.visibility = View.VISIBLE
locationIcon?.visibility = View.GONE
viewModel.setMapShowing(false)
if(useMQTTPositions) liveBusViewModel.stopMatoUpdates()
//map.overlayManager.remove(busPositionsOverlay)
switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30))
}
private fun hideStopListAndShowMap(){
stopsRecyclerView.visibility = View.GONE
mapView.visibility = View.VISIBLE
locationIcon?.visibility = View.VISIBLE
viewModel.setMapShowing(true)
//map.overlayManager.add(busPositionsOverlay)
//map.
if(useMQTTPositions)
liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID))
else
liveBusViewModel.requestGTFSUpdates()
switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30))
}
private fun setLocationIconEnabled(setTrue: Boolean){
if(setTrue)
locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
else
locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
}
/**
* Handles logic of enabling the user location on the map
*/
@SuppressLint("MissingPermission")
private fun setMapUserLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) {
if (enabled) {
val permissionOk = assumePermissions || Permissions.bothLocationPermissionsGranted(requireContext())
if (permissionOk) {
Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions")
locationComponent.isLocationComponentEnabled = true
//locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING
setLocationIconEnabled(true)
if (fromClick) Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show()
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
//TODO: show dialog for permission rationale
Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show()
}
Log.d(DEBUG_TAG, "Requesting permission to show user location")
enablingPositionFromClick = fromClick
showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
}
} else{
locationComponent.isLocationComponentEnabled = false
setLocationIconEnabled(false)
if (fromClick) {
Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show()
//TODO: Cancel the request for the enablement of the position if needed
}
}
}
/**
* Switch position icon from activ
*/
private fun onPositionIconButtonClick(view: View){
if(locationComponent.isLocationComponentEnabled) setMapUserLocationEnabled(false, false, true)
else{
setMapUserLocationEnabled(true, false, true)
}
}
// ------------- Map Code -------------------------
/**
* This method sets up the map and the layers
*/
override fun onMapReady(mapReady: MapLibreMap) {
this.map = mapReady
val mjson = Styles.getJsonStyleFromAsset(requireContext(), "map_style_good_noshops.json")
//ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json")
activity?.run {
val builder = Style.Builder().fromJson(mjson!!)
mapReady.setStyle(builder) { style ->
mapStyle = style
//setupLayers(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
}
// Start observing data
initMapUserLocation(style, mapReady, requireContext())
- //initStopsLayer(style, FeatureCollection.fromFeatures(ArrayList()))
- //init stop layer with this
- //val stopsInCache = stopsViewModel.getAllStopsLoaded()
- //if(stopsInCache.isEmpty())
+
+ //if(!stopsLayerStarted)
initStopsPolyLineLayers(style, FeatureCollection.fromFeatures(ArrayList()), null)
+ /*if(patternShown!=null){
+ viewModel.stopsForPatternLiveData.value?.let {
+ Log.d(DEBUG_TAG, "Show stops from the cache")
+ displayPatternWithStopsOnMap(patternShown!!, it, true)
+ }
+ }
+ if(!stopsLayerStarted) {
+ Log.d(DEBUG_TAG, "Stop layer is not started yet")
+ initStopsPolyLineLayers(style, FeatureCollection.fromFeatures(ArrayList()), null)
+ }*/
//else
// displayStops(stopsInCache)
setupBusLayer(style)
}
mapReady.addOnMapClickListener { point ->
val screenPoint = mapReady.projection.toScreenLocation(point)
val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID)
val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_LAYER_ID)
if (features.isNotEmpty()) {
val feature = features[0]
val id = feature.getStringProperty("id")
val name = feature.getStringProperty("name")
//Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show()
val stop = viewModel.getStopByID(id)
stop?.let {
if (isBottomSheetShowing){
hideStopBottomSheet()
}
openStopInBottomSheet(it)
isBottomSheetShowing = true
//move camera
if(it.latitude!=null && it.longitude!=null)
//mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(it.latitude!!, it.longitude!!)).build()
mapReady.animateCamera(CameraUpdateFactory.newLatLng(LatLng(it.latitude!!,it.longitude!!)),750)
}
return@addOnMapClickListener true
} else if (busNearby.isNotEmpty()){
val feature = busNearby[0]
val vehid = feature.getStringProperty("veh")
val route = feature.getStringProperty("line")
+ if(isBottomSheetShowing && shownStopInBottomSheet!=null)
+ hideStopBottomSheet()
+ //if(context!=null){
+ // Toast.makeText(context, "Veh $vehid on route ${route.slice(0..route.length-2)}", Toast.LENGTH_SHORT).show()
+ //}
+ showVehicleTripInBottomSheet(vehid)
- if(context!=null){
- Toast.makeText(context, "Veh $vehid on route ${route.slice(0..route.length-2)}", Toast.LENGTH_SHORT).show()
- }
return@addOnMapClickListener true
}
false
}
-
- mapInitCompleted = true
// we start requesting the bus positions now
observeBusPositionUpdates()
}
/*savedMapStateOnPause?.let{
restoreMapStateFromBundle(it)
pendingLocationActivation = false
Log.d(DEBUG_TAG, "Restored map state from the saved bundle")
}
*/
//reset saved State at the end
//if( savedMapStateOnPause == null) {
//set initial position
val zoom = 12.0
//center position
//val latlngTarget = initialStopToShow?.let {
// LatLng(it.latitude!!, it.longitude!!)
//} ?:
val latlngTarget = LatLng(MapLibreFragment.DEFAULT_CENTER_LAT, MapLibreFragment.DEFAULT_CENTER_LON)
mapReady.cameraPosition = CameraPosition.Builder().target(latlngTarget).zoom(zoom).build()
//}
//reset saved state
//savedMapStateOnPause = null
}
private fun observeBusPositionUpdates(){
//live bus positions
liveBusViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ updates ->
//Log.d(DEBUG_TAG, "Received ${updates.size} updates for the positions")
if(mapView.visibility == View.GONE || patternShown ==null){
//DO NOTHING
Log.w(DEBUG_TAG, "not doing anything because map is not visible")
return@observe
}
updateBusPositionsInMap(updates)
/*
val filtdLineID = GtfsUtils.stripGtfsPrefix(lineID)
//filter buses with direction, show those only with the same direction
val updsForVeh = HashMap>()
val currentPattern = viewingPattern!!.pattern
val numUpds = updates.entries.size
Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}")
// cannot understand where this is used
val patternsDirections = HashMap()
for((tripId, pair) in updates.entries){
//remove trips with wrong line ideas
val posUp = pair.first
if(pair.first.routeID!=filtdLineID)
continue
if(pair.second!=null && pair.second?.pattern !=null){
val dir = pair.second?.pattern?.directionId
if(dir !=null && dir == currentPattern.directionId){
updsForVeh[posUp.vehicle] = pair
}
patternsDirections[tripId] = dir ?: -10
} else{
updsForVeh[posUp.vehicle] = pair
//Log.d(DEBUG_TAG, "No pattern for tripID: $tripId")
patternsDirections[tripId] = -10
}
}
Log.d(DEBUG_TAG, " Filtered updates are ${updsForVeh.keys.size}") // Original updates directs: $patternsDirections\n
*/
//if not using MQTT positions
if(!useMQTTPositions){
liveBusViewModel.requestDelayedGTFSUpdates(2000)
}
}
//download missing tripIDs
liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){
//gtfsPosViewModel.downloadTripsFromMato(dat);
MatoTripsDownloadWorker.requestMatoTripsDownload(
it, requireContext().applicationContext,
"BusTO-MatoTripDownload"
)
}
}
/**
* 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 =
MapUtils.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
*/
private fun openStopInBottomSheet(stop: Stop){
bottomLayout?.let {
//lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
stopTitleTextView.text = stop.stopDefaultName
stopNumberTextView.text = stop.ID
+ stopTitleTextView.visibility = View.VISIBLE
+
val string_show = if (stop.numRoutesStopping==0) ""
else if (stop.numRoutesStopping <= 1)
requireContext().getString(R.string.line_fill, stop.routesThatStopHereToString())
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
+ 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)
)
}
shownStopInBottomSheet = stop
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
// 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
//remove initial stop
//if(initialStopToShow!=null){
// initialStopToShow = null
//}
shownStopInBottomSheet = null
}
+ private fun showVehicleTripInBottomSheet(veh: String){
+ val data = updatesByVehDict[veh]
+ if(data==null) return
+
+ bottomLayout?.let {
+ val lineName = FiveTNormalizer.fixShortNameForDisplay(
+ GtfsUtils.getLineNameFromGtfsID(data.posUpdate.routeID), true)
+ stopNumberTextView.text = requireContext().getString(R.string.line_fill, lineName)
+ data.pattern?.let { pat ->
+ stopTitleTextView.text = pat.headsign
+ stopTitleTextView.visibility = View.VISIBLE
+ } ?:{
+ stopTitleTextView.visibility = View.GONE
+ }
+ linesPassingTextView.text = data.posUpdate.vehicle
+ }
+ arrivalsCard.visibility=View.GONE
+ bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_magnifying_glass, activity?.theme))
+
+
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+
+ }
+
// ------- MAP LAYERS INITIALIZE ----
/**
* Initialize the map layers for the stops
*/
private fun initStopsPolyLineLayers(style: Style, stopFeatures:FeatureCollection, lineFeature: Feature?){
+ Log.d(DEBUG_TAG, "INIT STOPS CALLED")
stopsSource = GeoJsonSource(STOPS_SOURCE_ID,stopFeatures)
style.addSource(stopsSource)
//val context = requireContext()
val stopIcon = ResourcesCompat.getDrawable(resources,R.drawable.ball, activity?.theme)!!
val imgStop = ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!
//set the image tint
//DrawableCompat.setTint(imgBus,ContextCompat.getColor(context,R.color.line_drawn_poly))
// add icon
style.addImage(STOP_IMAGE_ID,stopIcon)
style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!)
// Stops layer
val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID)
stopsLayer.withProperties(
PropertyFactory.iconImage(STOP_IMAGE_ID),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconIgnorePlacement(true)
)
polylineSource = lineFeature?.let { GeoJsonSource(POLYLINE_SOURCE, it) } ?: GeoJsonSource(POLYLINE_SOURCE)
style.addSource(polylineSource)
val color=ContextCompat.getColor(requireContext(),R.color.line_drawn_poly)
//paint.style = Paint.Style.FILL_AND_STROKE
//paint.strokeJoin = Paint.Join.ROUND
//paint.strokeCap = Paint.Cap.ROUND
val lineLayer = LineLayer(POLYLINE_LAYER, POLYLINE_SOURCE).withProperties(
PropertyFactory.lineColor(color),
PropertyFactory.lineWidth(5.0f), //originally 13f
PropertyFactory.lineOpacity(1.0f),
PropertyFactory.lineJoin(Property.LINE_JOIN_ROUND),
PropertyFactory.lineCap(Property.LINE_CAP_ROUND)
)
style.addLayerBelow(lineLayer,"label_country_1")
style.addLayerAbove(stopsLayer, POLYLINE_LAYER)
stopsLayerStarted = true
}
/**
* Setup the Map Layers
*/
private fun setupBusLayer(style: Style) {
// Buses source
busesSource = GeoJsonSource(BUSES_SOURCE_ID)
style.addSource(busesSource)
style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!)
// Buses layer
val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply {
withProperties(
PropertyFactory.iconImage("bus_symbol"),
//PropertyFactory.iconSize(1.2f),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconIgnorePlacement(true),
PropertyFactory.iconRotate(Expression.get("bearing")),
PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP)
)
}
style.addLayerAbove(busesLayer, STOPS_LAYER_ID)
}
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()
}
}
private fun savePatternsToShow(patterns: List){
currentPatterns = patterns.sortedWith(patternsSorter)
patternsAdapter?.let {
it.clear()
it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" })
it.notifyDataSetChanged()
}
patternShown?.let {
showPattern(it)
}
}
/**
* Called when the position of the spinner is updated
*/
private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){
Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}")
viewModel.selectedPatternLiveData.value = patternWithStops
viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order }
patternShown = patternWithStops
viewModel.requestStopsForPatternWithStops(patternWithStops)
}
private fun showPattern(patternWs: MatoPatternWithStops){
Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}")
var pos = -2
val code = patternWs.pattern.code.trim()
for(k in currentPatterns.indices){
if(currentPatterns[k].pattern.code.trim() == code){
pos = k
break
}
}
Log.d(DEBUG_TAG, "Found pattern $code in position: $pos")
if(pos>=0)
patternsSpinner.setSelection(pos)
//set pattern
setPatternAndReqStops(patternWs)
}
private fun zoomToCurrentPattern(){
if(polyline==null) return
val NULL_VALUE = -4000.0
var maxLat = NULL_VALUE
var minLat = NULL_VALUE
var minLong = NULL_VALUE
var maxLong = NULL_VALUE
polyline?.let {
for(p in it.coordinates()){
val lat = p.latitude()
val lon = p.longitude()
// get max latitude
if(maxLat == NULL_VALUE)
maxLat =lat
else if (maxLat < lat) maxLat = lat
// find min latitude
if (minLat ==NULL_VALUE)
minLat = lat
else if (minLat > lat) minLat = lat
if(maxLong == NULL_VALUE || maxLong < lon )
maxLong = lon
if (minLong == NULL_VALUE || minLong > lon)
minLong = lon
}
val padding = 100 // Pixel di padding intorno ai limiti
Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong")
val bbox = LatLngBounds.from(maxLat,maxLong, minLat, minLong)
//map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false)
map?.animateCamera(CameraUpdateFactory.newLatLngBounds(bbox, padding))
}
}
- private fun displayPatternWithStopsOnMap(patternWs: MatoPatternWithStops, stops: List){
+ private fun displayPatternWithStopsOnMap(patternWs: MatoPatternWithStops, stops: List, zoomToPattern: Boolean){
Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}")
//if(viewingPattern==null || map == null) return
- if (map==null || !mapInitCompleted) return
+ if (map==null) return
patternShown = patternWs
val pattern = patternWs.pattern
val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength)
val pointsToShow = pointsList.map { Point.fromLngLat(it.longitude, it.latitude) }
Log.d(DEBUG_TAG, "The polyline has ${pointsToShow.size} points to display")
polyline = LineString.fromLngLats(pointsToShow)
val lineFeature = Feature.fromGeometry(polyline)
//Log.d(DEBUG_TAG, "Polyline in JSON is: ${lineFeature.toJson()}")
// --- STOPS---
val features = ArrayList()//stops.mapNotNull { stop ->
//stop.latitude?.let { lat ->
// stop.longitude?.let { lon ->
for (s in stops){
if (s.latitude!=null && s.longitude!=null) {
val loc = if (showOnTopOfLine) findOptimalPosition(s, pointsList)
else LatLng(s.latitude!!, s.longitude!!)
features.add(
Feature.fromGeometry(
Point.fromLngLat(loc.longitude, loc.latitude),
JsonObject().apply {
addProperty("id", s.ID)
addProperty("name", s.stopDefaultName)
//addProperty("routes", s.routesThatStopHereToString()) // Add routes array to JSON object
}
)
)
}
}
Log.d(DEBUG_TAG,"Have put ${features.size} stops to display")
// if the layer is already started, substitute the stops inside, otherwise start it
if (stopsLayerStarted) {
stopsSource.setGeoJson(FeatureCollection.fromFeatures(features))
polylineSource.setGeoJson(lineFeature)
lastStopsSizeShown = features.size
} else
map?.let {
Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer")
initStopsPolyLineLayers(mapStyle, FeatureCollection.fromFeatures(features),lineFeature)
Log.d(DEBUG_TAG,"Started stops layer on map")
lastStopsSizeShown = features.size
stopsLayerStarted = true
}
/* OLD CODE
for(s in stops){
val gp =
val marker = MarkerUtils.makeMarker(
gp, s.ID, s.stopDefaultName,
s.routesThatStopHereToString(),
map,stopTouchResponder, stopIcon,
R.layout.linedetail_stop_infowindow,
R.color.line_drawn_poly
)
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
stopsOverlay.add(marker)
}
*/
//POINTS LIST IS NOT IN ORDER ANY MORE
//if(!map.overlayManager.contains(stopsOverlay)){
// map.overlayManager.add(stopsOverlay)
//}
- zoomToCurrentPattern()
+ if(zoomToPattern) zoomToCurrentPattern()
//map.invalidate()
}
private fun initializeRecyclerView(){
val llManager = LinearLayoutManager(context)
llManager.orientation = LinearLayoutManager.VERTICAL
stopsRecyclerView.layoutManager = llManager
}
private fun showStopsAsList(stops: List){
Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value)
val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index}
val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] }
val numStops = stopsSorted.size
Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}")
val setNewAdapter = true
if(setNewAdapter){
stopsRecyclerView.adapter = StopRecyclerAdapter(
stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES,
NameCapitalize.FIRST
)
}
}
-
-
+ /**
+ * This method fixes the display of the pattern, to be used when clicking on a bus
+ */
private fun displayPatternInSpinner(patternId: String){
//var index = 0
Log.d(DEBUG_TAG, "Showing pattern with code $patternId ")
for (i in currentPatterns.indices){
val pattStop = currentPatterns[i]
if(pattStop.pattern.code == patternId){
Log.d(DEBUG_TAG, "Pattern found in position $i")
//setPatternAndReqStops(pattStop)
patternsSpinner.setSelection(i)
break
}
}
}
/**
* 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(positionsByVehDict.keys)
+ val vehsOld = HashSet(updatesByVehDict.keys)
Log.d(DEBUG_TAG, "In fragment, have ${incomingData.size} updates to show")
var countUpds = 0
//val symbolsToUpdate = ArrayList()
for (upsWithTrp in incomingData.values){
val pos = upsWithTrp.first
+ val patternStops = upsWithTrp.second
val vehID = pos.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]
+ val oldPos = updatesByVehDict[vehID]?.posUpdate
var avoidShowingUpdateBecauseIsImpossible = false
oldPos?.let{
- if(oldPos.routeID!=pos.routeID) {
+
+ if(it.routeID!=pos.routeID) {
val dist = LatLng(it.latitude, it.longitude).distanceTo(LatLng(pos.latitude, pos.longitude))
val speed = dist*3.6 / (pos.timestamp - it.timestamp) //this should be in km/h
Log.w(DEBUG_TAG, "Vehicle $vehID changed route from ${oldPos.routeID} to ${pos.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==pos.latitude)&&(oldPos.longitude == pos.longitude) }?:false
+ val samePosition = oldPos?.let { (it.latitude==pos.latitude)&&(it.longitude == pos.longitude) }?:false
if(!samePosition) {
//val isPositionInBounds = isInsideVisibleRegion(
// pos.latitude, pos.longitude, true
//) || (oldPos?.let { isInsideVisibleRegion(it.latitude,it.longitude,true) } ?: false)
val skip = true
if (skip) {
//animate = true
//this moves both the icon and the label
moveVehicleToNewPosition(pos)
} else {
- positionsByVehDict[vehID] = pos
+ //update
+ updatesByVehDict[vehID] = LivePositionTripPattern(pos,patternStops?.pattern)
/*busLabelSymbolsByVeh[vehID]?.let {
it.latLng = LatLng(pos.latitude, pos.longitude)
symbolsToUpdate.add(it)
}*/
}
}
countUpds++
}
else{
+ //not inside
// update it simply
- positionsByVehDict[vehID] = pos
+ updatesByVehDict[vehID] = LivePositionTripPattern(pos, patternStops?.pattern)
//createLabelForVehicle(pos)
}
}
//symbolManager.update(symbolsToUpdate)
//remove old positions
Log.d(DEBUG_TAG, "Updated $countUpds 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 (positionsByVehDict[vehID]!!.timestamp - currentTimeStamp > 2*60){
- positionsByVehDict.remove(vehID)
+ if (updatesByVehDict[vehID]!!.posUpdate.timestamp - currentTimeStamp > 2*60){
+ updatesByVehDict.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 moveVehicleToNewPosition(positionUpdate: LivePositionUpdate){
- if (positionUpdate.vehicle !in positionsByVehDict.keys)
+ if (positionUpdate.vehicle !in updatesByVehDict.keys)
return
val vehID = positionUpdate.vehicle
- val currentUpdate = positionsByVehDict[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(it.latitude, it.longitude)
+ val currentPos = LatLng(posUp.latitude, posUp.longitude)
val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
val valueAnimator = ValueAnimator.ofObject(MapUtils.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]!!
+ val update = updatesByVehDict[positionUpdate.vehicle]!!
latLng?.let { ll->
- update.latitude = ll.latitude
- update.longitude = ll.longitude
+ update.posUpdate.latitude = ll.latitude
+ update.posUpdate.longitude = ll.longitude
updatePositionsIcons(false)
}
}
})
/*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 = currentUpdate.latitude
- positionUpdate.longitude = currentUpdate.longitude
- positionsByVehDict[vehID] = positionUpdate
+ positionUpdate.latitude = posUp.latitude
+ positionUpdate.longitude = posUp.longitude
+ 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")
- positionsByVehDict[positionUpdate.vehicle] = positionUpdate
+ //updatesByVehDict[positionUpdate.vehicle] = positionUpdate
}
}
/**
* 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
return
}
val features = ArrayList()//stops.mapNotNull { stop ->
//stop.latitude?.let { lat ->
// stop.longitude?.let { lon ->
- for (pos in positionsByVehDict.values){
+ for (dat in updatesByVehDict.values){
//if (s.latitude!=null && s.longitude!=null)
+ val pos = dat.posUpdate
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)
}
)
)
/*busLabelSymbolsByVeh[pos.vehicle]?.let {
it.latLng = LatLng(pos.latitude, pos.longitude)
symbolsToUpdate.add(it)
}
*/
}
busesSource.setGeoJson(FeatureCollection.fromFeatures(features))
//update labels, clear cache to be used
//symbolManager.update(symbolsToUpdate)
//symbolsToUpdate.clear()
lastUpdateTime = System.currentTimeMillis()
}
override fun onResume() {
super.onResume()
Log.d(DEBUG_TAG, "Resetting paused from onResume")
mapView.onResume()
pausedFragment = false
val keySourcePositions = getString(R.string.pref_positions_source)
useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions, "mqtt").contentEquals("mqtt")
//separate paths
if(useMQTTPositions)
liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID))
else
liveBusViewModel.requestGTFSUpdates()
if(mapViewModel.currentLat.value!=MapViewModel.INVALID) {
Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+
" zoom ${mapViewModel.currentZoom.value}")
//THIS WAS A FIX FOR THE OLD OSMDROID MAP
/*val controller = map.controller
viewLifecycleOwner.lifecycleScope.launch {
delay(100)
Log.d(DEBUG_TAG, "zooming back to point")
controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!),
mapViewModel.currentZoom.value!!,null,null)
//controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!))
//controller.setZoom(mapViewModel.currentZoom.value!!)
}
*/
}
//initialize GUI here
fragmentListener.readyGUIfor(FragmentKind.LINES)
}
override fun onPause() {
super.onPause()
mapView.onPause()
if(useMQTTPositions) liveBusViewModel.stopMatoUpdates()
pausedFragment = true
//save map
val camera = map?.cameraPosition
camera?.let {cam->
mapViewModel.currentLat.value = cam.target?.latitude ?: -400.0
mapViewModel.currentLong.value = cam.target?.longitude ?: -400.0
mapViewModel.currentZoom.value = cam.zoom
}
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
+ override fun onDestroyView() {
+ super.onDestroyView()
+ Log.d(DEBUG_TAG, "Destroying the views")
+ mapStyle.removeLayer(STOPS_LAYER_ID)
+
+ mapStyle?.removeSource(STOPS_SOURCE_ID)
+
+ mapStyle.removeLayer(POLYLINE_LAYER)
+ mapStyle.removeSource(POLYLINE_SOURCE)
+ //stopsLayerStarted = false
+ }
+
override fun getBaseViewForSnackBar(): View? {
return null
}
companion object {
private const val LINEID_KEY="lineID"
private const val STOPS_SOURCE_ID = "stops-source"
private const val STOPS_LAYER_ID = "stops-layer"
private const val BUSES_SOURCE_ID = "buses-source"
private const val BUSES_LAYER_ID = "buses-layer"
private const val STOP_ACTIVE_IMG = "stop_active_img"
private const val STOP_IMAGE_ID = "stop-img"
private const val POLYLINE_LAYER = "polyline-layer"
private const val POLYLINE_SOURCE = "polyline-source"
- private const val DEBUG_TAG="BusTO-LinesDetailFragment"
+ private const val DEBUG_TAG="BusTO-LineDetailFragment"
fun makeArgs(lineID: String): Bundle{
val b = Bundle()
b.putString(LINEID_KEY, lineID)
return b
}
fun newInstance(lineID: String?) = LinesDetailFragment().apply {
lineID?.let { arguments = makeArgs(it) }
}
@JvmStatic
private fun findOptimalPosition(stop: Stop, pointsList: MutableList): LatLng{
if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty())
throw IllegalArgumentException()
val sLat = stop.latitude!!
val sLong = stop.longitude!!
if(pointsList.size < 2)
return pointsList[0]
pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) }
val p1 = pointsList[0]
val p2 = pointsList[1]
if (p1.longitude == p2.longitude){
//Log.e(DEBUG_TAG, "Same longitude")
return LatLng(sLat, p1.longitude)
} else if (p1.latitude == p2.latitude){
//Log.d(DEBUG_TAG, "Same latitude")
return LatLng(p2.latitude,sLong)
}
val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude)
val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude)
val cR = p1.latitude - p1.longitude * m
val longNew = (minv * sLong + sLat -cR ) / (m+minv)
val latNew = (m*longNew + cR)
//Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)")
return LatLng(latNew,longNew)
}
private const val DEFAULT_CENTER_LAT = 45.12
private const val DEFAULT_CENTER_LON = 7.6858
}
+
+ enum class BottomShowing{
+ STOP, VEHICLE
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt
index 5b17a87..d73e400 100644
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt
@@ -1,123 +1,132 @@
package it.reyboz.bustorino.viewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import it.reyboz.bustorino.backend.Result
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.data.GtfsRepository
import it.reyboz.bustorino.data.NextGenDB
import it.reyboz.bustorino.data.OldDataRepository
import it.reyboz.bustorino.data.gtfs.GtfsDatabase
import it.reyboz.bustorino.data.gtfs.GtfsRoute
import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops
import it.reyboz.bustorino.data.gtfs.PatternStop
+import org.maplibre.android.geometry.LatLng
import java.util.concurrent.Executors
class LinesViewModel(application: Application) : AndroidViewModel(application) {
private val gtfsRepo: GtfsRepository
private val oldRepo: OldDataRepository
//val patternsByRouteLiveData: LiveData>
private val routeIDToSearch = MutableLiveData()
private var lastShownPatternStops = ArrayList()
val currentPatternStops = MutableLiveData>()
val selectedPatternLiveData = MutableLiveData()
val stopsForPatternLiveData = MutableLiveData>()
private val executor = Executors.newFixedThreadPool(2)
val mapShowing = MutableLiveData(true)
fun setMapShowing(yes: Boolean){
mapShowing.value = yes
//retrigger redraw
stopsForPatternLiveData.postValue(stopsForPatternLiveData.value)
}
init {
val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao()
gtfsRepo = GtfsRepository(gtfsDao)
oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application))
}
val routesGTTLiveData: LiveData> by lazy{
gtfsRepo.getLinesLiveDataForFeed("gtt")
}
val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap {
gtfsRepo.getPatternsWithStopsForRouteID(it)
}
val gtfsRoute = routeIDToSearch.switchMap {
gtfsRepo.getRouteFromGtfsId(it)
}
fun setRouteIDQuery(routeID: String){
routeIDToSearch.value = routeID
}
fun getRouteIDQueried(): String?{
return routeIDToSearch.value
}
var shouldShowMessage = true
fun setPatternToDisplay(patternStops: MatoPatternWithStops){
selectedPatternLiveData.value = patternStops
}
/**
* Find the
*/
private fun requestStopsForGTFSIDs(gtfsIDs: List){
if (gtfsIDs.equals(lastShownPatternStops)){
//nothing to do
return
}
oldRepo.requestStopsWithGtfsIDs(gtfsIDs) {
if (it.isSuccess) {
stopsForPatternLiveData.postValue(it.result)
} else {
Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID")
it.exception?.printStackTrace()
}
}
lastShownPatternStops.clear()
for(id in gtfsIDs)
lastShownPatternStops.add(id)
}
fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){
val gtfsIDs = ArrayList()
for(pat in patternStops.stopsIndices){
gtfsIDs.add(pat.stopGtfsId)
}
requestStopsForGTFSIDs(gtfsIDs)
}
fun getStopByID(id:String) : Stop?{
//var stop : Stop? = null
val stop = stopsForPatternLiveData.value?.let { stops ->
for (s in stops){
if(s.ID == id)
return@let s
}
return@let null
}
return stop
}
+ private var lastMapPos: Pair? = null
+
+ fun saveMapPos(latLng: LatLng, zoom: Float){
+ lastMapPos = Pair(latLng, zoom)
+ }
+
+ fun getLastMapPos(): Pair? = lastMapPos
+
/*fun getLinesGTT(): MutableLiveData> {
val routesData = MutableLiveData>()
viewModelScope.launch {
val routes=gtfsRepo.getLinesForFeed("gtt")
routesData.postValue(routes)
}
return routesData
}*/
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt
index 9e5a744..72bf165 100644
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt
@@ -1,349 +1,348 @@
/*
BusTO - ViewModel components
Copyright (C) 2023 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.viewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.android.volley.DefaultRetryPolicy
import com.android.volley.Response
import it.reyboz.bustorino.backend.NetworkVolleyManager
import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest
import it.reyboz.bustorino.backend.gtfs.GtfsUtils
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
import it.reyboz.bustorino.backend.mato.MQTTMatoClient
import it.reyboz.bustorino.data.GtfsRepository
import it.reyboz.bustorino.data.MatoPatternsDownloadWorker
import it.reyboz.bustorino.data.MatoTripsDownloadWorker
import it.reyboz.bustorino.data.gtfs.MatoPattern
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet
typealias FullPositionUpdatesMap = HashMap>
typealias FullPositionUpdate = Pair
class LivePositionsViewModel(application: Application): AndroidViewModel(application) {
private val gtfsRepo = GtfsRepository(application)
//private val updates = UpdatesMap()
private val positionsToBeMatchedLiveData = MutableLiveData>()
private val netVolleyManager = NetworkVolleyManager.getInstance(application)
private var mqttClient = MQTTMatoClient()
private var lineListening = ""
private var lastTimeReceived: Long = 0
private val gtfsRtRequestRunning = MutableLiveData(false)
private val lastFailedTripsRequest = HashMap()
private val workManager = WorkManager.getInstance(application)
private var lastRequestedDownloadTrips = MutableLiveData>()
//INPUT FILTER FOR LINE
private var gtfsLineToFilterPos = MutableLiveData>()
fun setGtfsLineToFilterPos(line: String, pattern: MatoPattern?){
gtfsLineToFilterPos.value = Pair(line, pattern)
}
var isLastWorkResultGood = workManager
.getWorkInfosForUniqueWorkLiveData(MatoTripsDownloadWorker.TAG_TRIPS).map { it ->
if (it.isEmpty()) return@map false
var res = true
if(it[0].state == WorkInfo.State.FAILED){
val currDate = Date()
res = false
lastRequestedDownloadTrips.value?.let { trips->
for(tr in trips){
lastFailedTripsRequest[tr] = currDate
}
}
}
return@map res
}
/**
* Responder to the MQTT Client
*/
private val matoPositionListener = MQTTMatoClient.Companion.MQTTMatoListener{
val mupds = ArrayList()
if(lineListening==MQTTMatoClient.LINES_ALL){
for(sdic in it.values){
for(update in sdic.values){
mupds.add(update)
}
}
} else{
//we're listening to one
if (it.containsKey(lineListening.trim()) ){
for(up in it[lineListening]?.values!!){
mupds.add(up)
}
}
}
val time = System.currentTimeMillis()
if(lastTimeReceived == (0.toLong()) || (time-lastTimeReceived)>500){
positionsToBeMatchedLiveData.postValue(mupds)
lastTimeReceived = time
}
}
//find the trip IDs in the updates
private val tripsIDsInUpdates = positionsToBeMatchedLiveData.map { it ->
//Log.d(DEBUG_TI, "Updates map has keys ${upMap.keys}")
it.map { pos -> "gtt:"+pos.tripID }
}
// get the trip IDs in the DB
private val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap {
//Log.i(DEBUG_TI, "tripsIds in updates: ${it.size}")
gtfsRepo.gtfsDao.getTripPatternStops(it)
}
//trip IDs to query, which are not present in the DB
//REMEMBER TO OBSERVE THIS IN THE MAP
val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns ->
val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID }
Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB")
if (tripsIDsInUpdates.value!=null)
return@map tripsIDsInUpdates.value!!.filter { !(tripNames.contains(it) || it.contains("null"))}
else {
Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??")
return@map ArrayList()
}
}
// unify trips with updates
- // TODO: Unify trip data inside the LivePositionUpdate
val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns->
Log.i(DEBUG_TI, "Mapping trips and patterns")
- val mdict = HashMap>()
+ val mdict = HashMap()
//missing patterns
val routesToDownload = HashSet()
if(positionsToBeMatchedLiveData.value!=null)
for(update in positionsToBeMatchedLiveData.value!!){
val trID:String = update.tripID
var found = false
for(trip in tripPatterns){
if (trip.pattern == null){
//pattern is null, which means we have to download
// the pattern data from MaTO
routesToDownload.add(trip.trip.routeID)
}
if (trip.trip.tripID == "gtt:$trID"){
found = true
//insert directly
mdict[trID] = Pair(update,trip)
break
}
}
if (!found){
//Log.d(DEBUG_TI, "Cannot find pattern ${tr}")
//give the update anyway
mdict[trID] = Pair(update,null)
}
}
//have to request download of missing Patterns
if (routesToDownload.size > 0){
Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload")
//downloadMissingPatterns (ArrayList(routesToDownload))
MatoPatternsDownloadWorker.downloadPatternsForRoutes(routesToDownload.toList(), getApplication())
}
return@map mdict
}
//OBSERVE THIS TO GET THE LOCATION UPDATES FILTERED
val filteredLocationUpdates = MediatorLiveData()
init {
filteredLocationUpdates.addSource(updatesWithTripAndPatterns){
filteredLocationUpdates.value = filterUpdatesForGtfsLine(it, gtfsLineToFilterPos.value!!)
}
filteredLocationUpdates.addSource(gtfsLineToFilterPos){
updatesWithTripAndPatterns.value?.let{ ups-> filteredLocationUpdates.value = filterUpdatesForGtfsLine(ups, it)}
}
}
private fun filterUpdatesForGtfsLine(updates: FullPositionUpdatesMap,
linePatt: Pair):
HashMap{
val gtfsLineId = linePatt.first
val pattern = linePatt.second
val filtdLineID = GtfsUtils.stripGtfsPrefix(gtfsLineId)
//filter buses with direction, show those only with the same direction
val updsForTripId = HashMap>()
val directionId = pattern?.directionId ?: -100
val numUpds = updates.entries.size
Log.d(DEBUG_TI, "Got $numUpds updates, current pattern is: ${pattern?.name}, directionID: ${pattern?.directionId}")
// cannot understand where this is used
val patternsDirections = HashMap()
for((tripId, pair) in updates.entries){
//remove trips with wrong line ideas
val posUp = pair.first
if(pair.first.routeID!=filtdLineID)
continue
if(directionId!=-100 && pair.second!=null && pair.second?.pattern !=null){
val dir = pair.second?.pattern?.directionId
if(dir !=null && dir == directionId){
updsForTripId[tripId] = pair
}
patternsDirections[tripId] = dir ?: -10
} else{
updsForTripId[tripId] = pair
//Log.d(DEBUG_TAG, "No pattern for tripID: $tripId")
patternsDirections[tripId] = -10
}
}
Log.d(DEBUG_TI, " Filtered updates are ${updsForTripId.keys.size}") // Original updates directs: $patternsDirections\n
return updsForTripId
}
fun requestMatoPosUpdates(line: String){
lineListening = line
viewModelScope.launch {
mqttClient.startAndSubscribe(line,matoPositionListener, getApplication())
}
//updatePositions(1000)
}
fun stopMatoUpdates(){
viewModelScope.launch {
val tt = System.currentTimeMillis()
mqttClient.stopMatoRequests(matoPositionListener)
val time = System.currentTimeMillis() -tt
Log.d(DEBUG_TI, "Took $time ms to unsubscribe")
}
}
fun retriggerPositionUpdate(){
if(positionsToBeMatchedLiveData.value!=null){
positionsToBeMatchedLiveData.postValue(positionsToBeMatchedLiveData.value)
}
}
//Gtfs Real time
private val gtfsPositionsReqListener = object: GtfsRtPositionsRequest.Companion.RequestListener{
override fun onResponse(response: ArrayList?) {
Log.i(DEBUG_TI,"Got response from the GTFS RT server")
response?.let {it:ArrayList ->
if (it.size == 0) {
Log.w(DEBUG_TI,"No position updates from the GTFS RT server")
return
}
else {
//Log.i(DEBUG_TI, "Posting value to positionsLiveData")
viewModelScope.launch { positionsToBeMatchedLiveData.postValue(it) }
}
}
gtfsRtRequestRunning.postValue(false)
}
}
private val positionRequestErrorListener = Response.ErrorListener {
Log.e(DEBUG_TI, "Could not download the update", it)
gtfsRtRequestRunning.postValue(false)
}
fun requestGTFSUpdates(){
if(gtfsRtRequestRunning.value == null || !gtfsRtRequestRunning.value!!) {
val request = GtfsRtPositionsRequest(positionRequestErrorListener, gtfsPositionsReqListener)
request.setRetryPolicy(
DefaultRetryPolicy(1000,10,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)
)
netVolleyManager.requestQueue.add(request)
Log.i(DEBUG_TI, "Requested GTFS realtime position updates")
gtfsRtRequestRunning.value = true
}
}
fun requestDelayedGTFSUpdates(timems: Long){
viewModelScope.launch {
delay(timems)
requestGTFSUpdates()
}
}
override fun onCleared() {
//stop the MQTT Service
Log.d(DEBUG_TI, "Clearing the live positions view model, stopping the mqttClient")
mqttClient.disconnect()
super.onCleared()
}
//Request trips download
fun downloadTripsFromMato(trips: List): Boolean{
if(trips.isEmpty())
return false
var shouldContinue = false
val currentDateTime = Date().time
for (tr in trips){
if (!lastFailedTripsRequest.containsKey(tr)){
shouldContinue = true
break
} else{
//Log.i(DEBUG_TI, "Last time the trip has failed is ${lastFailedTripsRequest[tr]}")
if ((lastFailedTripsRequest[tr]!!.time - currentDateTime) > MAX_TIME_RETRY){
shouldContinue =true
break
}
}
}
if (shouldContinue) {
//if one trip
val workRequ =MatoTripsDownloadWorker.requestMatoTripsDownload(trips, getApplication(), "BusTO-MatoTripsDown")
workRequ?.let { req ->
Log.d(DEBUG_TI, "Enqueueing new work, saving work info")
lastRequestedDownloadTrips.postValue(trips)
//isLastWorkResultGood =
}
} else{
Log.w(DEBUG_TI, "Requested to fetch data for ${trips.size} trips but they all have failed before in the last $MAX_MINUTES_RETRY mins")
}
return shouldContinue
}
companion object{
private const val DEBUG_TI = "BusTO-LivePosViewModel"
private const val MAX_MINUTES_RETRY = 3
private const val MAX_TIME_RETRY = MAX_MINUTES_RETRY*60*1000 //3 minutes (in milliseconds)
}
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_magnifying_glass.xml b/app/src/main/res/drawable/ic_magnifying_glass.xml
new file mode 100644
index 0000000..96d592b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_magnifying_glass.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_lines_detail.xml b/app/src/main/res/layout/fragment_lines_detail.xml
index 0cc9ef2..9891e94 100644
--- a/app/src/main/res/layout/fragment_lines_detail.xml
+++ b/app/src/main/res/layout/fragment_lines_detail.xml
@@ -1,278 +1,280 @@
+
+ android:backgroundTint="?colorAccent">
\ No newline at end of file