diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java
index 79ddce7..4693a53 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java
@@ -1,341 +1,349 @@
/*
BusTO (backend components)
Copyright (C) 2016 Ludovico Pavesi
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.backend;
import android.location.Location;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import it.reyboz.bustorino.util.LinesNameSorter;
import org.osmdroid.api.IGeoPoint;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
public class Stop implements Comparable {
// remove "final" in case you need to set these from outside the parser\scrapers\fetchers
public final @NonNull String ID;
private @Nullable String name;
private @Nullable String username;
public @Nullable String location;
public @Nullable Route.Type type;
private @Nullable List routesThatStopHere;
private final @Nullable Double lat;
private final @Nullable Double lon;
// leave this non-final
private @Nullable String routesThatStopHereString = null;
private @Nullable String absurdGTTPlaceName = null;
//
public @Nullable String gtfsID = null;
/**
* Hey, look, method overloading!
*/
public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List routesThatStopHere) {
this.ID = ID;
this.name = name;
this.username = null;
this.location = (location != null && location.length() == 0) ? null : location;
this.type = type;
this.routesThatStopHere = routesThatStopHere;
this.lat = null;
this.lon = null;
}
/**
* Hey, look, method overloading!
*/
public Stop(final @NonNull String ID) {
this.ID = ID;
this.name = null;
this.username = null;
this.location = null;
this.type = null;
this.routesThatStopHere = null;
this.lat = null;
this.lon = null;
}
/**
* Constructor that sets EVERYTHING.
*/
public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName,
@Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere,
@Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) {
this.ID = ID;
this.name = name;
this.username = userName;
this.location = location;
this.type = type;
this.routesThatStopHere = routesThatStopHere;
this.lat = lat;
this.lon = lon;
this.gtfsID = gtfsID;
}
public @Nullable String routesThatStopHereToString() {
// M E M O I Z A T I O N
if(this.routesThatStopHereString != null) {
return this.routesThatStopHereString;
}
// no string yet? build it!
return buildRoutesString();
}
@Nullable
public String getAbsurdGTTPlaceName() {
return absurdGTTPlaceName;
}
public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) {
this.absurdGTTPlaceName = absurdGTTPlaceName;
}
public void setRoutesThatStopHere(@Nullable List routesThatStopHere) {
this.routesThatStopHere = routesThatStopHere;
}
protected void setRoutesThatStopHereString(String routesStopping){
this.routesThatStopHereString = routesStopping;
}
+ public int getNumRoutesStopping(){
+ if(this.routesThatStopHere == null) {
+ return 0;
+ } else {
+ return this.routesThatStopHere.size();
+ }
+ }
+
@Nullable
protected List getRoutesThatStopHere(){
return routesThatStopHere;
}
protected @Nullable String buildRoutesString() {
// no routes => no string
if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) {
return null;
}
StringBuilder sb = new StringBuilder();
Collections.sort(routesThatStopHere,new LinesNameSorter());
int i, lenMinusOne = routesThatStopHere.size() - 1;
for (i = 0; i < lenMinusOne; i++) {
sb.append(routesThatStopHere.get(i)).append(", ");
}
// last one:
sb.append(routesThatStopHere.get(i));
this.routesThatStopHereString = sb.toString();
return this.routesThatStopHereString;
}
@Override
public int compareTo(@NonNull Stop other) {
int res;
int thisAsInt = networkTools.failsafeParseInt(this.ID);
int otherAsInt = networkTools.failsafeParseInt(other.ID);
// numeric stop IDs
if(thisAsInt != 0 && otherAsInt != 0) {
return thisAsInt - otherAsInt;
} else {
// non-numeric
res = this.ID.compareTo(other.ID);
if (res != 0) {
return res;
}
}
// try with name, then
if(this.name != null && other.name != null) {
res = this.name.compareTo(other.name);
}
// and give up
return res;
}
/**
* Sets a name.
*
* @param name stop name as string (not null)
*/
public final void setStopName(@NonNull String name) {
this.name = name;
}
/**
* Sets user name. Empty string is converted to null.
*
* @param name a string of non-zero length, or null
*/
public final void setStopUserName(@Nullable String name) {
if(name == null) {
this.username = null;
} else if(name.length() == 0) {
this.username = null;
} else {
this.username = name;
}
}
/**
* Returns stop name or username (if set).
* - empty string means "already searched everywhere, can't find it"
* - null means "didn't search, yet. Maybe you should try."
* - string means "here's the name.", obviously.
*
* @return string if known, null if still unknown
*/
public final @Nullable String getStopDisplayName() {
if(this.username == null) {
return this.name;
} else {
return this.username;
}
}
/**
* Same as getStopDisplayName, only returns default name.
* I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName
* refers to the method exactly above this one and not some arcane and esoteric unknown symbol.
*/
public final @Nullable String getStopDefaultName() {
return this.name;
}
/**
* Same as getStopDisplayName, only returns user name.
* Also, never an empty string.
*/
public final @Nullable String getStopUserName() {
return this.username;
}
/**
* Gets username and name from other stop if they exist, sets itself accordingly.
*
* @param other another Stop
* @return did we actually set/change anything?
*/
public final boolean mergeNameFrom(Stop other) {
boolean ret = false;
if(other.name != null) {
if(this.name == null || !this.name.equals(other.name)) {
this.name = other.name;
ret = true;
}
}
if(other.username != null) {
if(this.username == null || !this.username.equals(other.username)) {
this.username = other.username;
ret = true;
}
}
return ret;
}
public final @Nullable String getGeoURL() {
if(this.lat == null || this.lon == null) {
return null;
}
// Android documentation suggests US for machine readable output (use dot as decimal separator)
return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon);
}
public final @Nullable String getGeoURLWithAddress() {
String url = getGeoURL();
if(url == null) {
return null;
}
if(this.location != null) {
try {
String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8"));
return url.concat(addThis);
} catch (Exception ignored) {}
}
return url;
}
@Nullable
public Double getLatitude() {
return lat;
}
@Nullable
public Double getLongitude() {
return lon;
}
public Double getDistanceFromLocation(IGeoPoint loc){
return getDistanceFromLocation(loc.getLatitude(), loc.getLongitude());
}
public Double getDistanceFromLocation(double latitude, double longitude){
if(this.lat!=null && this.lon !=null)
return utils.measuredistanceBetween(this.lat,this.lon,latitude, longitude);
else return Double.POSITIVE_INFINITY;
}
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString("ID", ID);
bundle.putString("name", name);
bundle.putString("username", username);
bundle.putString("location", location);
bundle.putString("type", (type != null) ? type.name() : null);
bundle.putStringArrayList("routesThatStopHere", (routesThatStopHere != null) ? new ArrayList<>(routesThatStopHere) : null);
if (lat != null) bundle.putDouble("lat", lat);
if (lon != null) bundle.putDouble("lon", lon);
if (gtfsID !=null) bundle.putString("gtfsID", gtfsID);
return bundle;
}
public static Stop fromBundle(Bundle bundle) {
String ID = bundle.getString("ID");
if (ID == null) throw new IllegalArgumentException("ID cannot be null");
String name = bundle.getString("name");
String username = bundle.getString("username");
String location = bundle.getString("location");
String typeStr = bundle.getString("type");
Route.Type type = (typeStr != null) ? Route.Type.valueOf(typeStr) : null;
List routesThatStopHere = bundle.getStringArrayList("routesThatStopHere");
Double lat = bundle.containsKey("lat") ? bundle.getDouble("lat") : null;
Double lon = bundle.containsKey("lon") ? bundle.getDouble("lon") : null;
String gtfsId = bundle.getString("gtfsID");
return new Stop(ID, name, username, location, type, routesThatStopHere, lat, lon, gtfsId);
}
}
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 bf18704..2a77edf 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt
@@ -1,407 +1,412 @@
package it.reyboz.bustorino.fragments
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.viewModels
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.map.Styles
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.maps.MapView
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
import org.maplibre.android.location.LocationComponent
import org.maplibre.android.location.LocationComponentActivationOptions
import org.maplibre.android.location.LocationComponentOptions
import org.maplibre.android.location.engine.LocationEngineRequest
import org.maplibre.android.location.modes.CameraMode
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.OnMapReadyCallback
import org.maplibre.android.maps.Style
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 ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [MapLibreFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class MapLibreFragment : Fragment(), OnMapReadyCallback {
//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
protected var map: MapLibreMap? = null
// Sources for stops and buses
private lateinit var stopsSource: GeoJsonSource
private lateinit var busesSource: GeoJsonSource
private var isStopsLayerStarted = false
private var lastStopsSizeShown = 0
private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0)
private lateinit var mapStyle: Style
//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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/*arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
*/
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)
// Init layout view
// Init the MapView
mapView = rootView.findViewById(R.id.libreMapView)
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)
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
// Setup close button
rootView.findViewById(R.id.btnClose).setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
isBottomSheetShowing = false
}
return rootView
}
override fun onMapReady(mapReady: MapLibreMap) {
this.map = mapReady
mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)).zoom(
15.0).build()
val mjson = Styles.getJsonStyleFromAsset(requireContext(), "map_style_good_noshops.json")//ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json")
activity?.run {
mapReady.setStyle(Style.Builder().fromJson(mjson!!)) { style ->
mapStyle = style
setupLayers(style)
// Start observing data
observeViewModels()
initLocation(style, mapReady, requireContext())
}
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.addOnMapClickListener { point ->
val screenPoint = mapReady.projection.toScreenLocation(point)
val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_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){
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
isBottomSheetShowing = false
}
showStopInBottomSheet(it)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
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
}
false
}
//makeStyleMapBoxUrl(false))
}
}
@SuppressLint("MissingPermission")
private fun initLocation(style: Style, map: MapLibreMap, context: Context){
locationComponent = map.locationComponent
val locationComponentOptions =
LocationComponentOptions.builder(context)
.pulseEnabled(true)
.build()
val locationComponentActivationOptions =
buildLocationComponentActivationOptions(style, locationComponentOptions, context)
locationComponent.activateLocationComponent(locationComponentActivationOptions)
locationComponent.isLocationComponentEnabled = true
locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING
locationComponent.forceLocationUpdate(lastLocation)
}
private fun buildLocationComponentActivationOptions(
style: Style,
locationComponentOptions: LocationComponentOptions,
context: Context
): LocationComponentActivationOptions {
return LocationComponentActivationOptions
.builder(context, style)
.locationComponentOptions(locationComponentOptions)
.useDefaultLocationEngine(true)
.locationEngineRequest(
LocationEngineRequest.Builder(750)
.setFastestInterval(750)
.setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
.build()
)
.build()
}
private fun startLayerStops(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, 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")
isStopsLayerStarted = true
}
/**
* Setup the Map Layers
*/
private fun setupLayers(style: Style) {
// Buses source
// TODO when adding the buses
//busesSource = GeoJsonSource(BUSES_SOURCE_ID)
//style.addSource(busesSource)
/*
// TODO when adding the buses
// Buses layer
val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply {
withProperties(
PropertyFactory.iconImage("bus"),
PropertyFactory.iconSize(1.0f),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconRotate(Expression.get("bearing"))
)
}
style.addLayer(busesLayer)
*/
}
private fun showStopInBottomSheet(stop: Stop?){
if (stop==null) return
- bottomLayout?.let { _ ->
+ bottomLayout?.let { lay ->
- //layout.findViewById(R.id.stopTitleTextView).text = stop.stopDefaultName//"${stop.ID} - ${stop.stopDefaultName}"
+ //lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}"
stopTitleTextView.text = stop.stopDefaultName
stopNumberTextView.text = stop.ID
- linesPassingTextView.text = stop.routesThatStopHereToString()
+ 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
+
}
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onResume() {
super.onResume()
mapView.onResume()
}
override fun onPause() {
super.onPause()
mapView.onPause()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
private fun observeViewModels() {
// Observe stops
stopsViewModel.stopsToShow.observe(viewLifecycleOwner) { stops ->
stopsShowing = stops
displayStops(stops)
}
}
/**
* 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, we have the same stop (can only increase!)")
return
}
val features = stops.mapNotNull { stop ->
stop.latitude?.let { lat ->
stop.longitude?.let { lon ->
Feature.fromGeometry(
Point.fromLngLat(lon, lat),
JsonObject().apply {
addProperty("id", stop.ID)
addProperty("name", stop.stopDefaultName)
addProperty("routes", stop.routesThatStopHereToString()) // Add routes array to JSON object
}
)
}
}
}
Log.d(DEBUG_TAG,"Have put ${features.size} stops to display")
if (isStopsLayerStarted) {
stopsSource.setGeoJson(FeatureCollection.fromFeatures(features))
lastStopsSizeShown = features.size
} else
map?.let { startLayerStops(mapStyle, FeatureCollection.fromFeatures(features))
Log.d(DEBUG_TAG,"Started stops layer on map")
lastStopsSizeShown = features.size
}
}
companion object {
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_IMAGE_ID ="bus-stop-icon"
private const val DEFAULT_CENTER_LAT = 45.0708
private const val DEFAULT_CENTER_LON = 7.6858
private const val POSITION_FOUND_ZOOM = 16.5
private const val NO_POSITION_ZOOM = 17.1
private const val ACCESS_TOKEN="KxO8lF4U3kiO63m0c7lzqDCDrMUVg1OA2JVzRXxxmYSyjugr1xpe4W4Db5rFNvbQ"
private const val MAPLIBRE_URL = "https://api.jawg.io/styles/"
private const val DEBUG_TAG = "BusTO-MapLibreFrag"
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment MapLibreFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
MapLibreFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
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