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 98cf68c..ae9d5d9 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
@@ -1,906 +1,907 @@
/*
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.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Paint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.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.MatoPattern
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 kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.FolderOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList
class LinesDetailFragment() : ScreenBaseFragment() {
private var lineID = ""
private lateinit var patternsSpinner: Spinner
private var patternsAdapter: ArrayAdapter? = null
//private var patternsSpinnerState: Parcelable? = null
private lateinit var currentPatterns: List
private lateinit var map: MapView
private var viewingPattern: 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 var polyline: Polyline? = null
//private var stopPosList = ArrayList()
private lateinit var stopsOverlay: FolderOverlay
private lateinit var locationOverlay: LocationOverlay
private val locationOverlayResponder = object : LocationOverlay.OverlayCallbacks{
override fun onDisableFollowMyLocation() {
Log.d(DEBUG_TAG, "Follow location disabled")
}
override fun onEnableFollowMyLocation() {
Log.d(DEBUG_TAG, "Follow location enabled")
}
}
//location request responder
private val locationRequestResLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()){ res ->
//onActivityResult(res: map)
if(res[Permissions.LOCATION_PERMISSIONS[0]] ==true || res[Permissions.LOCATION_PERMISSIONS[1]] ==true)
locationIcon?.let { onPositionIconButtonClick(it) }
else{
context?.let { Toast.makeText(it,R.string.location_permission_not_granted, Toast.LENGTH_SHORT).show() }
}
}
//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 = true
private var recyclerInitDone = false
private var useMQTTPositions = true
//position of live markers
private val busPositionMarkersByTrip = HashMap()
private var busPositionsOverlay = FolderOverlay()
private val tripMarkersAnimators = HashMap()
private val liveBusViewModel: LivePositionsViewModel by viewModels()
@SuppressLint("SetTextI18n")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false)
lineID = requireArguments().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
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
initializeMap(rootView)
initializeRecyclerView()
switchButton.setOnClickListener{
if(map.visibility == View.VISIBLE){
map.visibility = View.GONE
stopsRecyclerView.visibility = View.VISIBLE
locationIcon?.visibility = View.GONE
viewModel.setMapShowing(false)
liveBusViewModel.stopMatoUpdates()
//map.overlayManager.remove(busPositionsOverlay)
switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30))
} else{
stopsRecyclerView.visibility = View.GONE
map.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))
}
}
locationIcon?.let {view ->
if(!LocationUtils.isLocationEnabled(requireContext()) || !Permissions.anyLocationPermissionsGranted(requireContext()))
setLocationIconEnabled(false)
//set click Listener
view.setOnClickListener(this::onPositionIconButtonClick)
}
//set
viewModel.setRouteIDQuery(lineID)
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(map.visibility ==View.VISIBLE)
showPatternWithStopsOnMap(stops)
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
}
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 patternWithStops = currentPatterns.get(position)
//viewModel.setPatternToDisplay(patternWithStops)
setPatternAndReqStops(patternWithStops)
Log.d(DEBUG_TAG, "item Selected, cleaning bus markers")
if(map?.visibility == View.VISIBLE) {
busPositionsOverlay.closeAllInfoWindows()
busPositionsOverlay.items.clear()
busPositionMarkersByTrip.clear()
stopAnimations()
tripMarkersAnimators.clear()
liveBusViewModel.retriggerPositionUpdate()
}
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
//live bus positions
liveBusViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){
if(map.visibility == View.GONE || viewingPattern ==null){
//DO NOTHING
return@observe
}
val filtdLineID = GtfsUtils.stripGtfsPrefix(lineID)
//filter buses with direction, show those only with the same direction
val outmap = HashMap>()
val currentPattern = viewingPattern!!.pattern
val numUpds = it.entries.size
Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}")
val patternsDirections = HashMap()
for((tripId, pair) in it.entries){
//remove trips with wrong line ideas
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){
outmap[tripId] = pair
}
patternsDirections.set(tripId,if (dir!=null) dir else -10)
} else{
outmap[tripId] = pair
//Log.d(DEBUG_TAG, "No pattern for tripID: $tripId")
patternsDirections[tripId] = -10
}
}
Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n
updateBusPositionsInMap(outmap)
//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"
)
}
return rootView
}
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))
}
/**
* Switch position icon from activ
*/
private fun onPositionIconButtonClick(view: View){
if(locationOverlay.isMyLocationEnabled){
//switch off
locationOverlay.disableMyLocation()
//set image on respective button
setLocationIconEnabled(false)
if(context!=null) {
if (LocationUtils.isLocationEnabled(context)) {
//show message
Toast.makeText(context, R.string.location_disabled, Toast.LENGTH_SHORT).show()
}
}
} else{
//switch on
locationOverlay.enableMyLocation()
if(context!=null) {
if(!Permissions.anyLocationPermissionsGranted(context)) {
locationRequestResLauncher.launch(Permissions.LOCATION_PERMISSIONS)
Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show()
}
else if (LocationUtils.isLocationEnabled(context)) {
//set image on button
setLocationIconEnabled(true)
//show message
Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show()
} else{
Toast.makeText(context, R.string.map_location_disabled_device, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun initializeMap(rootView : View){
val ctx = requireContext().applicationContext
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx))
map = rootView.findViewById(R.id.lineMap)
map.let {
it.setTileSource(TileSourceFactory.MAPNIK)
locationOverlay = LocationOverlay.createLocationOverlay(true, it, requireContext(), locationOverlayResponder)
locationOverlay.disableFollowLocation()
stopsOverlay = FolderOverlay()
busPositionsOverlay = FolderOverlay()
//map.setTilesScaledToDpi(true);
//map.setTilesScaledToDpi(true);
it.setFlingEnabled(true)
it.setUseDataConnection(true)
// add ability to zoom with 2 fingers
it.setMultiTouchControls(true)
it.minZoomLevel = 12.0
//map controller setup
val mapController = it.controller
var zoom = 12.0
var centerMap = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)
if(mapViewModel.currentLat.value!=MapViewModel.INVALID) {
Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+
" zoom ${mapViewModel.currentZoom.value}")
zoom = mapViewModel.currentZoom.value!!
centerMap = GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)
/*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!!)
*/
}
mapController.setZoom(zoom)
mapController.setCenter(centerMap)
Log.d(DEBUG_TAG, "Initializing map, first init $firstInit")
//map.invalidate()
it.overlayManager.add(stopsOverlay)
it.overlayManager.add(locationOverlay)
it.overlayManager.add(busPositionsOverlay)
zoomToCurrentPattern()
firstInit = false
}
}
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 tripMarkersAnimators.values){
anim.cancel()
}
}
private fun savePatternsToShow(patterns: List){
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)
}
currentPatterns = patterns.sortedWith(patternsSorter)
patternsAdapter?.let {
it.clear()
it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" })
it.notifyDataSetChanged()
}
viewingPattern?.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 }
viewingPattern = 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(){
var pointsList: List
if(viewingPattern==null) {
Log.e(DEBUG_TAG, "asked to zoom to pattern but current viewing pattern is null")
if(polyline!=null)
pointsList = polyline!!.actualPoints
else {
Log.d(DEBUG_TAG, "The polyline is null")
return
}
}else{
val pattern = viewingPattern!!.pattern
pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength)
}
var maxLat = -4000.0
var minLat = -4000.0
var minLong = -4000.0
var maxLong = -4000.0
for (p in pointsList){
// get max latitude
if(maxLat == -4000.0)
maxLat = p.latitude
else if (maxLat < p.latitude) maxLat = p.latitude
// find min latitude
if (minLat == -4000.0)
minLat = p.latitude
else if (minLat > p.latitude) minLat = p.latitude
if(maxLong == -4000.0 || maxLong < p.longitude )
maxLong = p.longitude
if (minLong == -4000.0 || minLong > p.longitude)
minLong = p.longitude
}
val del = 0.008
//map.controller.c
Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong")
map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false)
}
private fun showPatternWithStopsOnMap(stops: List){
Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}")
if(viewingPattern==null || map == null) return
val pattern = viewingPattern!!.pattern
val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength)
var maxLat = -4000.0
var minLat = -4000.0
var minLong = -4000.0
var maxLong = -4000.0
for (p in pointsList){
// get max latitude
if(maxLat == -4000.0)
maxLat = p.latitude
else if (maxLat < p.latitude) maxLat = p.latitude
// find min latitude
if (minLat == -4000.0)
minLat = p.latitude
else if (minLat > p.latitude) minLat = p.latitude
if(maxLong == -4000.0 || maxLong < p.longitude )
maxLong = p.longitude
if (minLong == -4000.0 || minLong > p.longitude)
minLong = p.longitude
}
//val polyLine=Polyline(map)
//polyLine.setPoints(pointsList)
//save points
if(map.overlayManager.contains(polyline)){
map.overlayManager.remove(polyline)
}
polyline = Polyline(map, false)
polyline!!.setPoints(pointsList)
//polyline.color = ContextCompat.getColor(context!!,R.color.brown_vd)
polyline!!.infoWindow = null
val paint = Paint()
paint.color = ContextCompat.getColor(requireContext(),R.color.line_drawn_poly)
paint.isAntiAlias = true
paint.strokeWidth = 13f
paint.style = Paint.Style.FILL_AND_STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeCap = Paint.Cap.ROUND
polyline!!.outlinePaintLists.add(MonochromaticPaintList(paint))
map.overlayManager.add(0,polyline!!)
stopsOverlay.closeAllInfoWindows()
stopsOverlay.items.clear()
val stopIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ball)
for(s in stops){
val gp = if (showOnTopOfLine)
findOptimalPosition(s,pointsList)
else GeoPoint(s.latitude!!,s.longitude!!)
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)
//}
polyline!!.setOnClickListener(Polyline.OnClickListener { polyline, mapView, eventPos ->
Log.d(DEBUG_TAG, "clicked")
true
})
//map.controller.zoomToB//#animateTo(pointsList[0])
val del = 0.008
map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), true)
//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
)
}
}
/**
* Remove bus marker from overlay associated with tripID
*/
private fun removeBusMarker(tripID: String){
if(!busPositionMarkersByTrip.containsKey(tripID)){
Log.e(DEBUG_TAG, "Asked to remove veh with tripID $tripID but it's supposedly not shown")
return
}
val marker = busPositionMarkersByTrip[tripID]
busPositionsOverlay.remove(marker)
busPositionMarkersByTrip.remove(tripID)
val animator = tripMarkersAnimators[tripID]
animator?.let{
it.cancel()
tripMarkersAnimators.remove(tripID)
}
}
private fun showPatternWithStop(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
}
}
}
/**
* draw the position of the buses in the map. Copied from MapFragment
*/
private fun updateBusPositionsInMap(tripsPatterns: java.util.HashMap>
) {
//Log.d(MapFragment.DEBUG_TAG, "Updating positions of the buses")
//if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
// cleanup the patterns
// at first run, the buses which have no direction are still displayed. If those become missing in the data,
// it becomes clear that they don't have the same direction
val currentBusesTripsIds = HashSet(busPositionMarkersByTrip.keys)
for (tripID in currentBusesTripsIds){
if (!tripsPatterns.keys.contains(tripID)){
//the tripId is not in the updates anymore, remove it
removeBusMarker(tripID)
}
}
val noPatternsTrips = ArrayList()
for (tripID in tripsPatterns.keys) {
val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue
var marker: Marker? = null
//check if Marker is already created
if (busPositionMarkersByTrip.containsKey(tripID)) {
//check if the trip direction ID is the same, if not remove
if(tripWithPatternStops?.pattern != null &&
tripWithPatternStops.pattern.directionId != viewingPattern?.pattern?.directionId){
removeBusMarker(tripID)
} else {
//need to change the position of the marker
marker = busPositionMarkersByTrip.get(tripID)!!
BusPositionUtils.updateBusPositionMarker(map, marker, update, tripMarkersAnimators, false)
// Set the pattern to add the info
if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) {
val window = marker.infoWindow as BusInfoWindow
if (window.pattern == null && tripWithPatternStops != null) {
//Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID);
window.setPatternAndDraw(tripWithPatternStops.pattern)
}
}
}
} else {
//marker is not there, need to make it
//if (mapView == null) Log.e(MapFragment.DEBUG_TAG, "Creating marker with null map, things will explode")
marker = Marker(map)
//String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID());
val mdraw = ResourcesCompat.getDrawable(getResources(), R.drawable.map_bus_position_icon, null)!!
//mdraw.setBounds(0,0,28,28);
marker.icon = mdraw
var markerPattern: MatoPattern? = null
if (tripWithPatternStops != null) {
if (tripWithPatternStops.pattern != null)
markerPattern = tripWithPatternStops.pattern
}
marker.infoWindow = BusInfoWindow(map, update, markerPattern, true) {
// set pattern to show
if(it!=null)
showPatternWithStop(it.code)
}
//marker.infoWindow as BusInfoWindow
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
+ marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
BusPositionUtils.updateBusPositionMarker(map,marker, update, tripMarkersAnimators,true)
// the overlay is null when it's not attached yet?
// cannot recreate it because it becomes null very soon
// if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
//save the marker
if (busPositionsOverlay != null) {
busPositionsOverlay.add(marker)
busPositionMarkersByTrip.put(tripID, marker)
}
}
}
if (noPatternsTrips.size > 0) {
Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips")
}
}
override fun onResume() {
super.onResume()
Log.d(DEBUG_TAG, "Resetting paused from 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}")
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!!)
}
//controller.setZoom()
}
//initialize GUI here
fragmentListener.readyGUIfor(FragmentKind.LINES)
}
override fun onPause() {
super.onPause()
liveBusViewModel.stopMatoUpdates()
pausedFragment = true
//save map
val center = map.mapCenter
mapViewModel.currentLat.value = center.latitude
mapViewModel.currentLong.value = center.longitude
mapViewModel.currentZoom.value = map.zoomLevel.toDouble()
}
override fun getBaseViewForSnackBar(): View? {
return null
}
companion object {
private const val LINEID_KEY="lineID"
fun newInstance() = LinesDetailFragment()
const val DEBUG_TAG="LinesDetailFragment"
fun makeArgs(lineID: String): Bundle{
val b = Bundle()
b.putString(LINEID_KEY, lineID)
return b
}
@JvmStatic
private fun findOptimalPosition(stop: Stop, pointsList: MutableList): GeoPoint{
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 GeoPoint(sLat, p1.longitude)
} else if (p1.latitude == p2.latitude){
//Log.d(DEBUG_TAG, "Same latitude")
return GeoPoint(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 GeoPoint(latNew,longNew)
}
private const val DEFAULT_CENTER_LAT = 45.12
private const val DEFAULT_CENTER_LON = 7.6858
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java
index 6e3992e..fddea79 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java
@@ -1,836 +1,837 @@
/*
BusTO - Fragments components
Copyright (C) 2020 Andrea Ugo
Copyright (C) 2021 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.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate;
import it.reyboz.bustorino.backend.mato.MQTTMatoClient;
import it.reyboz.bustorino.backend.utils;
import it.reyboz.bustorino.data.gtfs.MatoPattern;
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops;
import it.reyboz.bustorino.map.*;
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel;
import it.reyboz.bustorino.viewmodels.StopsMapViewModel;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
import org.osmdroid.events.DelayedMapListener;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.FolderOverlay;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.infowindow.InfoWindow;
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider;
import java.util.*;
import kotlin.Pair;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.middleware.GeneralActivity;
import it.reyboz.bustorino.util.Permissions;
import static it.reyboz.bustorino.fragments.SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE;
public class MapFragment extends ScreenBaseFragment {
//private static final String TAG = "Busto-MapActivity";
private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom";
private static final String MAP_CENTER_LAT_KEY = "map-center-lat";
private static final String MAP_CENTER_LON_KEY = "map-center-lon";
private static final String FOLLOWING_LOCAT_KEY ="following";
public static final String BUNDLE_LATIT = "lat";
public static final String BUNDLE_LONGIT = "lon";
public static final String BUNDLE_NAME = "name";
public static final String BUNDLE_ID = "ID";
public static final String BUNDLE_ROUTES_STOPPING = "routesStopping";
public static final String FRAGMENT_TAG="BusTOMapFragment";
private static final double DEFAULT_CENTER_LAT = 45.0708;
private static final double DEFAULT_CENTER_LON = 7.6858;
private static final double POSITION_FOUND_ZOOM = 18.3;
public static final double NO_POSITION_ZOOM = 17.1;
private static final String DEBUG_TAG=FRAGMENT_TAG;
protected FragmentListenerMain listenerMain;
private HashSet shownStops = null;
private MapView map = null;
public Context ctx;
private LocationOverlay mLocationOverlay = null;
private FolderOverlay stopsFolderOverlay = null;
private Bundle savedMapState = null;
protected ImageButton btCenterMap;
protected ImageButton btFollowMe;
protected CoordinatorLayout coordLayout;
private boolean hasMapStartFinished = false;
private boolean followingLocation = false;
//the ViewModel from which we get the stop to display in the map
private StopsMapViewModel stopsViewModel;
//private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class);
private LivePositionsViewModel livePositionsViewModel;
private Boolean useMQTTViewModel = true;
private final HashMap busPositionMarkersByTrip = new HashMap<>();
private FolderOverlay busPositionsOverlay = null;
private final HashMap tripMarkersAnimators = new HashMap<>();
protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() {
@Override
public void onActionUp(@NonNull String stopID, @Nullable String stopName) {
if (listenerMain!= null){
Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: "+stopID);
listenerMain.requestArrivalsForStopID(stopID);
}
}
};
protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() {
@Override
public void onDisableFollowMyLocation() {
updateGUIForLocationFollowing(false);
followingLocation=false;
}
@Override
public void onEnableFollowMyLocation() {
updateGUIForLocationFollowing(true);
followingLocation=true;
}
};
private final ActivityResultLauncher positionRequestLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
if (result == null){
Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?");
}
else if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) &&
Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){
map.getOverlays().remove(mLocationOverlay);
startLocationOverlay(true, map);
if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null)
return;
LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
@SuppressLint("MissingPermission")
Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (userLocation != null) {
map.getController().setZoom(POSITION_FOUND_ZOOM);
GeoPoint startPoint = new GeoPoint(userLocation);
setLocationFollowing(true);
map.getController().setCenter(startPoint);
}
}
else Log.w(DEBUG_TAG,"No location permission");
});
public MapFragment() {
}
public static MapFragment getInstance(){
return new MapFragment();
}
public static MapFragment getInstance(@NonNull Stop stop){
MapFragment fragment= new MapFragment();
Bundle args = new Bundle();
args.putDouble(BUNDLE_LATIT, stop.getLatitude());
args.putDouble(BUNDLE_LONGIT, stop.getLongitude());
args.putString(BUNDLE_NAME, stop.getStopDisplayName());
args.putString(BUNDLE_ID, stop.ID);
args.putString(BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString());
fragment.setArguments(args);
return fragment;
}
//public static MapFragment getInstance(@NonNull Stop stop){
// return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID);
//}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//use the same layout as the activity
View root = inflater.inflate(R.layout.fragment_map, container, false);
if (getContext() == null){
throw new IllegalStateException();
}
ctx = getContext().getApplicationContext();
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
map = root.findViewById(R.id.map);
map.setTileSource(TileSourceFactory.MAPNIK);
//map.setTilesScaledToDpi(true);
map.setFlingEnabled(true);
// add ability to zoom with 2 fingers
map.setMultiTouchControls(true);
btCenterMap = root.findViewById(R.id.icon_center_map);
btFollowMe = root.findViewById(R.id.icon_follow);
coordLayout = root.findViewById(R.id.coord_layout);
//setup FolderOverlay
stopsFolderOverlay = new FolderOverlay();
//setup Bus Markers Overlay
busPositionsOverlay = new FolderOverlay();
//reset shown bus updates
busPositionMarkersByTrip.clear();
tripMarkersAnimators.clear();
//set map not done
hasMapStartFinished = false;
String keySourcePositions=getString(R.string.pref_positions_source);
useMQTTViewModel = (
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions,LIVE_POSITIONS_PREF_MQTT_VALUE).contentEquals(LIVE_POSITIONS_PREF_MQTT_VALUE));
//Start map from bundle
if (savedInstanceState !=null)
startMap(getArguments(), savedInstanceState);
else startMap(getArguments(), savedMapState);
//set listeners
map.addMapListener(new DelayedMapListener(new MapListener() {
@Override
public boolean onScroll(ScrollEvent paramScrollEvent) {
requestStopsToShow();
//Log.d(DEBUG_TAG, "Scrolling");
//if (moveTriggeredByCode) moveTriggeredByCode =false;
//else setLocationFollowing(false);
return true;
}
@Override
public boolean onZoom(ZoomEvent event) {
requestStopsToShow();
return true;
}
}));
btCenterMap.setOnClickListener(v -> {
//Log.i(TAG, "centerMap clicked ");
if(Permissions.bothLocationPermissionsGranted(getContext())) {
final GeoPoint myPosition = mLocationOverlay.getMyLocation();
map.getController().animateTo(myPosition);
} else
Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT)
.show();
});
btFollowMe.setOnClickListener(v -> {
//Log.i(TAG, "btFollowMe clicked ");
if(Permissions.bothLocationPermissionsGranted(getContext()))
setLocationFollowing(!followingLocation);
else
Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT)
.show();
});
return root;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
ViewModelProvider provider = new ViewModelProvider(this);
//gtfsPosViewModel = provider.get(GtfsPositionsViewModel.class);
livePositionsViewModel = provider.get(LivePositionsViewModel.class);
stopsViewModel = provider.get(StopsMapViewModel.class);
if (context instanceof FragmentListenerMain) {
listenerMain = (FragmentListenerMain) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement FragmentListenerMain");
}
}
@Override
public void onDetach() {
super.onDetach();
listenerMain = null;
//stop animations
// setupOnAttached = true;
Log.w(DEBUG_TAG, "Fragment detached");
}
@Override
public void onPause() {
super.onPause();
Log.w(DEBUG_TAG, "On pause called mapfrag");
saveMapState();
for (ObjectAnimator animator : tripMarkersAnimators.values()) {
if(animator!=null && animator.isRunning()){
animator.cancel();
}
}
tripMarkersAnimators.clear();
if(useMQTTViewModel) livePositionsViewModel.stopMatoUpdates();
}
/**
* Save the map state inside the fragment
* (calls saveMapState(bundle))
*/
private void saveMapState(){
savedMapState = new Bundle();
saveMapState(savedMapState);
}
/**
* Save the state of the map to restore it to a later time
* @param bundle the bundle in which to save the data
*/
private void saveMapState(Bundle bundle){
Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation);
bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation);
if (map == null){
//The map is null, it can happen?
Log.e(DEBUG_TAG, "Cannot save map center, map is null");
return;
}
final IGeoPoint loc = map.getMapCenter();
bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude());
bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude());
bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble());
}
@Override
public void onResume() {
super.onResume();
//TODO: cleanup duplicate code (maybe merging the positions classes?)
if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP);
/// choose which to use
String keySourcePositions=getString(R.string.pref_positions_source);
useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions,LIVE_POSITIONS_PREF_MQTT_VALUE).contentEquals(
LIVE_POSITIONS_PREF_MQTT_VALUE);
if(livePositionsViewModel !=null) {
//gtfsPosViewModel.requestUpdates();
if(useMQTTViewModel)
livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL);
else
livePositionsViewModel.requestGTFSUpdates();
//mapViewModel.testCascade();
livePositionsViewModel.isLastWorkResultGood().observe(this, d ->
Log.d(DEBUG_TAG, "Last trip download result is "+d));
livePositionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> {
Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat);
livePositionsViewModel.downloadTripsFromMato(dat);
/*MatoTripsDownloadWorker.Companion.requestMatoTripsDownload(dat,
requireContext().getApplicationContext(),
"BusTO-MatoTripDownload");
*/
});
} /*else if(gtfsPosViewModel!=null){
gtfsPosViewModel.requestUpdates();
gtfsPosViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> {
Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat);
//gtfsPosViewModel.downloadTripsFromMato(dat);
MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(),
"BusTO-MatoTripDownload");
});
}
*/
else Log.e(DEBUG_TAG, "livePositionsViewModel is null at onResume");
//rerequest stop
stopsViewModel.requestStopsInBoundingBox(map.getBoundingBox());
}
private void startRequestsPositions(){
if (livePositionsViewModel != null) {
//should always be the case
livePositionsViewModel.getUpdatesWithTripAndPatterns().observe(getViewLifecycleOwner(), data -> {
Log.d(DEBUG_TAG, "Have " + data.size() + " trip updates, has Map start finished: " + hasMapStartFinished);
if (hasMapStartFinished) updateBusPositionsInMap(data);
if(!isDetached() && !useMQTTViewModel)
livePositionsViewModel.requestDelayedGTFSUpdates(3000);
});
} else {
Log.e(DEBUG_TAG, "PositionsViewModel is null");
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
saveMapState(outState);
super.onSaveInstanceState(outState);
}
//own methods
/**
* Switch following the location on and off
* @param value true if we want to follow location
*/
public void setLocationFollowing(Boolean value){
followingLocation = value;
if(mLocationOverlay==null || getContext() == null || map ==null)
//nothing else to do
return;
if (value){
mLocationOverlay.enableFollowLocation();
} else {
mLocationOverlay.disableFollowLocation();
}
}
/**
* Do all the stuff you need to do on the gui, when parameter is changed to value
* @param following value
*/
protected void updateGUIForLocationFollowing(boolean following){
if (following)
btFollowMe.setImageResource(R.drawable.ic_follow_me_on);
else
btFollowMe.setImageResource(R.drawable.ic_follow_me);
}
/**
* Build the location overlay. Enable only when
* a) we know we have the permission
* b) the location map is set
*/
private void startLocationOverlay(boolean enableLocation, MapView map){
if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now");
// Location Overlay
// from OpenBikeSharing (THANK GOD)
Log.d(DEBUG_TAG, "Starting position overlay");
GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext());
imlp.setLocationUpdateMinDistance(5);
imlp.setLocationUpdateMinTime(2000);
final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks);
if (enableLocation) overlay.enableMyLocation();
overlay.setOptionsMenuEnabled(true);
//map.getOverlays().add(this.mLocationOverlay);
this.mLocationOverlay = overlay;
map.getOverlays().add(mLocationOverlay);
}
public void startMap(Bundle incoming, Bundle savedInstanceState) {
//Check that we're attached
GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null;
if(getContext()==null|| activity==null){
//we are not attached
Log.e(DEBUG_TAG, "Calling startMap when not attached");
return;
}else{
Log.d(DEBUG_TAG, "Starting map from scratch");
}
//clear previous overlays
map.getOverlays().clear();
//parse incoming bundle
GeoPoint marker = null;
String name = null;
String ID = null;
String routesStopping = "";
if (incoming != null) {
double lat = incoming.getDouble(BUNDLE_LATIT);
double lon = incoming.getDouble(BUNDLE_LONGIT);
marker = new GeoPoint(lat, lon);
name = incoming.getString(BUNDLE_NAME);
ID = incoming.getString(BUNDLE_ID);
routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, "");
}
//ask for location permission
if(!Permissions.bothLocationPermissionsGranted(activity)){
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();
}
positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS);
}
shownStops = new HashSet<>();
// move the map on the marker position or on a default view point: Turin, Piazza Castello
// and set the start zoom
IMapController mapController = map.getController();
GeoPoint startPoint = null;
startLocationOverlay(Permissions.bothLocationPermissionsGranted(activity),
map);
// set the center point
if (marker != null) {
//startPoint = marker;
mapController.setZoom(POSITION_FOUND_ZOOM);
setLocationFollowing(false);
// put the center a little bit off (animate later)
startPoint = new GeoPoint(marker);
startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20));
startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20));
//don't need to do all the rest since we want to show a point
} else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) {
mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY));
mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY),
savedInstanceState.getDouble(MAP_CENTER_LON_KEY)));
Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY));
setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY));
} else {
Log.d(DEBUG_TAG, "No position found from intent or saved state");
boolean found = false;
LocationManager locationManager =
(LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
//check for permission
if (locationManager != null && Permissions.bothLocationPermissionsGranted(activity)) {
@SuppressLint("MissingPermission")
Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (userLocation != null) {
double distan = utils.measuredistanceBetween(userLocation.getLatitude(), userLocation.getLongitude(),
DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON);
if (distan < 100_000.0) {
mapController.setZoom(POSITION_FOUND_ZOOM);
startPoint = new GeoPoint(userLocation);
found = true;
setLocationFollowing(true);
}
}
}
if(!found){
startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON);
mapController.setZoom(NO_POSITION_ZOOM);
setLocationFollowing(false);
}
}
// set the minimum zoom level
map.setMinZoomLevel(15.0);
//add contingency check (shouldn't happen..., but)
if (startPoint != null) {
mapController.setCenter(startPoint);
}
//add stops overlay
//map.getOverlays().add(mLocationOverlay);
map.getOverlays().add(this.stopsFolderOverlay);
Log.d(DEBUG_TAG, "Requesting stops load");
// This is not necessary, by setting the center we already move
// the map and we trigger a stop request
//requestStopsToShow();
if (marker != null) {
// make a marker with the info window open for the searched marker
//TODO: make Stop Bundle-able
Marker stopMarker = makeMarker(marker, ID , name, routesStopping,true);
map.getController().animateTo(marker);
}
//add the overlays with the bus stops
if(busPositionsOverlay == null){
//Log.i(DEBUG_TAG, "Null bus positions overlay,redo");
busPositionsOverlay = new FolderOverlay();
}
startRequestsPositions();
if(stopsViewModel !=null){
stopsViewModel.getStopsInBoundingBox().observe(getViewLifecycleOwner(),
this::showStopsMarkers
);
} else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null");
map.getOverlays().add(this.busPositionsOverlay);
//set map as started
hasMapStartFinished = true;
}
/**
* Start a request to load the stops that are in the current view
* from the database
*/
private void requestStopsToShow(){
// get the top, bottom, left and right screen's coordinate
BoundingBox bb = map.getBoundingBox();
Log.d(DEBUG_TAG, "Requesting stops in bounding box, stopViewModel is null "+(stopsViewModel==null));
if(stopsViewModel!=null){
stopsViewModel.requestStopsInBoundingBox(bb);
}
/*double latFrom = bb.getLatSouth();
double latTo = bb.getLatNorth();
double lngFrom = bb.getLonWest();
double lngTo = bb.getLonEast();
if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED)
stopFetcher.cancel(true);
stopFetcher = new AsyncStopFetcher(this);
stopFetcher.execute(
new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo));
*/
}
private void updateBusMarker(final Marker marker, final LivePositionUpdate posUpdate, @Nullable boolean justCreated){
GeoPoint position;
final String updateID = posUpdate.getTripID();
if(!justCreated){
position = marker.getPosition();
if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){
GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude());
ObjectAnimator valueAnimator = MarkerUtils.makeMarkerAnimator(
map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200);
valueAnimator.setAutoCancel(true);
tripMarkersAnimators.put(updateID,valueAnimator);
valueAnimator.start();
}
//marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()));
} else {
position = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude());
marker.setPosition(position);
}
if(posUpdate.getBearing()!=null)
marker.setRotation(posUpdate.getBearing()*(-1.f));
}
private void updateBusPositionsInMap(HashMap> tripsPatterns){
Log.d(DEBUG_TAG, "Updating positions of the buses");
//if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
final ArrayList noPatternsTrips = new ArrayList<>();
for(String tripID: tripsPatterns.keySet()) {
final Pair pair = tripsPatterns.get(tripID);
if (pair == null) continue;
final LivePositionUpdate update = pair.getFirst();
final TripAndPatternWithStops tripWithPatternStops = pair.getSecond();
//check if Marker is already created
if (busPositionMarkersByTrip.containsKey(tripID)){
//need to change the position of the marker
final Marker marker = busPositionMarkersByTrip.get(tripID);
assert marker!=null;
updateBusMarker(marker, update, false);
if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){
BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow();
if(tripWithPatternStops != null) {
//Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID);
window.setPatternAndDraw(tripWithPatternStops.getPattern());
}
}
} else{
//marker is not there, need to make it
if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode");
final Marker marker = new Marker(map);
/*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources(
getResources(),
R.drawable.point_heading_icon,
R.dimen.map_icons_size, R.dimen.map_icons_size);
*/
//String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID());
final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.map_bus_position_icon, null);
/*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(),
R.drawable.point_heading_icon,
R.color.white,
route,12);
*/
assert mdraw != null;
//mdraw.setBounds(0,0,28,28);
marker.setIcon(mdraw);
if(tripWithPatternStops == null){
noPatternsTrips.add(tripID);
}
MatoPattern markerPattern = null;
if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null)
markerPattern = tripWithPatternStops.getPattern();
marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , false, (pattern) -> { }));
+ marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
updateBusMarker(marker, update, true);
// the overlay is null when it's not attached yet?5
// cannot recreate it because it becomes null very soon
// if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
//save the marker
if(busPositionsOverlay!=null) {
busPositionsOverlay.add(marker);
busPositionMarkersByTrip.put(tripID, marker);
}
}
}
if(noPatternsTrips.size()>0){
Log.i(DEBUG_TAG, "These trips have no matching pattern: "+noPatternsTrips);
}
}
/**
* Add stops as Markers on the map
* @param stops the list of stops that must be included
*/
protected void showStopsMarkers(List stops){
if (getContext() == null || stops == null){
//we are not attached
return;
}
boolean good = true;
for (Stop stop : stops) {
if (shownStops.contains(stop.ID)){
continue;
}
if(stop.getLongitude()==null || stop.getLatitude()==null)
continue;
shownStops.add(stop.ID);
if(!map.isShown()){
if(good)
Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already");
good = false;
continue;
} else if(map.getRepository() == null){
Log.e(DEBUG_TAG, "Map view repository is null");
}
GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude());
Marker stopMarker = makeMarker(marker, stop, false);
stopsFolderOverlay.add(stopMarker);
if (!map.getOverlays().contains(stopsFolderOverlay)) {
Log.w(DEBUG_TAG, "Map doesn't have folder overlay");
}
good=true;
}
//Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay");
//force redraw of markers
map.invalidate();
}
public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){
return makeMarker(geoPoint,stop.ID,
stop.getStopDefaultName(),
stop.routesThatStopHereToString(), isStartMarker);
}
public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName,
String routesStopping, boolean isStartMarker) {
// add a marker
final Marker marker = new Marker(map);
// set custom info window as info window
CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping,
responder, R.layout.linedetail_stop_infowindow, R.color.red_darker);
marker.setInfoWindow(popup);
// make the marker clickable
marker.setOnMarkerClickListener((thisMarker, mapView) -> {
if (thisMarker.isInfoWindowOpen()) {
// on second click
Log.w(DEBUG_TAG, "Pressed on the click marker");
} else {
// on first click
// hide all opened info window
InfoWindow.closeAllInfoWindowsOn(map);
// show this particular info window
thisMarker.showInfoWindow();
// move the map to its position
map.getController().animateTo(thisMarker.getPosition());
}
return true;
});
// set its position
marker.setPosition(geoPoint);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
// add to it an icon
//marker.setIcon(getResources().getDrawable(R.drawable.bus_marker));
marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme()));
// add to it a title
marker.setTitle(stopName);
// set the description as the ID
marker.setSnippet(stopID);
// show popup info window of the searched marker
if (isStartMarker) {
marker.showInfoWindow();
//map.getController().animateTo(marker.getPosition());
}
return marker;
}
@Nullable
@Override
public View getBaseViewForSnackBar() {
return coordLayout;
}
}