diff --git a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java
index d0b8c6c..42882d0 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java
@@ -1,107 +1,108 @@
/*
BusTO - Data components
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.data;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import it.reyboz.bustorino.R;
import static android.content.Context.MODE_PRIVATE;
import androidx.preference.PreferenceManager;
import it.reyboz.bustorino.fragments.SettingsFragment;
import it.reyboz.bustorino.map.MapLibreUtils;
import java.util.HashSet;
import java.util.Set;
/**
* Static class for commonly used SharedPreference operations
*/
public abstract class PreferencesHolder {
public static final String PREF_GTFS_DB_VERSION = "gtfs_db_version";
public static final String PREF_INTRO_ACTIVITY_RUN ="pref_intro_activity_run";
public static final String DB_GTT_VERSION_KEY = "NextGenDB.GTTVersion";
public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate";
public static final String PREF_FAVORITE_LINES = "pref_favorite_lines";
public static final Set KEYS_MERGE_SET = Set.of(PREF_FAVORITE_LINES);
public static final Set IGNORE_KEYS_LOAD_MAIN = Set.of(PREF_GTFS_DB_VERSION, PREF_INTRO_ACTIVITY_RUN, DB_GTT_VERSION_KEY, DB_LAST_UPDATE_KEY);
public static SharedPreferences getMainSharedPreferences(Context context){
return context.getSharedPreferences(context.getString(R.string.mainSharedPreferences), MODE_PRIVATE);
}
public static SharedPreferences getAppPreferences(Context con){
return PreferenceManager.getDefaultSharedPreferences(con);
}
public static int getGtfsDBVersion(SharedPreferences pref){
return pref.getInt(PREF_GTFS_DB_VERSION,-1);
}
public static void setGtfsDBVersion(SharedPreferences pref,int version){
SharedPreferences.Editor ed = pref.edit();
ed.putInt(PREF_GTFS_DB_VERSION,version);
ed.apply();
}
/**
* Check if the introduction activity has been run at least one
* @param con the context needed
* @return true if it has been run
*/
public static boolean hasIntroFinishedOneShot(Context con){
final SharedPreferences pref = getMainSharedPreferences(con);
return pref.getBoolean(PREF_INTRO_ACTIVITY_RUN, false);
}
public static boolean addOrRemoveLineToFavorites(Context con, String gtfsLineId, boolean addToFavorites){
final SharedPreferences pref = getMainSharedPreferences(con);
final HashSet favorites = new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>()));
boolean modified = true;
if(addToFavorites)
favorites.add(gtfsLineId);
else if(favorites.contains(gtfsLineId))
favorites.remove(gtfsLineId);
else
modified = false; // we are not changing anything
if(modified) {
final SharedPreferences.Editor editor = pref.edit();
editor.putStringSet(PREF_FAVORITE_LINES, favorites);
editor.apply();
}
return modified;
}
public static HashSet getFavoritesLinesGtfsIDs(@NonNull Context con){
final SharedPreferences pref = getMainSharedPreferences(con);
return new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>()));
}
public static String getMapLibreStyleFile(Context con){
final SharedPreferences pref = getAppPreferences(con);
final String mapStyle_val = pref.getString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, "");
return switch (mapStyle_val) {
//MUST MATCH IN keys.xml -> map_style_pref_values
case "versatiles_c" -> MapLibreUtils.STYLE_VERSATILES_COLORFUL_JSON;
- default -> MapLibreUtils.STYLE_BRIGHT_DEFAULT_JSON; //default is "bright"
+ case "bright" -> MapLibreUtils.STYLE_BRIGHT_DEFAULT_JSON; //default is "bright"
+ default -> MapLibreUtils.STYLE_VERSATILES_COLORFUL_JSON;
};
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt
new file mode 100644
index 0000000..9244d16
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt
@@ -0,0 +1,148 @@
+package it.reyboz.bustorino.fragments
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.preference.PreferenceManager
+import it.reyboz.bustorino.backend.Stop
+import it.reyboz.bustorino.data.PreferencesHolder
+import org.maplibre.android.MapLibre
+import org.maplibre.android.camera.CameraPosition
+import org.maplibre.android.geometry.LatLng
+import org.maplibre.android.maps.MapLibreMap
+import org.maplibre.android.maps.MapView
+import org.maplibre.android.maps.OnMapReadyCallback
+
+abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback {
+ protected var map: MapLibreMap? = null
+ protected var shownStopInBottomSheet : Stop? = null
+ protected var savedMapStateOnPause : Bundle? = null
+
+ // Declare a variable for MapView
+ protected lateinit var mapView: MapView
+
+ protected lateinit var sharedPreferences: SharedPreferences
+
+ private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener(){ pref, key ->
+ /*when(key){
+ SettingsFragment.LIBREMAP_STYLE_PREF_KEY -> reloadMap()
+ }
+
+ */
+ if(key == SettingsFragment.LIBREMAP_STYLE_PREF_KEY){
+ Log.d(DEBUG_TAG,"ASKING RELOAD OF MAP")
+
+ reloadMap()
+ }
+ }
+
+ private var lastMapStyle =""
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ //sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ lastMapStyle = PreferencesHolder.getMapLibreStyleFile(requireContext())
+
+ //init map
+ MapLibre.getInstance(requireContext())
+
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ lastMapStyle = PreferencesHolder.getMapLibreStyleFile(requireContext())
+ Log.d(DEBUG_TAG, "onCreateView lastMapStyle: $lastMapStyle")
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val newMapStyle = PreferencesHolder.getMapLibreStyleFile(requireContext())
+ Log.d(DEBUG_TAG, "onResume newMapStyle: $newMapStyle, lastMapStyle: $lastMapStyle")
+ if(newMapStyle!=lastMapStyle){
+ reloadMap()
+ }
+ }
+
+ override fun onLowMemory() {
+ super.onLowMemory()
+ mapView.onLowMemory()
+ }
+
+
+ protected fun reloadMap(){
+ /*map?.let {
+ Log.d("GeneralMapFragment", "RELOADING MAP")
+ //save map state
+ savedMapStateOnPause = saveMapStateInBundle()
+
+ onMapDestroy()
+ //Destroy and recreate MAP
+ mapView.onDestroy()
+ mapView.onCreate(null)
+ mapView.getMapAsync(this)
+ }
+
+ */
+ //TODO figure out how to switch map safely
+ }
+
+ abstract fun openStopInBottomSheet(stop: Stop)
+
+ //For extra stuff to do when the map is destroyed
+ abstract fun onMapDestroy()
+
+ protected fun restoreMapStateFromBundle(bundle: Bundle){
+ val nullDouble = -10_000.0
+ val latCenter = bundle.getDouble("center_map_lat", -10.0)
+ val lonCenter = bundle.getDouble("center_map_lon",-10.0)
+ val zoom = bundle.getDouble("map_zoom", -10.0)
+ val bearing = bundle.getDouble("map_bearing", nullDouble)
+ val tilt = bundle.getDouble("map_tilt", nullDouble)
+ if(lonCenter>=0 &&latCenter>=0) map?.let {
+ val newPos = CameraPosition.Builder().target(LatLng(latCenter,lonCenter))
+ if(zoom>0) newPos.zoom(zoom)
+ if(bearing!=nullDouble) newPos.bearing(bearing)
+ if(tilt != nullDouble) newPos.tilt(tilt)
+ it.cameraPosition=newPos.build()
+
+ }
+ val mStop = bundle.getBundle("shown_stop")?.let {
+ Stop.fromBundle(it)
+ }
+ mStop?.let { openStopInBottomSheet(it) }
+ }
+
+ protected fun saveMapStateBeforePause(bundle: Bundle){
+ map?.let {
+ val newBbox = it.projection.visibleRegion.latLngBounds
+
+
+ val cp = it.cameraPosition
+ bundle.putDouble("center_map_lat", newBbox.center.latitude)
+ bundle.putDouble("center_map_lon", newBbox.center.longitude)
+ it.cameraPosition.zoom.let { z-> bundle.putDouble("map_zoom",z) }
+ bundle.putDouble("map_bearing",cp.bearing)
+ bundle.putDouble("map_tilt", cp.tilt)
+ }
+ shownStopInBottomSheet?.let {
+ bundle.putBundle("shown_stop", it.toBundle())
+ }
+ }
+
+ protected fun saveMapStateInBundle(): Bundle {
+ val b = Bundle()
+ saveMapStateBeforePause(b)
+ return b
+ }
+
+ companion object{
+ private const val DEBUG_TAG="GeneralMapLibreFragment"
+
+ const val BUSES_SOURCE_ID = "buses-source"
+ const val BUSES_LAYER_ID = "buses-layer"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
index e180e92..c2c3461 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
@@ -1,1438 +1,1444 @@
/*
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.Intent
import android.content.SharedPreferences
import android.location.Location
import android.net.Uri
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 {
+class LinesDetailFragment() : GeneralMapLibreFragment() {
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 shouldMapLocationBeReactivated = true
//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 lateinit var stopsSource: GeoJsonSource
private lateinit var busesSource: GeoJsonSource
private lateinit var polylineSource: GeoJsonSource
private var savedCameraPosition: CameraPosition? = null
private var vehShowing = ""
private var stopsLayerStarted = false
private var lastStopsSizeShown = 0
private var lastUpdateTime:Long = -2
//BUS POSITIONS
private val updatesByVehDict = HashMap(5)
private val animatorsByVeh = HashMap()
private var lastLocation : Location? = null
private var enablingPositionFromClick = false
private var polyline: LineString? = null
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
//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)
}
/*
*/
viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops ->
if(mapView.visibility ==View.VISIBLE)
patternShown?.let{
// 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
}
/*
*/
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()
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))
hideStopBottomSheet()
if(locationComponent.isLocationComponentEnabled){
locationComponent.isLocationComponentEnabled = false
shouldMapLocationBeReactivated = true
} else
shouldMapLocationBeReactivated = false
}
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))
if(shouldMapLocationBeReactivated && Permissions.bothLocationPermissionsGranted(requireContext())){
locationComponent.isLocationComponentEnabled = true
}
}
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 context = requireContext()
val mjson = Styles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) //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())
//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)
}
*/
setupBusLayer(style)
mapViewModel.stopShowing?.let {
openStopInBottomSheet(it)
}
mapViewModel.stopShowing = null
}
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.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)
updatesByVehDict[vehid]?.let {
//if (it.posUpdate.latitude != null && it.longitude != null)
mapReady.animateCamera(
CameraUpdateFactory.newLatLng(LatLng(it.posUpdate.latitude, it.posUpdate.longitude)),
750
)
}
return@addOnMapClickListener true
}
false
}
// 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")
}
*/
val zoom = 12.0
val latlngTarget = LatLng(MapLibreFragment.DEFAULT_CENTER_LAT, MapLibreFragment.DEFAULT_CENTER_LON)
mapReady.cameraPosition = savedCameraPosition ?:CameraPosition.Builder().target(latlngTarget).zoom(zoom).build()
savedCameraPosition = null
if(shouldMapLocationBeReactivated) setMapUserLocationEnabled(true, false, false)
}
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)
//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 =
MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context)
locationComponent.activateLocationComponent(locationComponentActivationOptions)
locationComponent.isLocationComponentEnabled = false
lastLocation?.let {
if (it.accuracy < 200)
locationComponent.forceLocationUpdate(it)
}
}
/**
* Update the bottom sheet with the stop information
*/
- private fun openStopInBottomSheet(stop: Stop){
+ override fun openStopInBottomSheet(stop: Stop){
bottomLayout?.let {
//lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
val stopName = stop.stopUserName ?: stop.stopDefaultName
stopTitleTextView.text = stopName//stop.stopDefaultName
stopNumberTextView.text = 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
directionsCard.setOnClickListener {
if(stop.latitude==null || stop.longitude==null){
//TODO: show message
Log.e(DEBUG_TAG, "Navigate to stop but longitude and/or latitude are null")
}else{
val uri = "geo:?q=${stop.latitude},${stop.longitude}(${stop.ID} - $stopName)"
val intent =Intent(Intent.ACTION_VIEW, Uri.parse(uri))
context?.run{
if(intent.resolveActivity(packageManager)!=null){
startActivity(intent)
}
}
}
}
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
vehShowing = ""
}
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))
directionsCard.setOnClickListener {
data.pattern?.let {
showPatternWithCode(it.code)
} //TODO
// ?: {
// context?.let { ctx -> Toast.makeText(ctx,"") }
//}
}
vehShowing = veh
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)
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 = GeoJsonSource(POLYLINE_SOURCE) //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, zoomToPattern: Boolean){
Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}")
//if(viewingPattern==null || map == null) 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)
//}
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 showPatternWithCode(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(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 = updatesByVehDict[vehID]?.posUpdate
var avoidShowingUpdateBecauseIsImpossible = false
oldPos?.let{
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 { (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 {
//update
updatesByVehDict[vehID] = LivePositionTripPattern(pos,patternStops?.pattern)
/*busLabelSymbolsByVeh[vehID]?.let {
it.latLng = LatLng(pos.latitude, pos.longitude)
symbolsToUpdate.add(it)
}*/
//if(vehShowing==vehID)
// map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500)
//TODO: Follow the vehicle
}
}
countUpds++
}
else{
//not inside
// update it simply
updatesByVehDict[vehID] = LivePositionTripPattern(pos, patternStops?.pattern)
//createLabelForVehicle(pos)
//if(vehShowing==vehID)
// map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500)
}
if (vehID == vehShowing){
//update the data
showVehicleTripInBottomSheet(vehID)
}
}
//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 (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 updatesByVehDict.keys)
return
val vehID = positionUpdate.vehicle
val currentUpdate = updatesByVehDict[positionUpdate.vehicle]
currentUpdate?.let { it ->
//cancel current animation on vehicle
animatorsByVeh[vehID]?.cancel()
val posUp = it.posUpdate
val currentPos = LatLng(posUp.latitude, posUp.longitude)
val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
val valueAnimator = ValueAnimator.ofObject(MapLibreUtils.LatLngEvaluator(), currentPos, newPos)
valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
private var latLng: LatLng? = null
override fun onAnimationUpdate(animation: ValueAnimator) {
latLng = animation.animatedValue as LatLng
//update position on animation
val update = updatesByVehDict[positionUpdate.vehicle]!!
latLng?.let { ll->
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 = 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")
//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 (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()
shownStopInBottomSheet?.let {
mapViewModel.stopShowing = it
}
shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled
}
override fun onDestroyView() {
map?.run {
Log.d(DEBUG_TAG, "Saving camera position")
savedCameraPosition = cameraPosition
}
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 onMapDestroy() {
+ mapStyle.removeLayer(STOPS_LAYER_ID)
+
+ mapStyle.removeSource(STOPS_SOURCE_ID)
+
+ mapStyle.removeLayer(POLYLINE_LAYER)
+ mapStyle.removeSource(POLYLINE_SOURCE)
+ mapStyle.removeLayer(BUSES_LAYER_ID)
+ mapStyle.removeSource(BUSES_SOURCE_ID)
+
+
+ map?.locationComponent?.isLocationComponentEnabled = 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-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/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
index ecc8d1b..3ae06bc 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
@@ -1,1152 +1,1117 @@
package it.reyboz.bustorino.fragments
import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.net.Uri
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.ImageButton
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.gson.Gson
import com.google.gson.JsonObject
import it.reyboz.bustorino.R
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate
import it.reyboz.bustorino.backend.mato.MQTTMatoClient
import it.reyboz.bustorino.data.PreferencesHolder
import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
import it.reyboz.bustorino.fragments.SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE
import it.reyboz.bustorino.map.MapLibreUtils
import it.reyboz.bustorino.map.Styles
import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.viewmodels.LivePositionsViewModel
import it.reyboz.bustorino.viewmodels.StopsMapViewModel
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.location.modes.CameraMode
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.OnMapReadyCallback
import org.maplibre.android.maps.Style
import org.maplibre.android.plugins.annotation.Symbol
import org.maplibre.android.plugins.annotation.SymbolManager
import org.maplibre.android.plugins.annotation.SymbolOptions
import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.layers.Property.*
import org.maplibre.android.style.layers.PropertyFactory
import org.maplibre.android.style.layers.SymbolLayer
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val STOP_TO_SHOW = "stoptoshow"
/**
* A simple [Fragment] subclass.
* Use the [MapLibreFragment.newInstance] factory method to
* create an instance of this fragment.
*/
-class MapLibreFragment : ScreenBaseFragment(), OnMapReadyCallback {
+class MapLibreFragment : GeneralMapLibreFragment() {
protected var fragmentListener: CommonFragmentListener? = null
- //private var param1: String? = null
- //private var param2: String? = null
- // Declare a variable for MapView
- private lateinit var mapView: MapView
private lateinit var locationComponent: LocationComponent
private var lastLocation: Location? = null
private val stopsViewModel: StopsMapViewModel by viewModels()
private val gson = Gson()
private var stopsShowing = ArrayList(0)
private var isBottomSheetShowing = false
private lateinit var symbolManager: SymbolManager
- protected var map: MapLibreMap? = null
// Sources for stops and buses
private lateinit var stopsSource: GeoJsonSource
private lateinit var busesSource: GeoJsonSource
private var stopsLayerStarted = false
private var lastStopsSizeShown = 0
private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0)
private lateinit var mapStyle: Style
private var mapInitCompleted =false
private var stopsRedrawnTimes = 0
//bottom Sheet behavior
private lateinit var bottomSheetBehavior: BottomSheetBehavior
private var bottomLayout: RelativeLayout? = null
private lateinit var stopTitleTextView: TextView
private lateinit var stopNumberTextView: TextView
private lateinit var linesPassingTextView: TextView
private lateinit var arrivalsCard: CardView
private lateinit var directionsCard: CardView
private var stopActiveSymbol: Symbol? = null
// Location stuff
private lateinit var locationManager: LocationManager
private lateinit var showUserPositionButton: ImageButton
private lateinit var centerUserButton: ImageButton
private lateinit var followUserButton: ImageButton
private var followingUserLocation = false
private var pendingLocationActivation = false
private var ignoreCameraMovementForFollowing = true
private var enablingPositionFromClick = false
private val positionRequestLauncher =
registerForActivityResult, Map>(
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
Log.d(DEBUG_TAG, "HAVE THE PERMISSIONS")
if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null)
return@ActivityResultCallback ///@registerForActivityResult
val locationManager =
requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
@SuppressLint("MissingPermission") val userLocation =
locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (userLocation != null) {
if(LatLng(userLocation.latitude, userLocation.longitude).distanceTo(DEFAULT_LATLNG) >= MAX_DIST_KM*1000){
setMapLocationEnabled(true, true, false)
}
} else requestInitialUserLocation()
} else{
Toast.makeText(requireContext(),"User location disabled", Toast.LENGTH_SHORT).show()
Log.w(DEBUG_TAG, "No location permission")
}
})
private val showUserPositionRequestLauncher =
registerForActivityResult, Map>(
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
setMapLocationEnabled(true, true, enablingPositionFromClick)
} else Log.w(DEBUG_TAG, "No location permission")
})
//BUS POSITIONS
private var useMQTTViewModel = true
private val livePositionsViewModel : LivePositionsViewModel by viewModels()
private val positionsByVehDict = HashMap(5)
private val animatorsByVeh = HashMap()
private var lastUpdateTime : Long = -1
private var busLabelSymbolsByVeh = HashMap()
private val symbolsToUpdate = ArrayList()
private var initialStopToShow : Stop? = null
private var initialStopShown = false
//shown stuff
private var savedStateOnStop : Bundle? = null
- private var savedMapStateOnPause : Bundle? = null
- private var shownStopInBottomSheet : Stop? = null
private val showBusLayer = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
initialStopToShow = Stop.fromBundle(arguments)
}
-
- MapLibre.getInstance(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val rootView = inflater.inflate(R.layout.fragment_map_libre,
container, false)
//reset the counter
lastStopsSizeShown = 0
stopsRedrawnTimes = 0
stopsLayerStarted = false
symbolsToUpdate.clear()
// Init layout view
// Init the MapView
mapView = rootView.findViewById(R.id.libreMapView)
if(savedStateOnStop!=null){
mapView.onCreate(savedStateOnStop)
} else mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this) //{ //map ->
//map.setStyle("https://demotiles.maplibre.org/style.json") }
//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)
showUserPositionButton = rootView.findViewById(R.id.locationEnableIcon)
showUserPositionButton.setOnClickListener(this::switchUserLocationStatus)
followUserButton = rootView.findViewById(R.id.followUserImageButton)
centerUserButton = rootView.findViewById(R.id.centerMapImageButton)
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
arrivalsCard.setOnClickListener {
if(context!=null){
Toast.makeText(context,"ARRIVALS", Toast.LENGTH_SHORT).show()
}
}
centerUserButton.setOnClickListener {
if(context!=null && locationComponent.isLocationComponentEnabled) {
val location = locationComponent.lastKnownLocation
location?.let {
mapView.getMapAsync { map ->
map.animateCamera(CameraUpdateFactory.newCameraPosition(
CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build()), 500)
}
}
}
}
followUserButton.setOnClickListener {
// onClick user following button
if(context!=null && locationComponent.isLocationComponentEnabled){
if(followingUserLocation)
locationComponent.cameraMode = CameraMode.NONE
else locationComponent.cameraMode = CameraMode.TRACKING
// CameraMode.TRACKING makes the camera move and jump to the location
setFollowingUser(!followingUserLocation)
}
}
locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (Permissions.bothLocationPermissionsGranted(requireContext())) {
requestInitialUserLocation()
} 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()
}
positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS)
}
// Setup close button
rootView.findViewById(R.id.btnClose).setOnClickListener {
hideStopBottomSheet()
}
Log.d(DEBUG_TAG, "Fragment View Created!")
//TODO: Reshow last open stop when switching back to the map fragment
return rootView
}
/**
* This method sets up the map and the layers
*/
override fun onMapReady(mapReady: MapLibreMap) {
this.map = mapReady
val context = requireContext()
val mjson = Styles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context))
//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.textIgnorePlacement =true
symbolManager.addClickListener{ _ ->
if (stopActiveSymbol!=null){
hideStopBottomSheet()
return@addClickListener true
} else
return@addClickListener false
}
+
// Start observing data
observeStops()
initMapLocation(style, mapReady, requireContext())
//init stop layer with this
val stopsInCache = stopsViewModel.getAllStopsLoaded()
if(stopsInCache.isEmpty())
initStopsLayer(style, FeatureCollection.fromFeatures(ArrayList()))
else
displayStops(stopsInCache)
if(showBusLayer) setupBusLayer(style)
}
mapReady.addOnCameraIdleListener {
map?.let {
val newBbox = it.projection.visibleRegion.latLngBounds
if ((newBbox.center==lastBBox.center) && (newBbox.latitudeSpan==lastBBox.latitudeSpan) && (newBbox.longitudeSpan==lastBBox.latitudeSpan)){
//do nothing
} else {
stopsViewModel.loadStopsInLatLngBounds(newBbox)
lastBBox = newBbox
}
}
}
mapReady.addOnCameraMoveStartedListener {
map?.let { setFollowingUser(it.locationComponent.cameraMode == CameraMode.TRACKING) }
//setFollowingUser()
}
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 = stopsViewModel.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(context!=null){
- Toast.makeText(context, "Veh $vehid on route $route", Toast.LENGTH_SHORT).show()
- }
+ Toast.makeText(context, "Veh $vehid on route $route", Toast.LENGTH_SHORT).show()
return@addOnMapClickListener true
}
false
}
mapInitCompleted = true
// we start requesting the bus positions now
startRequestingPositions()
}
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 = 15.0
//center position
val latlngTarget = initialStopToShow?.let {
LatLng(it.latitude!!, it.longitude!!)
} ?: LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)
mapReady.cameraPosition = CameraPosition.Builder().target(latlngTarget).zoom(zoom).build()
}
//reset saved state
savedMapStateOnPause = null
}
private fun initStopsLayer(style: Style, features:FeatureCollection){
stopsSource = GeoJsonSource(STOPS_SOURCE_ID,features)
style.addSource(stopsSource)
// add icon
style.addImage(STOP_IMAGE_ID,
ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!)
style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!)
// Stops layer
val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID)
stopsLayer.withProperties(
PropertyFactory.iconImage(STOP_IMAGE_ID),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconIgnorePlacement(true)
)
style.addLayerBelow(stopsLayer, "label_country_1")
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)
//Line names layer
/*vehiclesLabelsSource = GeoJsonSource(LABELS_SOURCE)
style.addSource(vehiclesLabelsSource)
val textLayer = SymbolLayer(LABELS_LAYER_ID, LABELS_SOURCE).apply {
withProperties(
PropertyFactory.textField("label"),
PropertyFactory.textSize(30f),
PropertyFactory.textColor(Color.BLACK),
//PropertyFactory.textHaloColor(Color.BLACK),
//PropertyFactory.textHaloWidth(1f),
PropertyFactory.textAnchor(TEXT_ANCHOR_CENTER),
PropertyFactory.textAllowOverlap(true),
PropertyFactory.textRotationAlignment(TEXT_ROTATION_ALIGNMENT_VIEWPORT)
)
}
style.addLayerAbove(textLayer, BUSES_LAYER_ID)
*/
}
/**
* Update the bottom sheet with the stop information
*/
- private fun openStopInBottomSheet(stop: Stop){
+ override fun openStopInBottomSheet(stop: Stop){
bottomLayout?.let {
//lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
val stopName = stop.stopUserName ?: stop.stopDefaultName
stopTitleTextView.text = stopName//stop.stopDefaultName
stopNumberTextView.text = stop.ID
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)
}
directionsCard.setOnClickListener {
if(stop.latitude==null || stop.longitude==null){
//TODO: show message
Log.e(DEBUG_TAG, "Navigate to stop but longitude and/or latitude are null")
}else{
val uri = "geo:?q=${stop.latitude},${stop.longitude}(${stop.ID} - $stopName)"
val intent =Intent(Intent.ACTION_VIEW, Uri.parse(uri))
context?.run{
if(intent.resolveActivity(packageManager)!=null){
startActivity(intent)
}
}
}
}
}
//add stop marker
if (stop.latitude!=null && stop.longitude!=null) {
stopActiveSymbol = symbolManager.create(
SymbolOptions()
.withLatLng(LatLng(stop.latitude!!, stop.longitude!!))
.withIconImage(STOP_ACTIVE_IMG)
.withIconAnchor(ICON_ANCHOR_CENTER)
+ //.withTextFont(arrayOf("noto_sans_regular"))
)
}
shownStopInBottomSheet = stop
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun onAttach(context: Context) {
super.onAttach(context)
fragmentListener = if (context is CommonFragmentListener) {
context
} else {
throw RuntimeException(
context.toString()
+ " must implement FragmentListenerMain"
)
}
}
override fun onDetach() {
super.onDetach()
fragmentListener = null
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onResume() {
super.onResume()
mapView.onResume()
val keySourcePositions = getString(R.string.pref_positions_source)
if(showBusLayer) {
useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(keySourcePositions, LIVE_POSITIONS_PREF_MQTT_VALUE)
.contentEquals(LIVE_POSITIONS_PREF_MQTT_VALUE)
if (useMQTTViewModel) livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL)
else livePositionsViewModel.requestGTFSUpdates()
//mapViewModel.testCascade();
livePositionsViewModel.isLastWorkResultGood.observe(this) { d: Boolean ->
Log.d(
DEBUG_TAG, "Last trip download result is $d"
)
}
livePositionsViewModel.tripsGtfsIDsToQuery.observe(this) { dat: List ->
Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat")
livePositionsViewModel.downloadTripsFromMato(dat)
}
}
fragmentListener?.readyGUIfor(FragmentKind.MAP)
}
override fun onPause() {
super.onPause()
mapView.onPause()
Log.d(DEBUG_TAG, "Fragment paused")
savedMapStateOnPause = saveMapStateInBundle()
if (useMQTTViewModel) livePositionsViewModel.stopMatoUpdates()
}
override fun onStop() {
super.onStop()
mapView.onStop()
Log.d(DEBUG_TAG, "Fragment stopped!")
savedStateOnStop = Bundle().let {
mapView.onSaveInstanceState(it)
it
}
}
- override fun onLowMemory() {
- super.onLowMemory()
- mapView.onLowMemory()
- }
-
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
Log.d(DEBUG_TAG, "Destroyed map Fragment!!")
}
+ override fun onMapDestroy() {
+ mapStyle.removeLayer(STOPS_LAYER_ID)
+ mapStyle.removeSource(STOPS_SOURCE_ID)
+
+ mapStyle.removeLayer(BUSES_LAYER_ID)
+ mapStyle.removeSource(BUSES_SOURCE_ID)
+
+
+ map?.locationComponent?.isLocationComponentEnabled = false
+ }
override fun getBaseViewForSnackBar(): View? {
return mapView
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
Log.d(DEBUG_TAG, "Saved instanceState")
}
private fun observeStops() {
// Observe stops
stopsViewModel.stopsToShow.observe(viewLifecycleOwner) { stops ->
stopsShowing = ArrayList(stops)
displayStops(stopsShowing)
initialStopToShow?.let{ s->
//show the stop in the bottom sheet
if(!initialStopShown) {
openStopInBottomSheet(s)
initialStopShown = true
}
}
}
}
/**
* Add the stops to the layers
*/
private fun displayStops(stops: List?) {
if (stops.isNullOrEmpty()) return
if (stops.size==lastStopsSizeShown){
Log.d(DEBUG_TAG, "Not updating, have same number of stops. After 3 times")
return
}
/*if(stops.size> lastStopsSizeShown){
stopsRedrawnTimes = 0
} else{
stopsRedrawnTimes++
}
*/
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)
features.add(
Feature.fromGeometry(
Point.fromLngLat(s.longitude!!, s.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))
lastStopsSizeShown = features.size
} else
map?.let {
Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer")
initStopsLayer(mapStyle, FeatureCollection.fromFeatures(features))
Log.d(DEBUG_TAG,"Started stops layer on map")
lastStopsSizeShown = features.size
stopsLayerStarted = true
}
}
// Hide the bottom sheet and remove extra symbol
private fun hideStopBottomSheet(){
if (stopActiveSymbol!=null){
symbolManager.delete(stopActiveSymbol)
stopActiveSymbol = null
}
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
isBottomSheetShowing = false
//remove initial stop
if(initialStopToShow!=null){
initialStopToShow = null
}
shownStopInBottomSheet = null
}
// --------------- BUS LOCATIONS STUFF --------------------------
/**
* Start requesting position updates
*/
private fun startRequestingPositions() {
livePositionsViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> ->
Log.d(
DEBUG_TAG,
"Have " + data.size + " trip updates, has Map start finished: " + mapInitCompleted
)
if (mapInitCompleted) updateBusPositionsInMap(data)
if (!isDetached && !useMQTTViewModel) livePositionsViewModel.requestDelayedGTFSUpdates(
3000
)
}
}
private fun isInsideVisibleRegion(latitude: Double, longitude: Double, nullValue: Boolean): Boolean{
var isInside = nullValue
val visibleRegion = map?.projection?.visibleRegion
visibleRegion?.let {
val bounds = it.latLngBounds
isInside = bounds.contains(LatLng(latitude, longitude))
}
return isInside
}
private fun createLabelForVehicle(positionUpdate: LivePositionUpdate){
- val newSymbol = symbolManager.create(SymbolOptions()
+ val symOpt = SymbolOptions()
.withLatLng(LatLng(positionUpdate.latitude, positionUpdate.longitude))
.withTextColor("#ffffff")
.withTextField(positionUpdate.routeID.substringBeforeLast('U'))
.withTextSize(13f)
.withTextAnchor(TEXT_ANCHOR_CENTER)
+ //.withTextFont(arrayOf("noto_sans_regular")) //"noto_sans_regular"))
+ val newSymbol = symbolManager.create(symOpt
)
busLabelSymbolsByVeh[positionUpdate.vehicle] = newSymbol
}
private fun removeVehicleLabel(vehicle: String){
busLabelSymbolsByVeh[vehicle]?.let {
symbolManager.delete(it)
busLabelSymbolsByVeh.remove(vehicle)
}
}
/**
* Update function for the bus positions
* Takes the processed updates and saves them accordingly
*/
private fun updateBusPositionsInMap(incomingData: HashMap>){
val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle })
val vehsOld = HashSet(positionsByVehDict.keys)
val symbolsToUpdate = ArrayList()
for (upsWithTrp in incomingData.values){
val pos = upsWithTrp.first
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]
var avoidShowingUpdateBecauseIsImpossible = false
oldPos?.let{
if(oldPos.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
if(!samePosition) {
val isPositionInBounds = isInsideVisibleRegion(
pos.latitude, pos.longitude, true
) || (oldPos?.let { isInsideVisibleRegion(it.latitude,it.longitude,true) } ?: false)
if (isPositionInBounds) {
//animate = true
//this moves both the icon and the label
moveVehicleToNewPosition(pos)
} else {
positionsByVehDict[vehID] = pos
busLabelSymbolsByVeh[vehID]?.let {
it.latLng = LatLng(pos.latitude, pos.longitude)
symbolsToUpdate.add(it)
}
}
}
}
else{
// update it simply
positionsByVehDict[vehID] = pos
createLabelForVehicle(pos)
}
}
symbolManager.update(symbolsToUpdate)
//remove old positions
vehsOld.removeAll(vehsNew)
//now vehsOld contains the vehicles id for those that have NOT been updated
val currentTimeStamp = System.currentTimeMillis() /1000
for(vehID in vehsOld){
//remove after 2 minutes of inactivity
if (positionsByVehDict[vehID]!!.timestamp - currentTimeStamp > 2*60){
positionsByVehDict.remove(vehID)
removeVehicleLabel(vehID)
}
}
//update UI
updatePositionsIcons()
}
/**
* This is the tricky part, animating the transitions
* Basically, we need to set the new positions with the data and redraw them all
*/
private fun moveVehicleToNewPosition(positionUpdate: LivePositionUpdate){
if (positionUpdate.vehicle !in positionsByVehDict.keys)
return
val vehID = positionUpdate.vehicle
val currentUpdate = positionsByVehDict[positionUpdate.vehicle]
currentUpdate?.let { it ->
//cancel current animation on vehicle
animatorsByVeh[vehID]?.cancel()
val currentPos = LatLng(it.latitude, it.longitude)
val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude)
val valueAnimator = ValueAnimator.ofObject(MapLibreUtils.LatLngEvaluator(), currentPos, newPos)
valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
private var latLng: LatLng? = null
override fun onAnimationUpdate(animation: ValueAnimator) {
latLng = animation.animatedValue as LatLng
//update position on animation
val update = positionsByVehDict[positionUpdate.vehicle]!!
latLng?.let { ll->
update.latitude = ll.latitude
update.longitude = ll.longitude
updatePositionsIcons()
}
}
})
valueAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
super.onAnimationStart(animation)
//val update = positionsByVehDict[positionUpdate.vehicle]!!
//remove the label at the start of the animation
//removeVehicleLabel(vehID)
val annot = busLabelSymbolsByVeh[vehID]
annot?.let { sym ->
sym.textOpacity = 0.0f
symbolsToUpdate.add(sym)
}
}
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
//recreate the label at the end of the animation
//createLabelForVehicle(positionUpdate)
val annot = busLabelSymbolsByVeh[vehID]
annot?.let { sym ->
sym.textOpacity = 1.0f
sym.latLng = newPos //LatLng(newPos)
symbolsToUpdate.add(sym)
}
}
})
//set the new position as the current one but with the old lat and lng
positionUpdate.latitude = currentUpdate.latitude
positionUpdate.longitude = currentUpdate.longitude
positionsByVehDict[vehID] = positionUpdate
valueAnimator.duration = 300
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.start()
animatorsByVeh[vehID] = valueAnimator
} ?: {
Log.e(DEBUG_TAG, "Have to run animation for veh ${positionUpdate.vehicle} but not in the dict, adding")
positionsByVehDict[positionUpdate.vehicle] = positionUpdate
}
}
/**
* Update the bus positions displayed on the map, from the existing data
*/
private fun updatePositionsIcons(){
//avoid frequent updates
val currentTime = System.currentTimeMillis()
if(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){
//if (s.latitude!=null && s.longitude!=null)
val point = Point.fromLngLat(pos.longitude, pos.latitude)
features.add(
Feature.fromGeometry(
point,
JsonObject().apply {
addProperty("veh", pos.vehicle)
addProperty("trip", pos.tripID)
addProperty("bearing", pos.bearing ?:0.0f)
addProperty("line", pos.routeID)
}
)
)
/*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()
}
// ------ LOCATION STUFF -----
@SuppressLint("MissingPermission")
private fun requestInitialUserLocation() {
val provider : String = LocationManager.GPS_PROVIDER//getBestLocationProvider()
//provider.let {
setLocationIconEnabled(true)
Toast.makeText(requireContext(), R.string.position_searching_message, Toast.LENGTH_SHORT).show()
pendingLocationActivation = true
locationManager.requestSingleUpdate(provider, object : LocationListener {
override fun onLocationChanged(location: Location) {
val userLatLng = LatLng(location.latitude, location.longitude)
val distanceToTarget = userLatLng.distanceTo(DEFAULT_LATLNG)
if (distanceToTarget <= MAX_DIST_KM*1000.0) {
map?.let{
// if we are still waiting for the position to enable
if(pendingLocationActivation)
setMapLocationEnabled(true, true, false)
}
} else {
Toast.makeText(context, "You are too far, not showing the position", Toast.LENGTH_SHORT).show()
}
}
override fun onProviderDisabled(provider: String) {}
override fun onProviderEnabled(provider: String) {}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
}, null)
}
/**
* Initialize the map location, but do not enable the component
*/
@SuppressLint("MissingPermission")
private fun initMapLocation(style: Style, map: MapLibreMap, context: Context){
locationComponent = map.locationComponent
val locationComponentOptions =
LocationComponentOptions.builder(context)
.pulseEnabled(true)
.build()
val locationComponentActivationOptions =
MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context)
locationComponent.activateLocationComponent(locationComponentActivationOptions)
locationComponent.isLocationComponentEnabled = false
lastLocation?.let {
if (it.accuracy < 200)
locationComponent.forceLocationUpdate(it)
}
}
/**
* Handles logic of enabling the user location on the map
*/
@SuppressLint("MissingPermission")
private fun setMapLocationEnabled(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
if (initialStopToShow==null) {
locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING
setFollowingUser(true)
}
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
setFollowingUser(false)
setLocationIconEnabled(false)
if (fromClick) {
Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show()
if(pendingLocationActivation) pendingLocationActivation=false //Cancel the request for the enablement of the position
}
}
}
private fun setLocationIconEnabled(enabled: Boolean){
if (enabled)
showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red))
else
showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey))
}
/**
* Helper method for GUI
*/
private fun updateFollowingIcon(enabled: Boolean){
if(enabled)
followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me_on))
else
followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me))
}
private fun setFollowingUser(following: Boolean){
updateFollowingIcon(following)
followingUserLocation = following
if(following)
ignoreCameraMovementForFollowing = true
}
private fun switchUserLocationStatus(view: View?){
if(pendingLocationActivation || locationComponent.isLocationComponentEnabled) setMapLocationEnabled(false, false, true)
else{
Log.d(DEBUG_TAG, "Request enable location")
setMapLocationEnabled(true, false, true)
}
}
- private fun saveMapStateBeforePause(bundle: Bundle){
- map?.let {
- val newBbox = it.projection.visibleRegion.latLngBounds
-
- bundle.putDouble("center_map_lat", newBbox.center.latitude)
- bundle.putDouble("center_map_lon", newBbox.center.longitude)
- it.cameraPosition.zoom.let { z-> bundle.putDouble("map_zoom",z) }
- }
- shownStopInBottomSheet?.let {
- bundle.putBundle("shown_stop", it.toBundle())
- }
- }
- private fun saveMapStateInBundle(): Bundle {
- val b = Bundle()
- saveMapStateBeforePause(b)
- return b
- }
-
- private fun restoreMapStateFromBundle(bundle: Bundle){
- val latCenter = bundle.getDouble("center_map_lat", -10.0)
- val lonCenter = bundle.getDouble("center_map_lon",-10.0)
- val zoom = bundle.getDouble("map_zoom", -10.0)
- if(lonCenter>=0 &&latCenter>=0) map?.let {
- val newPos = CameraPosition.Builder().target(LatLng(latCenter,lonCenter))
- if(zoom>0) newPos.zoom(zoom)
- it.cameraPosition=newPos.build()
- }
- val mStop = bundle.getBundle("shown_stop")?.let {
- Stop.fromBundle(it)
- }
- mStop?.let { openStopInBottomSheet(it) }
- }
-
companion object {
private const val STOPS_SOURCE_ID = "stops-source"
private const val STOPS_LAYER_ID = "stops-layer"
private const val STOPS_LAYER_SEL_ID ="stops-layer-selected"
- private const val BUSES_SOURCE_ID = "buses-source"
- private const val BUSES_LAYER_ID = "buses-layer"
+
private const val LABELS_LAYER_ID = "bus-labels-layer"
private const val LABELS_SOURCE = "labels-source"
private const val STOP_IMAGE_ID ="bus-stop-icon"
const val DEFAULT_CENTER_LAT = 45.0708
const val DEFAULT_CENTER_LON = 7.6858
private val DEFAULT_LATLNG = LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)
private const val POSITION_FOUND_ZOOM = 16.5
private const val NO_POSITION_ZOOM = 17.1
private const val MAX_DIST_KM = 90.0
//private const val ACCESS_TOKEN="KxO8lF4U3kiO63m0c7lzqDCDrMUVg1OA2JVzRXxxmYSyjugr1xpe4W4Db5rFNvbQ"
//private const val MAPLIBRE_URL = "https://api.jawg.io/styles/"
private const val DEBUG_TAG = "BusTO-MapLibreFrag"
private const val STOP_ACTIVE_IMG = "Stop-active"
private const val LOCATION_PERMISSION_REQUEST_CODE = 981202
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param stop Eventual stop to center the map into
* @return A new instance of fragment MapLibreFragment.
*/
@JvmStatic
fun newInstance(stop: Stop?) =
MapLibreFragment().apply {
arguments = Bundle().let {
// Cannot use Parcelable as it requires higher version of Android
//stop?.let{putParcelable(STOP_TO_SHOW, it)}
stop?.toBundle(it)
}
}
//private fun makeStyleUrl(style: String = "jawg-streets") =
// "${MAPLIBRE_URL+ style}.json?access-token=${ACCESS_TOKEN}"
private fun makeStyleMapBoxUrl(dark: Boolean) =
if(dark)
"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
else //"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
"https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
const val OPENFREEMAP_LIBERY = "https://tiles.openfreemap.org/styles/liberty"
const val OPENFREEMAP_BRIGHT = "https://tiles.openfreemap.org/styles/bright"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
index 944b588..91750b2 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
@@ -1,234 +1,234 @@
/*
BusTO - Fragments components
Copyright (C) 2020 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.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import androidx.preference.*;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import it.reyboz.bustorino.ActivityBackup;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.data.DatabaseUpdate;
import it.reyboz.bustorino.data.GtfsMaintenanceWorker;
import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.List;
public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = SettingsFragment.class.getName();
private static final String DIALOG_FRAGMENT_TAG =
"androidx.preference.PreferenceFragment.DIALOG";
//private static final
Handler mHandler;
// Matching preferences.xml
public final static String PREF_KEY_STARTUP_SCREEN="startup_screen_to_show";
public final static String KEY_ARRIVALS_FETCHERS_USE = "arrivals_fetchers_use_setting";
public final static String LIVE_POSITIONS_PREF_MQTT_VALUE="mqtt";
public final static String LIBREMAP_STYLE_PREF_KEY = "libremap_style";
private boolean setSummaryStartupPref = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mHandler = new Handler();
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
//getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences));
convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext());
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
setPreferencesFromResource(R.xml.preferences,rootKey);
/*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents));
editPref.setOnBindEditTextListener(editText -> {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setSelection(0,editText.getText().length());
});
*/
ListPreference startupScreenPref = findPreference(PREF_KEY_STARTUP_SCREEN);
if(startupScreenPref !=null){
if (startupScreenPref.getValue()==null){
startupScreenPref.setSummary(getString(R.string.nav_arrivals_text));
setSummaryStartupPref = true;
}
}
//Log.d("BusTO-PrefFrag","startup screen pref is "+startupScreenPref.getValue());
Preference dbUpdateNow = findPreference("pref_db_update_now");
if (dbUpdateNow!=null)
- dbUpdateNow.setOnPreferenceClickListener(
+ dbUpdateNow.setOnPreferenceClickListener(
preference -> {
//trigger update
if(getContext()!=null) {
DatabaseUpdate.requestDBUpdateWithWork(getContext().getApplicationContext(), true, true);
Toast.makeText(getContext(),R.string.requesting_db_update,Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
);
//set click listener on backup item
final Preference backupPref = findPreference("pref_backup_open");
if (backupPref!=null) backupPref.setOnPreferenceClickListener(
preference -> {
if(getActivity()!=null){
startActivity( new Intent(getActivity().getApplicationContext(), ActivityBackup.class) );
return true;
} else {
return false;
}
}
);
else {
Log.e("BusTO-Preferences", "Cannot find db update preference");
}
Preference clearGtfsTrips = findPreference("pref_clear_gtfs_trips");
if (clearGtfsTrips != null) {
clearGtfsTrips.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(@NonNull @NotNull Preference preference) {
if (getContext() != null) {
OneTimeWorkRequest requ = GtfsMaintenanceWorker.Companion.makeOneTimeRequest(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS);
WorkManager.getInstance(getContext()).enqueue(requ);
WorkManager.getInstance(getContext()).getWorkInfosByTagLiveData(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS).observe(getViewLifecycleOwner(),
(Observer>) workInfos -> {
if(workInfos.isEmpty())
return;
if(workInfos.get(0).getState()==(WorkInfo.State.SUCCEEDED)){
Toast.makeText(
getContext(), R.string.all_trips_removed, Toast.LENGTH_SHORT
).show();
}
});
return true;
}
return false;
}
});
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Preference pref = findPreference(key);
Log.d(TAG,"Preference key "+key+" changed");
if (key.equals(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE)){
Log.d(TAG, "New value is: "+sharedPreferences.getStringSet(key, new HashSet<>()));
}
//sometimes this happens
if(getContext()==null) return;
if(key.equals(PREF_KEY_STARTUP_SCREEN) && setSummaryStartupPref && pref !=null){
ListPreference listPref = (ListPreference) pref;
pref.setSummary(listPref.getEntry());
}
/*
THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES
if (key.equals(getString(R.string.pref_key_num_recents))){
//check that is it an int
String value = sharedPreferences.getString(key,"");
boolean valid = value.length() != 0;
try{
Integer intValue = Integer.parseInt(value);
} catch (NumberFormatException ex){
valid = false;
}
if (!valid){
Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show();
if(pref instanceof EditTextPreference){
EditTextPreference prefEdit = (EditTextPreference) pref;
//Intent intent = prefEdit.getIntent();
Log.d(TAG, "opening preference, dialog showing "+
(getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) );
//getPreferenceManager().showDialog(pref);
//onDisplayPreferenceDialog(prefEdit);
mHandler.postDelayed(new DelayedDisplay(prefEdit), 500);
}
}
}
*/
Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll());
}
private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){
if (con == null) return;
SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(con);
try{
Integer val = defaultSharedPref.getInt(preferenceKey, 0);
} catch (NumberFormatException | ClassCastException ex){
//convert the preference
//final String preferenceNumRecents = getString(R.string.pref_key_num_recents);
Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey);
String currentValue = defaultSharedPref.getString(preferenceKey, "10");
int newValue;
try{
newValue = Integer.parseInt(currentValue);
} catch (NumberFormatException e){
newValue = 10;
}
final SharedPreferences.Editor editor = defaultSharedPref.edit();
editor.remove(preferenceKey);
editor.putInt(preferenceKey, newValue);
editor.apply();
}
}
class DelayedDisplay implements Runnable{
private final WeakReference preferenceWeakReference;
public DelayedDisplay(DialogPreference preference) {
this.preferenceWeakReference = new WeakReference<>(preference);
}
@Override
public void run() {
if(preferenceWeakReference.get()==null)
return;
getPreferenceManager().showDialog(preferenceWeakReference.get());
}
}
}