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 a2da413..79ddce7 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java @@ -1,308 +1,341 @@ /* 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; } @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 536a00e..0a0eca4 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -1,152 +1,337 @@ 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 androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.viewModels +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.maps.MapView import org.maplibre.android.geometry.LatLng +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 { - // TODO: Rename and change types of parameters + //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) + + protected var map: MapLibreMap? = null + // Sources for stops and buses + private lateinit var stopsSource: GeoJsonSource + private lateinit var busesSource: GeoJsonSource 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) + 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") } return rootView } override fun onMapReady(mapReady: MapLibreMap) { this.map = mapReady - mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)).zoom(9.0).build() + mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)).zoom( + 15.0).build() activity?.run { - //TODO: copy from TransportR - //val mapStyle = makeStyleUrl("jawg-terrain") - /*if (mapStyle != null && mapReady.style?.uri != mapStyle) { - mapReady.setStyle(mapStyle, ::onMapStyleLoaded) //callback + mapReady.setStyle(Styles.CARTO_VOYAGER ) { style -> + setupSources(style) + setupLayers(style) + + // Start observing data + observeViewModels() + initLocation(style, mapReady, requireContext()) + } + + mapReady.addOnCameraIdleListener { + map?.let { + stopsViewModel.loadStopsInLatLngBounds(it.projection.visibleRegion.latLngBounds) + } + } - */ - mapReady.setStyle(OPENSTREETMAP_HOT_URL ) //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() + } + + /** + * Setup the Map Layers + */ + private fun setupLayers(style: Style) { + // add icon + ResourcesCompat.getDrawable(resources,R.drawable.bus_stop, activity?.theme) + ?.let { style.addImage(STOP_IMAGE_ID, it) } + // Stops layer + val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID).apply { + withProperties( + PropertyFactory.iconImage(STOP_IMAGE_ID), + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconIgnorePlacement(true) + + ) + } + style.addLayer(stopsLayer) + + /* + // 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) + + */ + } + + /** + * Setup data sources for the map + */ + private fun setupSources(style: Style) { + // Stops source + stopsSource = GeoJsonSource(STOPS_SOURCE_ID,) + style.addSource(stopsSource) + + // Buses source + // TODO when adding the buses + //busesSource = GeoJsonSource(BUSES_SOURCE_ID) + //style.addSource(busesSource) + } + + + /** + * Incremental updates of the layers + */ + fun updateLayerIncrementally(newPoints: List, layerSourceId: String) { + val source = map?.style?.getSourceAs(layerSourceId) ?: return + + //source.querySourceFeatures(null) + // Get existing features + val existingFeatures = source.querySourceFeatures(null).toMutableList() + + // Add new features + val newFeatures = newPoints.map { point -> + Feature.fromGeometry(point) } + existingFeatures.addAll(newFeatures) + + // Update source + source.setGeoJson(FeatureCollection.fromFeatures(existingFeatures)) } 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 == null) return + + val features = stops.mapNotNull { stop -> + stop.latitude?.let { lat -> + stop.longitude?.let { lon -> + Feature.fromGeometry( + Point.fromLngLat(lat, lon), + JsonObject().apply { + addProperty("id", stop.ID) + addProperty("name", stop.stopDefaultName) + addProperty("routes", stop.routesThatStopHereToString()) // Add routes array to JSON object + } + ) + } + } + } + Log.d("MapLibreFrag","Have put ${features.size} stops to display") + + stopsSource.setGeoJson(FeatureCollection.fromFeatures(features)) + } + 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 = 18.3 + private const val POSITION_FOUND_ZOOM = 16.5 + private const val NO_POSITION_ZOOM = 17.1 private const val ACCESS_TOKEN="KxO8lF4U3kiO63m0c7lzqDCDrMUVg1OA2JVzRXxxmYSyjugr1xpe4W4Db5rFNvbQ" - const val NO_POSITION_ZOOM = 17.1 private const val MAPLIBRE_URL = "https://api.jawg.io/styles/" - private const val OPENSTREETMAP_URL = "https://raw.githubusercontent.com/go2garret/maps/main/src/assets/json/openStreetMap.json" - private const val OPENSTREETMAP_HOT_URL="https://raw.githubusercontent.com/fabmazz/maps/refs/heads/main/src/assets/json/openStreetMap-hot.json" + /** * 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 diff --git a/app/src/main/java/it/reyboz/bustorino/map/Styles.kt b/app/src/main/java/it/reyboz/bustorino/map/Styles.kt new file mode 100644 index 0000000..d797f66 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/Styles.kt @@ -0,0 +1,50 @@ +package it.reyboz.bustorino.map +import org.maplibre.android.maps.Style + +object Styles { + const val DEMOTILES = "https://demotiles.maplibre.org/style.json" + + const val VERSATILES = "https://tiles.versatiles.org/assets/styles/colorful.json" + + const val AMERICANA = "https://americanamap.org/style.json" + + const val OPENFREEMAP_LIBERY = "https://tiles.openfreemap.org/styles/liberty" + + const val OPENFREEMAP_BRIGHT = "https://tiles.openfreemap.org/styles/bright" + + const val AWS_OPEN_DATA_STANDARD_LIGHT = + "https://maps.geo.us-east-2.amazonaws.com/maps/v0/maps/OpenDataStyle/style-descriptor?key=v1.public.eyJqdGkiOiI1NjY5ZTU4My0yNWQwLTQ5MjctODhkMS03OGUxOTY4Y2RhMzgifR_7GLT66TNRXhZJ4KyJ-GK1TPYD9DaWuc5o6YyVmlikVwMaLvEs_iqkCIydspe_vjmgUVsIQstkGoInXV_nd5CcmqRMMa-_wb66SxDdbeRDvmmkpy2Ow_LX9GJDgL2bbiCws0wupJPFDwWCWFLwpK9ICmzGvNcrPbX5uczOQL0N8V9iUvziA52a1WWkZucIf6MUViFRf3XoFkyAT15Ll0NDypAzY63Bnj8_zS8bOaCvJaQqcXM9lrbTusy8Ftq8cEbbK5aMFapXRjug7qcrzUiQ5sr0g23qdMvnKJQFfo7JuQn8vwAksxrQm6A0ByceEXSfyaBoVpFcTzEclxUomhY.NjAyMWJkZWUtMGMyOS00NmRkLThjZTMtODEyOTkzZTUyMTBi" + + private fun protomaps(style: String): String { + return "https://api.protomaps.com/styles/v2/${style}.json?key=e761cc7daedf832a" + } + + 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" + + val PROTOMAPS_LIGHT = protomaps("light") + + val PROTOMAPS_DARK = protomaps("dark") + + val PROTOMAPS_GRAYSCALE = protomaps("grayscale") + + val PROTOMAPS_WHITE = protomaps("white") + + val PROTOMAPS_BLACK = protomaps("black") + + val CARTO_DARK = makeStyleMapBoxUrl(true) + + val CARTO_VOYAGER = makeStyleMapBoxUrl(false) + + fun getPredefinedStyleWithFallback(name: String): String { + try { + val style = Style.getPredefinedStyle(name) + return style + } catch (e: Exception) { + return OPENFREEMAP_LIBERY + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt index 9d07f4f..3dd8948 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt @@ -1,45 +1,73 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.map -import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop -import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository -import it.reyboz.bustorino.data.gtfs.GtfsDatabase +import org.maplibre.android.geometry.LatLngBounds import org.osmdroid.util.BoundingBox -import java.util.ArrayList import java.util.concurrent.Executors +import kotlin.collections.ArrayList class StopsMapViewModel(application: Application): AndroidViewModel(application) { private val executor = Executors.newFixedThreadPool(2) private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) + val stopsToShow = MutableLiveData(ArrayList()) + private var stopsShownIDs = HashSet() + val stopsInBoundingBox = MutableLiveData>() private val callback = OldDataRepository.Callback> { res -> if(res.isSuccess){ stopsInBoundingBox.postValue(res.result) Log.d(DEBUG_TAG, "Setting value of stops in bounding box") } } + private val addStopsCallback = + OldDataRepository.Callback> { res -> + if(res.isSuccess) res.result?.let{ newStops -> + val stopsAdd = stopsToShow.value ?: ArrayList() + for (s in newStops){ + if (s.ID !in stopsShownIDs){ + stopsShownIDs.add(s.ID) + stopsAdd.add(s) + } + } + + stopsToShow.postValue(stopsAdd) + Log.d(DEBUG_TAG, "Loaded ${stopsAdd.size} stops in total") + } + } + fun requestStopsInBoundingBox(bb: BoundingBox) { bb.let { Log.d(DEBUG_TAG, "Launching stop request") oldRepo.requestStopsInArea(it.latSouth, it.latNorth, it.lonWest, it.lonEast, callback) } } + fun requestStopsInLatLng(bb: LatLngBounds) { + bb.let { + Log.d(DEBUG_TAG, "Launching stop request") + oldRepo.requestStopsInArea(it.latitudeSouth, it.latitudeNorth, it.longitudeWest, it.longitudeEast, callback) + } + } + fun loadStopsInLatLngBounds(bb: LatLngBounds?){ + bb?.let { + Log.d(DEBUG_TAG, "Launching stop request") + oldRepo.requestStopsInArea(it.latitudeSouth, it.latitudeNorth, it.longitudeWest, it.longitudeEast, + addStopsCallback) + } + } companion object{ private const val DEBUG_TAG = "BusTOStopMapViewModel" } } \ No newline at end of file