From ce60d490b78a48af9a0d21ad2967695ec4c8086f Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:33:06 -0500 Subject: [PATCH] fix: map regressions (#3004) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../mesh/android/prefs/GoogleMapsPrefs.kt | 2 + .../com/geeksville/mesh/ui/map/MapView.kt | 18 ++- .../geeksville/mesh/ui/map/MapViewModel.kt | 110 ++++++++++++------ .../android/prefs/StringSetPrefDelegate.kt | 35 ++++++ 4 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt diff --git a/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt b/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt index 3365f8129..bf39bc344 100644 --- a/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt +++ b/app/src/google/java/com/geeksville/mesh/android/prefs/GoogleMapsPrefs.kt @@ -24,10 +24,12 @@ import com.google.maps.android.compose.MapType interface GoogleMapsPrefs { var selectedGoogleMapType: String? var selectedCustomTileUrl: String? + var hiddenLayerUrls: Set } class GoogleMapsPrefsImpl(prefs: SharedPreferences) : GoogleMapsPrefs { override var selectedGoogleMapType: String? by NullableStringPrefDelegate(prefs, "selected_google_map_type", MapType.NORMAL.name) override var selectedCustomTileUrl: String? by NullableStringPrefDelegate(prefs, "selected_custom_tile_url", null) + override var hiddenLayerUrls: Set by StringSetPrefDelegate(prefs, "hidden_layer_urls", emptySet()) } diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt index f0f557002..9a2b4eb6a 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt @@ -217,7 +217,16 @@ fun MapView( var mapTypeMenuExpanded by remember { mutableStateOf(false) } var showCustomTileManagerSheet by remember { mutableStateOf(false) } - val cameraPositionState = rememberCameraPositionState {} + val cameraPositionState = rememberCameraPositionState { + position = + CameraPosition.fromLatLngZoom( + LatLng( + ourNodeInfo?.position?.latitudeI?.times(DEG_D) ?: 0.0, + ourNodeInfo?.position?.longitudeI?.times(DEG_D) ?: 0.0, + ), + 7f, + ) + } // Location tracking functionality val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } @@ -279,7 +288,12 @@ fun MapView( } } - DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } } + DisposableEffect(Unit) { + onDispose { + fusedLocationClient.removeLocationUpdates(locationCallback) + mapViewModel.clearLoadedLayerData() + } + } val allNodes by mapViewModel.nodes diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt index a0beb072c..30b1a557e 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -231,8 +232,11 @@ constructor( val mapLayers: StateFlow> = _mapLayers.asStateFlow() init { + viewModelScope.launch { + customTileProviderRepository.getCustomTileProviders().first() + loadPersistedMapType() + } loadPersistedLayers() - loadPersistedMapType() } private fun loadPersistedMapType() { @@ -271,6 +275,7 @@ constructor( val persistedLayerFiles = layersDir.listFiles() if (persistedLayerFiles != null) { + val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls val loadedItems = persistedLayerFiles.mapNotNull { file -> if (file.isFile) { @@ -286,10 +291,11 @@ constructor( } layerType?.let { + val uri = Uri.fromFile(file) MapLayerItem( name = file.nameWithoutExtension, - uri = Uri.fromFile(file), - isVisible = true, + uri = uri, + isVisible = !hiddenLayerUrls.contains(uri.toString()), layerType = it, ) } @@ -372,7 +378,25 @@ constructor( } fun toggleLayerVisibility(layerId: String) { - _mapLayers.value = _mapLayers.value.map { if (it.id == layerId) it.copy(isVisible = !it.isVisible) else it } + var toggledLayer: MapLayerItem? = null + val updatedLayers = + _mapLayers.value.map { + if (it.id == layerId) { + toggledLayer = it.copy(isVisible = !it.isVisible) + toggledLayer + } else { + it + } + } + _mapLayers.value = updatedLayers + + toggledLayer?.let { + if (it.isVisible) { + googleMapsPrefs.hiddenLayerUrls -= it.uri.toString() + } else { + googleMapsPrefs.hiddenLayerUrls += it.uri.toString() + } + } } fun removeMapLayer(layerId: String) { @@ -383,7 +407,10 @@ constructor( LayerType.GEOJSON -> layerToRemove.geoJsonLayerData?.removeLayerFromMap() null -> {} } - layerToRemove?.uri?.let { uri -> deleteFileToInternalStorage(uri) } + layerToRemove?.uri?.let { uri -> + deleteFileToInternalStorage(uri) + googleMapsPrefs.hiddenLayerUrls -= uri.toString() + } _mapLayers.value = _mapLayers.value.filterNot { it.id == layerId } } } @@ -418,40 +445,55 @@ constructor( if (layerItem.kmlLayerData != null || layerItem.geoJsonLayerData != null) return try { when (layerItem.layerType) { - LayerType.KML -> { - val kmlLayer = - getInputStreamFromUri(layerItem)?.use { KmlLayer(map, it, application.applicationContext) } - _mapLayers.update { currentLayers -> - currentLayers.map { - if (it.id == layerItem.id) { - it.copy(kmlLayerData = kmlLayer) - } else { - it - } - } - } - } - LayerType.GEOJSON -> { - val geoJsonLayer = - getInputStreamFromUri(layerItem)?.use { inputStream -> - val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() }) - GeoJsonLayer(map, jsonObject) - } - _mapLayers.update { currentLayers -> - currentLayers.map { - if (it.id == layerItem.id) { - it.copy(geoJsonLayerData = geoJsonLayer) - } else { - it - } - } - } - } + LayerType.KML -> loadKmlLayerIfNeeded(layerItem, map) + + LayerType.GEOJSON -> loadGeoJsonLayerIfNeeded(layerItem, map) } } catch (e: Exception) { Timber.tag("MapViewModel").e(e, "Error loading map layer for ${layerItem.uri}") } } + + private suspend fun loadKmlLayerIfNeeded(layerItem: MapLayerItem, map: GoogleMap) { + val kmlLayer = + getInputStreamFromUri(layerItem)?.use { + KmlLayer(map, it, application.applicationContext).apply { + if (!layerItem.isVisible) removeLayerFromMap() + } + } + _mapLayers.update { currentLayers -> + currentLayers.map { + if (it.id == layerItem.id) { + it.copy(kmlLayerData = kmlLayer) + } else { + it + } + } + } + } + + private suspend fun loadGeoJsonLayerIfNeeded(layerItem: MapLayerItem, map: GoogleMap) { + val geoJsonLayer = + getInputStreamFromUri(layerItem)?.use { inputStream -> + val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() }) + GeoJsonLayer(map, jsonObject).apply { if (!layerItem.isVisible) removeLayerFromMap() } + } + _mapLayers.update { currentLayers -> + currentLayers.map { + if (it.id == layerItem.id) { + it.copy(geoJsonLayerData = geoJsonLayer) + } else { + it + } + } + } + } + + fun clearLoadedLayerData() { + _mapLayers.update { currentLayers -> + currentLayers.map { it.copy(kmlLayerData = null, geoJsonLayerData = null) } + } + } } enum class LayerType { diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt new file mode 100644 index 000000000..714b4936b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/StringSetPrefDelegate.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * 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 com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences +import androidx.core.content.edit +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class StringSetPrefDelegate( + private val prefs: SharedPreferences, + private val key: String, + private val defaultValue: Set, +) : ReadWriteProperty> { + override fun getValue(thisRef: Any?, property: KProperty<*>): Set = + prefs.getStringSet(key, defaultValue) ?: emptySet() + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) = + prefs.edit { putStringSet(key, value) } +}