mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-10-02 16:44:33 -04:00
fix(map-maplibre): clear warnings and collect state with lifecycle (#7460)
This commit is contained in:
1 parent
47c254402b
commit
fafea7bb04
15 files changed
+47
-198
No files matched your search
@@ -25,8 +25,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.maps.android.compose.MapsComposeExperimentalApi
|
||||
import com.google.maps.android.compose.Marker
|
||||
import com.google.maps.android.compose.rememberComposeBitmapDescriptor
|
||||
import com.google.maps.android.compose.MarkerComposable
|
||||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.app.map.convertIntToEmoji
|
||||
@@ -67,10 +66,6 @@ fun WaypointMarkers(
|
||||
|
||||
val iconCodePoint = waypoint.icon.waypointIconOrDefault()
|
||||
val emojiText = convertIntToEmoji(iconCodePoint)
|
||||
val icon =
|
||||
rememberComposeBitmapDescriptor(iconCodePoint) {
|
||||
Text(text = emojiText, fontSize = 32.sp, modifier = Modifier.padding(2.dp))
|
||||
}
|
||||
|
||||
// Non-visual cue: the geofence is otherwise only an orange overlay, so surface it in the marker's
|
||||
// accessible snippet for screen-reader and color-challenged users.
|
||||
@@ -88,12 +83,9 @@ fun WaypointMarkers(
|
||||
val cleanName = waypoint.name.replace('\n', ' ').replace('\b', ' ')
|
||||
val title = if (waypoint.isLocked) "${convertIntToEmoji(LOCK)} $cleanName" else cleanName
|
||||
|
||||
// rememberComposeBitmapDescriptor shares its applier with its content lambda, so the Text drawn into the
|
||||
// icon makes this map-scope function infer as UI.
|
||||
@Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
|
||||
Marker(
|
||||
MarkerComposable(
|
||||
iconCodePoint,
|
||||
state = markerState,
|
||||
icon = icon,
|
||||
title = title,
|
||||
snippet = snippet,
|
||||
visible = true,
|
||||
@@ -109,7 +101,9 @@ fun WaypointMarkers(
|
||||
else -> onDeleteWaypointRequest(waypoint)
|
||||
}
|
||||
},
|
||||
)
|
||||
) {
|
||||
Text(text = emojiText, fontSize = 32.sp, modifier = Modifier.padding(2.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ internal fun rememberRestoredCamera(): RestoredCamera? {
|
||||
val mapPrefs: MapPrefs = koinInject()
|
||||
var restored by remember { mutableStateOf<RestoredCamera?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(mapPrefs) {
|
||||
val saved = mapPrefs.awaitCameraPosition()
|
||||
restored =
|
||||
RestoredCamera(
|
||||
|
||||
+1
-2
@@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -232,7 +231,7 @@ private fun rememberMapScreenMapState(
|
||||
basemap = basemap,
|
||||
initialCameraPosition = restored.position ?: CameraPosition(),
|
||||
overlays = screen.overlays,
|
||||
layerOpacity = koinInject<LayerOpacityStore>().opacity.collectAsState().value,
|
||||
layerOpacity = koinInject<LayerOpacityStore>().opacity.collectAsStateWithLifecycle().value,
|
||||
customLayers = customLayers,
|
||||
onClusterMembers = { screen.clusterMembers = it },
|
||||
onWaypointClick = { screen.infoWaypointId = it },
|
||||
|
||||
+4
-34
@@ -17,11 +17,8 @@
|
||||
package org.meshtastic.feature.map.maplibre
|
||||
|
||||
import org.maplibre.spatialk.geojson.BoundingBox
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.tan
|
||||
import org.meshtastic.feature.map.maplibre.terrain.toGeoBounds
|
||||
import org.meshtastic.feature.map.terrain.TerrainTileMath
|
||||
|
||||
/**
|
||||
* How many tiles cover [this] region between [minZoom] and [maxZoom] inclusive.
|
||||
@@ -33,33 +30,6 @@ import kotlin.math.tan
|
||||
* Standard slippy-map arithmetic, so it matches what the renderer will actually request.
|
||||
*/
|
||||
internal fun BoundingBox.tileCount(minZoom: Int, maxZoom: Int): Long {
|
||||
if (maxZoom < minZoom) return 0L
|
||||
|
||||
return (minZoom..maxZoom).sumOf { zoom ->
|
||||
val span = 1 shl zoom
|
||||
val left = longitudeToTileX(west, span)
|
||||
val right = longitudeToTileX(east, span)
|
||||
// Tile rows run north to south, so the northern edge gives the lower index.
|
||||
val top = latitudeToTileY(north, span)
|
||||
val bottom = latitudeToTileY(south, span)
|
||||
|
||||
// A box straddling the antimeridian arrives with west > east: its columns wrap around the tile grid,
|
||||
// and the direct difference would go negative.
|
||||
val columns = if (right >= left) right - left + 1 else span - left + right + 1
|
||||
columns.toLong() * (bottom - top + 1).toLong()
|
||||
}
|
||||
val bounds = toGeoBounds()
|
||||
return (minZoom..maxZoom).sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) }
|
||||
}
|
||||
|
||||
private fun longitudeToTileX(longitude: Double, span: Int): Int =
|
||||
floor((longitude + HALF_TURN) / FULL_TURN * span).toInt().coerceIn(0, span - 1)
|
||||
|
||||
private fun latitudeToTileY(latitude: Double, span: Int): Int {
|
||||
// Clamped to the Mercator limit: the projection runs to infinity at the poles.
|
||||
val radians = latitude.coerceIn(-MERCATOR_LIMIT, MERCATOR_LIMIT) * PI / HALF_TURN
|
||||
val projected = ln(tan(radians) + 1.0 / cos(radians)) / PI
|
||||
return floor((1.0 - projected) / 2.0 * span).toInt().coerceIn(0, span - 1)
|
||||
}
|
||||
|
||||
private const val HALF_TURN = 180.0
|
||||
private const val FULL_TURN = 360.0
|
||||
private const val MERCATOR_LIMIT = 85.05112878
|
||||
+2
-2
@@ -19,9 +19,9 @@ package org.meshtastic.feature.map.maplibre.component
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -58,7 +58,7 @@ internal fun rememberCacheUsage(packs: Set<OfflinePack>): CacheUsage {
|
||||
}
|
||||
}
|
||||
}
|
||||
.collectAsState(CacheUsage())
|
||||
.collectAsStateWithLifecycle(CacheUsage())
|
||||
return usage
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -17,8 +17,8 @@
|
||||
package org.meshtastic.feature.map.maplibre.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.koin.compose.koinInject
|
||||
import org.meshtastic.feature.map.component.CustomMapLayersSheet
|
||||
import org.meshtastic.feature.map.layers.LayerOpacityStore
|
||||
@@ -35,8 +35,8 @@ import org.meshtastic.feature.map.layers.rememberMapLayerPicker
|
||||
fun ImportedLayersSlot() {
|
||||
val manager: MapLayersManager = koinInject()
|
||||
val opacityStore: LayerOpacityStore = koinInject()
|
||||
val layers by manager.mapLayers.collectAsState()
|
||||
val opacity by opacityStore.opacity.collectAsState()
|
||||
val layers by manager.mapLayers.collectAsStateWithLifecycle()
|
||||
val opacity by opacityStore.opacity.collectAsStateWithLifecycle()
|
||||
val picker = rememberMapLayerPicker(onPick = manager::addMapLayer)
|
||||
|
||||
CustomMapLayersSheet(
|
||||
|
||||
+2
-2
@@ -25,13 +25,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import org.meshtastic.core.resources.Res
|
||||
@@ -68,7 +68,7 @@ internal fun MapLayersButton(
|
||||
) {
|
||||
var sheetVisible by remember { mutableStateOf(false) }
|
||||
val opacityStore: LayerOpacityStore = koinInject()
|
||||
val opacity by opacityStore.opacity.collectAsState()
|
||||
val opacity by opacityStore.opacity.collectAsStateWithLifecycle()
|
||||
|
||||
MapButton(
|
||||
icon = MeshtasticIcons.Layers,
|
||||
|
||||
+3
-3
@@ -29,7 +29,6 @@ import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -37,6 +36,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -91,7 +91,7 @@ internal fun OfflineMapsSection(target: OfflineMapTarget, onShowRegion: (Boundin
|
||||
// the one every map here uses, so its packs are the ones the user sees on the map.
|
||||
val manager = DefaultMapRuntime.instance.offlineManager
|
||||
val scope = rememberCoroutineScope()
|
||||
val packs by manager.packs.collectAsState()
|
||||
val packs by manager.packs.collectAsStateWithLifecycle()
|
||||
// A pack definition now carries the pixel ratio it was downloaded at, so the tiles match this display.
|
||||
val pixelRatio = LocalDensity.current.density
|
||||
|
||||
@@ -165,7 +165,7 @@ private fun OfflinePackRow(
|
||||
onToggle: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
val progress = pack.downloadProgress.collectAsState().value
|
||||
val progress = pack.downloadProgress.collectAsStateWithLifecycle().value
|
||||
val bounds = (pack.definition as? OfflinePackDefinition.TilePyramid)?.bounds
|
||||
|
||||
Row(
|
||||
|
||||
+8
-8
@@ -30,13 +30,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.maplibre.spatialk.geojson.BoundingBox
|
||||
@@ -59,13 +59,13 @@ import org.meshtastic.core.ui.icon.Delete
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.feature.map.maplibre.terrain.OfflineTerrainRegion
|
||||
import org.meshtastic.feature.map.maplibre.terrain.OfflineTerrainRepository
|
||||
import org.meshtastic.feature.map.maplibre.terrain.estimateTerrainTiles
|
||||
import org.meshtastic.feature.map.maplibre.terrain.toBoundingBox
|
||||
import org.meshtastic.feature.map.maplibre.terrain.toGeoBounds
|
||||
import org.meshtastic.feature.map.terrain.GeoBounds
|
||||
import org.meshtastic.feature.map.terrain.MapterhornEndpoints
|
||||
import org.meshtastic.feature.map.terrain.TerrainDownloadState
|
||||
import org.meshtastic.feature.map.terrain.TerrainRegionExtractor
|
||||
import org.meshtastic.feature.map.terrain.terrainTileCount
|
||||
|
||||
/**
|
||||
* Offline terrain — hillshade and elevation contours for the viewport, downloaded as its own section of the layers
|
||||
@@ -80,17 +80,17 @@ import org.meshtastic.feature.map.terrain.TerrainRegionExtractor
|
||||
internal fun OfflineTerrainSection(target: OfflineMapTarget, onShowRegion: (BoundingBox) -> Unit) {
|
||||
val repository = remember { OfflineTerrainRepository.default }
|
||||
val scope = rememberCoroutineScope()
|
||||
val region by repository.region.collectAsState()
|
||||
val region by repository.region.collectAsStateWithLifecycle()
|
||||
// Not a local composable state: startDownload runs on the repository's own scope (see its doc comment for
|
||||
// why), so the state it reports has to be read from there too, or progress would appear to vanish the moment
|
||||
// this composable leaves composition and reappear wrong on the next one.
|
||||
val downloadState by repository.downloadState.collectAsState()
|
||||
val downloadState by repository.downloadState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) { repository.refresh() }
|
||||
LaunchedEffect(repository) { repository.refresh() }
|
||||
|
||||
val bounds = target.bounds()
|
||||
val maxZoom = target.terrainMaxZoom()
|
||||
val estimate = bounds?.let { estimateTerrainTiles(it.toGeoBounds(), maxZoom) } ?: 0L
|
||||
val estimate = bounds?.let { terrainTileCount(it.toGeoBounds(), maxZoom) } ?: 0L
|
||||
val overLimit = estimate > TerrainRegionExtractor.MAX_TILES
|
||||
val isDownloading = downloadState is TerrainDownloadState.InProgress
|
||||
|
||||
@@ -224,8 +224,8 @@ private fun DownloadedTerrainRow(region: OfflineTerrainRegion, onShow: () -> Uni
|
||||
* The zoom levels a terrain download covers: the current level plus a couple deeper, mirroring [OfflineMapTarget]'s own
|
||||
* private `zoomRange` convention for the base map's offline packs.
|
||||
*
|
||||
* Bounded by [MapterhornEndpoints.REGIONAL_MAX_ZOOM] rather than MapLibre's own 0..20, since [estimateTerrainTiles] and
|
||||
* [TerrainRegionExtractor] never fetch anything deeper than that regardless of what is asked for.
|
||||
* Bounded by [MapterhornEndpoints.REGIONAL_MAX_ZOOM] rather than MapLibre's own 0..20, since [terrainTileCount] and
|
||||
* [TerrainRegionExtractor] never count or fetch anything deeper than that regardless of what is asked for.
|
||||
*/
|
||||
private fun OfflineMapTarget.terrainMaxZoom(): Int {
|
||||
val current = zoom().toInt().coerceIn(0, MapterhornEndpoints.REGIONAL_MAX_ZOOM)
|
||||
|
||||
+2
-2
@@ -17,7 +17,6 @@
|
||||
package org.meshtastic.feature.map.maplibre.layers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.produceState
|
||||
@@ -27,6 +26,7 @@ import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.LocalPlatformContext
|
||||
@@ -185,7 +185,7 @@ private fun rememberLayerIcons(urls: Set<String>): Map<String, Painter> {
|
||||
key(url) {
|
||||
val painter =
|
||||
rememberAsyncImagePainter(ImageRequest.Builder(LocalPlatformContext.current).data(url).build())
|
||||
val state by painter.state.collectAsState()
|
||||
val state by painter.state.collectAsStateWithLifecycle()
|
||||
// The loaded painter, not the async wrapper around it: MapLibre rasterizes a painter outside the
|
||||
// composition driving it, where an AsyncImagePainter draws nothing.
|
||||
(state as? AsyncImagePainter.State.Success)?.let { loaded[url] = it.painter }
|
||||
|
||||
+3
-3
@@ -18,11 +18,11 @@ package org.meshtastic.feature.map.maplibre.layers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.maplibre.compose.expressions.dsl.asNumber
|
||||
@@ -74,9 +74,9 @@ import org.meshtastic.feature.map.terrain.TerrainTileMath
|
||||
@Composable
|
||||
internal fun TerrainLayers(viewportBounds: BoundingBox?, zoom: Double, displayUnits: MeasurementSystem) {
|
||||
val repository = remember { OfflineTerrainRepository.default }
|
||||
val region by repository.region.collectAsState()
|
||||
val region by repository.region.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) { repository.refresh() }
|
||||
LaunchedEffect(repository) { repository.refresh() }
|
||||
|
||||
// Two early returns (detekt's ReturnCount limit), kept inline rather than in a helper function: a helper
|
||||
// returning a resolved nullable can't hand the compiler back a smart-cast on *this* function's own
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.maplibre.terrain
|
||||
|
||||
import org.meshtastic.feature.map.terrain.GeoBounds
|
||||
import org.meshtastic.feature.map.terrain.MapterhornEndpoints
|
||||
import org.meshtastic.feature.map.terrain.TerrainTileMath
|
||||
|
||||
/**
|
||||
* How many tiles [TerrainRegionExtractor][org.meshtastic.feature.map.terrain.TerrainRegionExtractor] would fetch for
|
||||
* [bounds] up to [maxZoom] — the global tier always, the regional tier only where [MapterhornEndpoints.regionalUrlFor]
|
||||
* finds one.
|
||||
*
|
||||
* The counting itself is [TerrainTileMath.tileCountAt] — the same O(1)-per-zoom corner arithmetic the extractor uses to
|
||||
* bound memory before it enumerates tiles, so this and the extractor can never silently disagree on *how many* a zoom
|
||||
* level covers. What's re-derived here, network-free, is only the *which zoom levels / which tier* decision from the
|
||||
* extractor's own suspending `download` [kotlinx.coroutines.flow.Flow] — matching
|
||||
* [org.meshtastic.feature.map.maplibre.tileCount]'s own "shown before a download starts" role for the base offline
|
||||
* layer. If the extractor's own tier/zoom-range logic ever changes, this must change with it — that coupling is
|
||||
* intentional, not an oversight; a unit test pins it against [MapterhornEndpoints]'s real constants rather than a copy
|
||||
* of them.
|
||||
*/
|
||||
internal fun estimateTerrainTiles(bounds: GeoBounds, maxZoom: Int): Long {
|
||||
val globalZoomRange = 0..minOf(maxZoom, MapterhornEndpoints.GLOBAL_MAX_ZOOM)
|
||||
val globalCount = globalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) }
|
||||
|
||||
val hasRegionalTier =
|
||||
maxZoom > MapterhornEndpoints.GLOBAL_MAX_ZOOM && MapterhornEndpoints.regionalUrlFor(bounds) != null
|
||||
val regionalCount =
|
||||
if (hasRegionalTier) {
|
||||
val regionalMaxZoom = minOf(maxZoom, MapterhornEndpoints.REGIONAL_MAX_ZOOM)
|
||||
val regionalZoomRange = MapterhornEndpoints.REGIONAL_MIN_ZOOM..regionalMaxZoom
|
||||
regionalZoomRange.sumOf { zoom -> TerrainTileMath.tileCountAt(zoom, bounds) }
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
|
||||
return globalCount + regionalCount
|
||||
}
|
||||
+2
-3
@@ -18,7 +18,6 @@ package org.meshtastic.feature.map.maplibre.geojson
|
||||
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.maplibre.spatialk.geojson.LineString
|
||||
import org.meshtastic.feature.map.terrain.ContourLine
|
||||
import org.meshtastic.feature.map.terrain.ContourPoint
|
||||
import org.meshtastic.feature.map.terrain.TerrainTileMath
|
||||
@@ -38,7 +37,7 @@ class ContourFeaturesTest {
|
||||
val features = contourLinesToFeatures(tile, listOf(line), zoom = 12, metric = true)
|
||||
|
||||
assertEquals(1, features.size)
|
||||
assertEquals(2, (features.single().geometry as LineString).coordinates.size)
|
||||
assertEquals(2, features.single().geometry.coordinates.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,7 +50,7 @@ class ContourFeaturesTest {
|
||||
fun `tile-local points convert to the same lat lon TerrainTileMath's own inverse would produce`() {
|
||||
val line = ContourLine(elevationMeters = 100f, points = listOf(ContourPoint(0f, 0f), ContourPoint(1f, 1f)))
|
||||
val feature = contourLinesToFeatures(tile, listOf(line), zoom = 12, metric = true).single()
|
||||
val positions = (feature.geometry as LineString).coordinates
|
||||
val positions = feature.geometry.coordinates
|
||||
|
||||
val expectedFirst = TerrainTileMath.lonLatAt(tile, 0f, 0f)
|
||||
assertEquals(expectedFirst.longitude, positions.first().longitude)
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.maplibre.terrain
|
||||
|
||||
import org.meshtastic.feature.map.terrain.GeoBounds
|
||||
import org.meshtastic.feature.map.terrain.MapterhornEndpoints
|
||||
import org.meshtastic.feature.map.terrain.TerrainTileMath
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TerrainDownloadEstimateTest {
|
||||
|
||||
private val tinyBounds = GeoBounds(south = 47.6, west = -122.4, north = 47.61, east = -122.39)
|
||||
|
||||
@Test
|
||||
fun `matches a manual sum of tilesAt across the global zoom range only`() {
|
||||
val maxZoom = MapterhornEndpoints.GLOBAL_MAX_ZOOM
|
||||
val expected = (0..maxZoom).sumOf { zoom -> TerrainTileMath.tilesAt(zoom, tinyBounds).size.toLong() }
|
||||
|
||||
assertEquals(expected, estimateTerrainTiles(tinyBounds, maxZoom))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a maxZoom at or below the global ceiling never counts a regional tier`() {
|
||||
val globalOnly = estimateTerrainTiles(tinyBounds, MapterhornEndpoints.GLOBAL_MAX_ZOOM)
|
||||
val deeper = estimateTerrainTiles(tinyBounds, MapterhornEndpoints.GLOBAL_MAX_ZOOM + 1)
|
||||
|
||||
// The regional tier only exists past the global ceiling — one more zoom level must add regional tiles too.
|
||||
assertTrue(deeper > globalOnly)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a huge box that never fits a single regional archive gets no regional tier`() {
|
||||
// Spans more than one z6 tile, so MapterhornEndpoints.regionalUrlFor returns null.
|
||||
val huge = GeoBounds(south = -60.0, west = -170.0, north = 60.0, east = 170.0)
|
||||
|
||||
val atGlobalCeiling = estimateTerrainTiles(huge, MapterhornEndpoints.GLOBAL_MAX_ZOOM)
|
||||
val past = estimateTerrainTiles(huge, MapterhornEndpoints.GLOBAL_MAX_ZOOM + 1)
|
||||
|
||||
// Past the global ceiling with no regional archive, nothing further is fetched — the count doesn't grow.
|
||||
assertEquals(atGlobalCeiling, past)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `negative maxZoom counts nothing`() {
|
||||
assertEquals(0L, estimateTerrainTiles(tinyBounds, -1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `maxZoom 0 counts the single world-covering z0 tile`() {
|
||||
// Unlike a negative maxZoom, 0 is a real, valid zoom range (0..0) -- it legitimately counts the one z0 tile
|
||||
// that covers the whole world, not nothing.
|
||||
assertEquals(1L, estimateTerrainTiles(tinyBounds, 0))
|
||||
}
|
||||
}
|
||||
+10
@@ -42,4 +42,14 @@ class TerrainTileCountTest {
|
||||
assertEquals(8_869_485L, globalOnly)
|
||||
assertEquals(globalOnly, terrainTileCount(continentalBox, maxZoom = MapterhornEndpoints.REGIONAL_MAX_ZOOM))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a negative maxZoom counts nothing`() {
|
||||
assertEquals(0L, terrainTileCount(tinySeattleBox, maxZoom = -1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `maxZoom 0 counts the one tile that covers the world`() {
|
||||
assertEquals(1L, terrainTileCount(tinySeattleBox, maxZoom = 0))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user