Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eeb086481 | |||
| 990b66e24f |
@@ -157,8 +157,8 @@ android {
|
|||||||
// • Keine Regressionen: Phantom-Layer-Fix, Lizenzsystem, Multi-Clip,
|
// • Keine Regressionen: Phantom-Layer-Fix, Lizenzsystem, Multi-Clip,
|
||||||
// PTT, Tour-Zähler, Backup/Cover, GPX-Import, Explorer/Routing,
|
// PTT, Tour-Zähler, Backup/Cover, GPX-Import, Explorer/Routing,
|
||||||
// Tour-/Waypoint-Kopien, Hintergrundwiedergabe.
|
// Tour-/Waypoint-Kopien, Hintergrundwiedergabe.
|
||||||
versionCode = 99
|
versionCode = 102
|
||||||
versionName = "2.5.45"
|
versionName = "2.5.48"
|
||||||
}
|
}
|
||||||
|
|
||||||
val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE")
|
val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE")
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ enum class MapBaseStyle(val key: String) {
|
|||||||
PERSPECTIVE("perspective");
|
PERSPECTIVE("perspective");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DEFAULT = MODERN_2D
|
val DEFAULT = CITY_3D
|
||||||
fun fromKey(key: String?): MapBaseStyle {
|
fun fromKey(key: String?): MapBaseStyle {
|
||||||
return when (key) {
|
return when (key) {
|
||||||
"modern_2d" -> MODERN_2D
|
"modern_2d" -> MODERN_2D
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import de.waypointaudio.data.TourAudioSettings
|
|||||||
import de.waypointaudio.data.TourCover
|
import de.waypointaudio.data.TourCover
|
||||||
import de.waypointaudio.data.TourPlaybackDefaults
|
import de.waypointaudio.data.TourPlaybackDefaults
|
||||||
import de.waypointaudio.data.TourRoute
|
import de.waypointaudio.data.TourRoute
|
||||||
|
import de.waypointaudio.data.TrackDraft
|
||||||
import de.waypointaudio.data.Waypoint
|
import de.waypointaudio.data.Waypoint
|
||||||
import de.waypointaudio.data.effectiveClips
|
import de.waypointaudio.data.effectiveClips
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
@@ -68,6 +69,10 @@ object BackupExportManager {
|
|||||||
// v2.5.5 — Tour-Cover (Icon + Palette) sind ein reines UI-Detail pro Tour
|
// v2.5.5 — Tour-Cover (Icon + Palette) sind ein reines UI-Detail pro Tour
|
||||||
// und wandern mit dem Tour-Export mit.
|
// und wandern mit dem Tour-Export mit.
|
||||||
val tourCovers: Map<String, TourCover> = emptyMap(),
|
val tourCovers: Map<String, TourCover> = emptyMap(),
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte (Name, Notiz, Markierungen, Punkte).
|
||||||
|
// gpsTracks bleibt für die Live-Aufzeichnung weiterhin im Backup-Input,
|
||||||
|
// wird aber NICHT mehr in track_drafts.json geschrieben.
|
||||||
|
val trackDrafts: List<TrackDraft> = emptyList(),
|
||||||
val appVersionName: String = "",
|
val appVersionName: String = "",
|
||||||
val appVersionCode: Int = 0
|
val appVersionCode: Int = 0
|
||||||
)
|
)
|
||||||
@@ -197,13 +202,17 @@ object BackupExportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----- data/track_drafts.json -----
|
// ----- data/track_drafts.json -----
|
||||||
|
// v2.5.48 FIX: Exportiert die echten TrackDraft-Objekte (Name, Notiz,
|
||||||
|
// Markierungen, Punkte) statt der gpsTracks-Map. Altes Format
|
||||||
|
// ("tracks": { tourName: [GpsTrackPoint] }) wird beim Import
|
||||||
|
// weiterhin toleriert (Abwärtskompatibilität).
|
||||||
if (selection.trackDrafts) {
|
if (selection.trackDrafts) {
|
||||||
val trackPayload = mapOf(
|
val trackPayload = mapOf(
|
||||||
"tracks" to input.gpsTracks
|
"drafts" to input.trackDrafts
|
||||||
)
|
)
|
||||||
writeJsonEntry(zip, "data/track_drafts.json", trackPayload)
|
writeJsonEntry(zip, "data/track_drafts.json", trackPayload)
|
||||||
val pointCount = input.gpsTracks.values.sumOf { it.size }
|
val pointCount = input.trackDrafts.sumOf { it.points.size }
|
||||||
sectionsWritten.add("Track-Entwürfe (${input.gpsTracks.size} Touren, $pointCount Punkte)")
|
sectionsWritten.add("Track-Entwürfe (${input.trackDrafts.size} Entwürfe, $pointCount Punkte)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- data/music_settings.json + media/... -----
|
// ----- data/music_settings.json + media/... -----
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import de.waypointaudio.data.TourAudioSettings
|
|||||||
import de.waypointaudio.data.TourPlaybackDefaults
|
import de.waypointaudio.data.TourPlaybackDefaults
|
||||||
import de.waypointaudio.data.TourCover
|
import de.waypointaudio.data.TourCover
|
||||||
import de.waypointaudio.data.TourRoute
|
import de.waypointaudio.data.TourRoute
|
||||||
|
import de.waypointaudio.data.TrackDraft
|
||||||
import de.waypointaudio.data.Waypoint
|
import de.waypointaudio.data.Waypoint
|
||||||
import de.waypointaudio.data.WaypointMusicBehavior
|
import de.waypointaudio.data.WaypointMusicBehavior
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -102,6 +103,8 @@ object BackupImportManager {
|
|||||||
val tourRoutesToWrite: Map<String, TourRoute> = emptyMap(),
|
val tourRoutesToWrite: Map<String, TourRoute> = emptyMap(),
|
||||||
// v2.5.5 — Tour-Cover, die geschrieben werden sollen (Map endgültiger Tourname → Cover).
|
// v2.5.5 — Tour-Cover, die geschrieben werden sollen (Map endgültiger Tourname → Cover).
|
||||||
val tourCoversToWrite: Map<String, TourCover> = emptyMap(),
|
val tourCoversToWrite: Map<String, TourCover> = emptyMap(),
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte, die als neue Drafts hinzugefügt werden sollen.
|
||||||
|
val trackDraftsToAdd: List<TrackDraft> = emptyList(),
|
||||||
val trackMinDistanceMeters: Int?,
|
val trackMinDistanceMeters: Int?,
|
||||||
val newTourSelection: String?,
|
val newTourSelection: String?,
|
||||||
val warnings: List<String>,
|
val warnings: List<String>,
|
||||||
@@ -514,42 +517,71 @@ object BackupImportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Track-Entwürfe ----
|
// ---- Track-Entwürfe ----
|
||||||
|
// v2.5.48 FIX: Importiert echte TrackDraft-Objekte aus dem "drafts"-Array.
|
||||||
|
// Abwärtskompatibilität: Alte Backups schrieben fälschlicherweise die
|
||||||
|
// gpsTracks-Map unter "tracks" in track_drafts.json. Falls "drafts" fehlt
|
||||||
|
// aber "tracks" vorhanden ist, wird das alte Format erkannt und toleriert
|
||||||
|
// (kein Crash, keine Datenverluste an bestehenden Touren/Waypoints).
|
||||||
val tracksToWrite = mutableMapOf<String, List<GpsTrackPoint>>()
|
val tracksToWrite = mutableMapOf<String, List<GpsTrackPoint>>()
|
||||||
|
val trackDraftsToAdd = mutableListOf<TrackDraft>()
|
||||||
var tracksImported = 0
|
var tracksImported = 0
|
||||||
if (selection.trackDrafts && draftsJson != null) {
|
if (selection.trackDrafts && draftsJson != null) {
|
||||||
val obj = runCatching {
|
val obj = runCatching {
|
||||||
JsonParser.parseString(draftsJson).asJsonObject
|
JsonParser.parseString(draftsJson).asJsonObject
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
val map: Map<String, List<GpsTrackPoint>> = if (obj != null) {
|
|
||||||
runCatching {
|
|
||||||
gson.fromJson<Map<String, List<GpsTrackPoint>>>(
|
|
||||||
obj.get("tracks"),
|
|
||||||
object : TypeToken<Map<String, List<GpsTrackPoint>>>() {}.type
|
|
||||||
) ?: emptyMap()
|
|
||||||
}.getOrDefault(emptyMap())
|
|
||||||
} else emptyMap()
|
|
||||||
|
|
||||||
map.forEach { (origTour, points) ->
|
if (obj != null) {
|
||||||
val mappedTour = tourRename[origTour]
|
// Neues Format (v2.5.48+): "drafts" enthält eine Liste echter TrackDraft-Objekte.
|
||||||
?: run {
|
val hasDraftsKey = obj.has("drafts") && !obj.get("drafts").isJsonNull
|
||||||
// Tour war nicht im Tour-Import (z. B. wenn nur Drafts importiert werden):
|
|
||||||
// prüfen, ob Name kollidiert.
|
if (hasDraftsKey) {
|
||||||
if (origTour == Waypoint.DEFAULT_TOUR_NAME) {
|
// Neues Format: Liste von TrackDraft-Objekten
|
||||||
origTour
|
val importedDrafts: List<TrackDraft> = runCatching {
|
||||||
} else if (origTour in resultTours) {
|
gson.fromJson<List<TrackDraft>>(
|
||||||
origTour
|
obj.get("drafts"),
|
||||||
} else {
|
object : TypeToken<List<TrackDraft>>() {}.type
|
||||||
resultTours.add(origTour)
|
) ?: emptyList()
|
||||||
origTour
|
}.getOrDefault(emptyList())
|
||||||
}
|
|
||||||
|
importedDrafts.forEach { draft ->
|
||||||
|
// Tourname-Mapping anwenden (für den Fall, dass Tour umbenannt wurde).
|
||||||
|
val origTour = draft.sourceTourName
|
||||||
|
val mappedTour = tourRename[origTour]
|
||||||
|
?: run {
|
||||||
|
if (origTour == Waypoint.DEFAULT_TOUR_NAME ||
|
||||||
|
origTour in resultTours
|
||||||
|
) {
|
||||||
|
origTour
|
||||||
|
} else {
|
||||||
|
resultTours.add(origTour)
|
||||||
|
origTour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Neue UUID vergeben, damit importierte Drafts nie mit
|
||||||
|
// vorhandenen Drafts-IDs kollidieren.
|
||||||
|
val newDraft = draft.copy(
|
||||||
|
id = java.util.UUID.randomUUID().toString(),
|
||||||
|
sourceTourName = mappedTour,
|
||||||
|
updatedAt = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
trackDraftsToAdd.add(newDraft)
|
||||||
|
tracksImported++
|
||||||
}
|
}
|
||||||
val existingPoints = existingTracks[mappedTour] ?: emptyList()
|
|
||||||
if (existingPoints.isEmpty()) {
|
|
||||||
// Bestehenden Track NICHT überschreiben.
|
|
||||||
tracksToWrite[mappedTour] = points
|
|
||||||
tracksImported++
|
|
||||||
} else {
|
} else {
|
||||||
warnings.add("Track-Entwurf für Tour „$mappedTour\" bereits vorhanden – nicht überschrieben.")
|
// Altes (fehlerhaftes) Format: "tracks" enthält gpsTracks-Map.
|
||||||
|
// Tolerieren ohne Crash, aber nicht in Tracks oder Drafts importieren,
|
||||||
|
// da der Inhalt GPS-Track-Daten (nicht Draft-Metadaten) sind.
|
||||||
|
// Hinweis in Warnings ausgeben.
|
||||||
|
val hasTracksKey = obj.has("tracks") && !obj.get("tracks").isJsonNull
|
||||||
|
if (hasTracksKey) {
|
||||||
|
warnings.add(
|
||||||
|
"Hinweis: track_drafts.json aus älterem Backup-Format erkannt " +
|
||||||
|
"(enthält GPS-Tracks statt Draft-Objekte). " +
|
||||||
|
"TrackDraft-Namen/Notizen können nicht wiederhergestellt werden. " +
|
||||||
|
"Bestehende Touren und Wegpunkte bleiben unverändert."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Keine Daten importieren aus altem, fehlerh. Format — kein Crash.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -636,6 +668,8 @@ object BackupImportManager {
|
|||||||
musicToWrite = musicToWrite,
|
musicToWrite = musicToWrite,
|
||||||
tourRoutesToWrite = tourRoutesToWrite,
|
tourRoutesToWrite = tourRoutesToWrite,
|
||||||
tourCoversToWrite = tourCoversToWrite,
|
tourCoversToWrite = tourCoversToWrite,
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte weitergeben.
|
||||||
|
trackDraftsToAdd = trackDraftsToAdd,
|
||||||
trackMinDistanceMeters = trackMinDistanceMeters,
|
trackMinDistanceMeters = trackMinDistanceMeters,
|
||||||
newTourSelection = newTourSelection,
|
newTourSelection = newTourSelection,
|
||||||
warnings = warnings,
|
warnings = warnings,
|
||||||
|
|||||||
@@ -503,34 +503,92 @@ fun ExplorerScreenMapLibre(
|
|||||||
// Schlüssel inkl. showPoiNames: Marker werden bei Toggle-Änderung neu gezeichnet.
|
// Schlüssel inkl. showPoiNames: Marker werden bei Toggle-Änderung neu gezeichnet.
|
||||||
// Fix: Kategorie-Bitmap wird per buildExplorerPoiIcon(cat, showPoiNames, name) mit
|
// Fix: Kategorie-Bitmap wird per buildExplorerPoiIcon(cat, showPoiNames, name) mit
|
||||||
// eindeutiger Kategorie-Kennung erzeugt — verhindert Icon-Cache-Kollisionen in MapLibre.
|
// eindeutiger Kategorie-Kennung erzeugt — verhindert Icon-Cache-Kollisionen in MapLibre.
|
||||||
|
// v2.5.46 — Atomare POI-Marker-Aktualisierung: erst neue Marker hinzufügen,
|
||||||
|
// dann alte entfernen. So gibt es keine sichtbare Lücke (kein Flackern) beim
|
||||||
|
// Kartennachladen. Außerdem: maximale Anzahl angezeigter Marker auf 280 begrenzt,
|
||||||
|
// benannte und wichtige Subtypen werden priorisiert.
|
||||||
LaunchedEffect(explorerPoiResults, styleReady, showPoiNames) {
|
LaunchedEffect(explorerPoiResults, styleReady, showPoiNames) {
|
||||||
if (!styleReady) return@LaunchedEffect
|
if (!styleReady) return@LaunchedEffect
|
||||||
val ml = mapLibreMap ?: return@LaunchedEffect
|
val ml = mapLibreMap ?: return@LaunchedEffect
|
||||||
// Alte Explorer-POI-Marker entfernen
|
if (explorerPoiResults.isEmpty()) {
|
||||||
|
// Nur leeren wenn wirklich leer (z.B. manueller Clear)
|
||||||
|
explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } }
|
||||||
|
explorerPoiMarkers.clear()
|
||||||
|
explorerPoiMarkerIdToResult.clear()
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
val iconFactory = IconFactory.getInstance(context)
|
||||||
|
|
||||||
|
// v2.5.46 — POI-Dichte-Begrenzung: max. 280 Marker pro Viewport,
|
||||||
|
// priorisiere benannte POIs und wichtige Subtypen.
|
||||||
|
val MAX_VISIBLE_POIS = 280
|
||||||
|
val importantSubtypes = setOf(
|
||||||
|
de.waypointaudio.util.PoiSubtype.CASTLE,
|
||||||
|
de.waypointaudio.util.PoiSubtype.CHURCH,
|
||||||
|
de.waypointaudio.util.PoiSubtype.MUSEUM,
|
||||||
|
de.waypointaudio.util.PoiSubtype.VIEWPOINT,
|
||||||
|
de.waypointaudio.util.PoiSubtype.MONUMENT,
|
||||||
|
de.waypointaudio.util.PoiSubtype.TRAIN_STATION,
|
||||||
|
de.waypointaudio.util.PoiSubtype.HOSPITAL,
|
||||||
|
de.waypointaudio.util.PoiSubtype.PHARMACY,
|
||||||
|
)
|
||||||
|
// Alle POIs flach aufsammeln, sortieren (benannte + wichtige zuerst)
|
||||||
|
val allPois: List<Pair<de.waypointaudio.data.PoiCategory, de.waypointaudio.util.ExplorerPoiResult>> =
|
||||||
|
explorerPoiResults.flatMap { (cat, pois) -> pois.map { cat to it } }
|
||||||
|
val totalCount = allPois.size
|
||||||
|
val sortedPois = allPois.sortedWith(
|
||||||
|
compareByDescending<Pair<de.waypointaudio.data.PoiCategory, de.waypointaudio.util.ExplorerPoiResult>> {
|
||||||
|
val poi = it.second
|
||||||
|
// Priorität: benannter POI + wichtiger Subtype = höchste Priorität
|
||||||
|
val isNamed = poi.displayName.isNotBlank() && poi.displayName != poi.subtype.displayLabel
|
||||||
|
val isImportant = poi.subtype in importantSubtypes
|
||||||
|
when {
|
||||||
|
isNamed && isImportant -> 3
|
||||||
|
isNamed -> 2
|
||||||
|
isImportant -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val visiblePois = if (totalCount > MAX_VISIBLE_POIS) sortedPois.take(MAX_VISIBLE_POIS) else sortedPois
|
||||||
|
val shownCount = visiblePois.size
|
||||||
|
|
||||||
|
// v2.5.46 — Status-Text (zeigt "N von M" wenn begrenzt)
|
||||||
|
if (totalCount > MAX_VISIBLE_POIS) {
|
||||||
|
autoPoiStatus = "POIs: $shownCount von $totalCount angezeigt"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Marker aufbauen (noch nicht auf der Karte)
|
||||||
|
val newMarkers = mutableListOf<org.maplibre.android.annotations.Marker>()
|
||||||
|
val newMarkerIdToResult = mutableMapOf<Long, de.waypointaudio.util.ExplorerPoiResult>()
|
||||||
|
|
||||||
|
for ((cat, poi) in visiblePois) {
|
||||||
|
// v2.5.45 — Subtype-Icon: Piktogramm variiert nach Untertyp, Farbe bleibt Kategorie
|
||||||
|
val iconBitmap = if (showPoiNames) {
|
||||||
|
buildExplorerPoiIconWithLabel(cat, poi.subtype, poi.displayName)
|
||||||
|
} else {
|
||||||
|
buildExplorerPoiIcon(cat, poi.subtype)
|
||||||
|
}
|
||||||
|
val icon = iconFactory.fromBitmap(iconBitmap)
|
||||||
|
val mo = MarkerOptions()
|
||||||
|
.position(LatLng(poi.latitude, poi.longitude))
|
||||||
|
.title(poi.displayName)
|
||||||
|
.snippet(poi.subtype.displayLabel) // v2.5.45: Subtype-Label statt categoryLabel
|
||||||
|
.icon(icon)
|
||||||
|
val marker = runCatching { ml.addMarker(mo) }.getOrNull() ?: continue
|
||||||
|
newMarkers.add(marker)
|
||||||
|
newMarkerIdToResult[marker.id] = poi
|
||||||
|
}
|
||||||
|
|
||||||
|
// v2.5.46 — ERST nach Hinzufügen der neuen Marker die alten entfernen
|
||||||
|
// → atomare Aktualisierung ohne sichtbare Lücke / Flackern
|
||||||
explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } }
|
explorerPoiMarkers.forEach { runCatching { ml.removeMarker(it) } }
|
||||||
explorerPoiMarkers.clear()
|
explorerPoiMarkers.clear()
|
||||||
explorerPoiMarkerIdToResult.clear()
|
explorerPoiMarkerIdToResult.clear()
|
||||||
if (explorerPoiResults.isEmpty()) return@LaunchedEffect
|
|
||||||
val iconFactory = IconFactory.getInstance(context)
|
explorerPoiMarkers.addAll(newMarkers)
|
||||||
for ((cat, pois) in explorerPoiResults) {
|
explorerPoiMarkerIdToResult.putAll(newMarkerIdToResult)
|
||||||
for (poi in pois) {
|
|
||||||
// v2.5.45 — Subtype-Icon: Piktogramm variiert nach Untertyp, Farbe bleibt Kategorie
|
|
||||||
val iconBitmap = if (showPoiNames) {
|
|
||||||
buildExplorerPoiIconWithLabel(cat, poi.subtype, poi.displayName)
|
|
||||||
} else {
|
|
||||||
buildExplorerPoiIcon(cat, poi.subtype)
|
|
||||||
}
|
|
||||||
val icon = iconFactory.fromBitmap(iconBitmap)
|
|
||||||
val mo = MarkerOptions()
|
|
||||||
.position(LatLng(poi.latitude, poi.longitude))
|
|
||||||
.title(poi.displayName)
|
|
||||||
.snippet(poi.subtype.displayLabel) // v2.5.45: Subtype-Label statt categoryLabel
|
|
||||||
.icon(icon)
|
|
||||||
val marker = runCatching { ml.addMarker(mo) }.getOrNull() ?: continue
|
|
||||||
explorerPoiMarkers.add(marker)
|
|
||||||
explorerPoiMarkerIdToResult[marker.id] = poi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var showSearchDialog by remember { mutableStateOf(false) }
|
var showSearchDialog by remember { mutableStateOf(false) }
|
||||||
@@ -2169,10 +2227,10 @@ fun ExplorerScreenMapLibre(
|
|||||||
lastAutoLoadCats, activeCats
|
lastAutoLoadCats, activeCats
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Debounce: 1.5 s
|
// v2.5.46: Debounce erhöht auf 2.5 s (vorher 1.5 s)
|
||||||
poiAutoLoadJob?.cancel()
|
poiAutoLoadJob?.cancel()
|
||||||
poiAutoLoadJob = scope.launch {
|
poiAutoLoadJob = scope.launch {
|
||||||
delay(1500L)
|
delay(2500L)
|
||||||
if (explorerPoiLoading) return@launch
|
if (explorerPoiLoading) return@launch
|
||||||
val bounds = ml.projection.visibleRegion.latLngBounds
|
val bounds = ml.projection.visibleRegion.latLngBounds
|
||||||
val bbox = de.waypointaudio.util.LatLngBbox(
|
val bbox = de.waypointaudio.util.LatLngBbox(
|
||||||
@@ -2196,7 +2254,7 @@ fun ExplorerScreenMapLibre(
|
|||||||
lastAutoLoadZoom = zoom
|
lastAutoLoadZoom = zoom
|
||||||
lastAutoLoadCats = activeCats
|
lastAutoLoadCats = activeCats
|
||||||
val total = results.values.sumOf { it.size }
|
val total = results.values.sumOf { it.size }
|
||||||
autoPoiStatus = if (total == 0) null else "$total POIs"
|
autoPoiStatus = if (total == 0) null else "POIs: $total"
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Alten Bestand behalten, nur dezente Fehlermeldung
|
// Alten Bestand behalten, nur dezente Fehlermeldung
|
||||||
autoPoiStatus = null
|
autoPoiStatus = null
|
||||||
@@ -2437,14 +2495,15 @@ fun ExplorerScreenMapLibre(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Status-Text
|
// v2.5.46 — Status-Text: nur bei echtem Laden "wird geladen",
|
||||||
|
// sonst ruhiger Status-Text
|
||||||
val statusText = when {
|
val statusText = when {
|
||||||
explorerPoiLoading -> "POIs werden aktualisiert…"
|
explorerPoiLoading -> "POIs laden…"
|
||||||
autoPoiStatus != null -> "POIs: $autoPoiStatus"
|
autoPoiStatus != null -> autoPoiStatus!!
|
||||||
autoPoiLoadEnabled && currentMapZoom < de.waypointaudio.util.ExplorerPoiClient.AUTO_LOAD_MIN_ZOOM
|
autoPoiLoadEnabled && currentMapZoom < de.waypointaudio.util.ExplorerPoiClient.AUTO_LOAD_MIN_ZOOM
|
||||||
-> "Hereinzoomen für Auto-Load"
|
-> "Hereinzoomen für Auto-Load"
|
||||||
autoPoiLoadEnabled -> "POIs: Auto-Laden aktiv"
|
autoPoiLoadEnabled -> "Auto-Laden aktiv"
|
||||||
else -> "POIs: manuell laden"
|
else -> "POIs: manuell"
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = statusText,
|
text = statusText,
|
||||||
@@ -3639,7 +3698,8 @@ private fun buildExplorerPoiIcon(
|
|||||||
): Bitmap {
|
): Bitmap {
|
||||||
// v2.5.43 — Echte Canvas-Piktogramme statt Buchstaben/Zeichen
|
// v2.5.43 — Echte Canvas-Piktogramme statt Buchstaben/Zeichen
|
||||||
// v2.5.45 — Subtype-Piktogramme für Sights/Gastro/Mobil/Infra
|
// v2.5.45 — Subtype-Piktogramme für Sights/Gastro/Mobil/Infra
|
||||||
val sizePx = 56
|
// v2.5.46 — Größe von 56 auf 68 px erhöht (bessere Lesbarkeit auf Smartphone-Screens)
|
||||||
|
val sizePx = 68
|
||||||
|
|
||||||
// Kategorie-Farben — dezent, App-Teal-Stil passend
|
// Kategorie-Farben — dezent, App-Teal-Stil passend
|
||||||
val fillColor = when (category) {
|
val fillColor = when (category) {
|
||||||
@@ -3740,9 +3800,10 @@ private fun buildExplorerPoiIcon(
|
|||||||
|
|
||||||
|
|
||||||
// ── Weißes Piktogramm per Canvas (v2.5.45: Subtype-Varianten) ──────────────────
|
// ── Weißes Piktogramm per Canvas (v2.5.45: Subtype-Varianten) ──────────────────
|
||||||
|
// v2.5.46: Strichstärke auf 2.8 erhöht (Icon-Größe 68 px statt 56 px)
|
||||||
paint.color = 0xFFFFFFFF.toInt()
|
paint.color = 0xFFFFFFFF.toInt()
|
||||||
paint.style = android.graphics.Paint.Style.FILL
|
paint.style = android.graphics.Paint.Style.FILL
|
||||||
paint.strokeWidth = 2.2f
|
paint.strokeWidth = 2.8f
|
||||||
|
|
||||||
when (subtype) {
|
when (subtype) {
|
||||||
// ─── SIGHTS Subtypes ────────────────────────────────────────────────────────
|
// ─── SIGHTS Subtypes ────────────────────────────────────────────────────────
|
||||||
@@ -4016,7 +4077,7 @@ private fun buildExplorerPoiIconWithLabel(
|
|||||||
subtype: de.waypointaudio.util.PoiSubtype = de.waypointaudio.util.PoiSubtype.GENERIC_SIGHTS,
|
subtype: de.waypointaudio.util.PoiSubtype = de.waypointaudio.util.PoiSubtype.GENERIC_SIGHTS,
|
||||||
name: String
|
name: String
|
||||||
): Bitmap {
|
): Bitmap {
|
||||||
val iconSize = 56 // gleiche Größe wie buildExplorerPoiIcon
|
val iconSize = 68 // v2.5.46: gleiche Größe wie buildExplorerPoiIcon (erhöht auf 68 px)
|
||||||
val labelMaxChars = 14
|
val labelMaxChars = 14
|
||||||
val displayName = if (name.length > labelMaxChars) name.take(labelMaxChars - 1) + "…" else name
|
val displayName = if (name.length > labelMaxChars) name.take(labelMaxChars - 1) + "…" else name
|
||||||
|
|
||||||
|
|||||||
@@ -160,11 +160,13 @@ object ExplorerPoiClient {
|
|||||||
private const val OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
private const val OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
||||||
private const val MAX_RADIUS_M = 5000
|
private const val MAX_RADIUS_M = 5000
|
||||||
|
|
||||||
// v2.5.45 — Auto-Load-Parameter
|
// v2.5.46 — Auto-Load-Parameter (erhöhte Schwellen für weniger Flackern)
|
||||||
/** Minimaler Zoom für automatisches Laden (unter diesem Zoom: kein Auto-Load). */
|
/** Minimaler Zoom für automatisches Laden (unter diesem Zoom: kein Auto-Load). */
|
||||||
const val AUTO_LOAD_MIN_ZOOM = 12.0
|
const val AUTO_LOAD_MIN_ZOOM = 12.0
|
||||||
/** Mindest-Kartenbewegung in Metern, bevor ein neuer Auto-Load ausgelöst wird. */
|
/** Mindest-Kartenbewegung in Metern, bevor ein neuer Auto-Load ausgelöst wird.
|
||||||
const val MOVE_THRESHOLD_M = 300.0
|
* v2.5.46: erhöht von 300 m auf 600 m, um überflüssige Nachlades bei kleinen
|
||||||
|
* Kamerabewegungen (Kippen, Heading-Änderungen, kleiner Zoom) zu vermeiden. */
|
||||||
|
const val MOVE_THRESHOLD_M = 600.0
|
||||||
|
|
||||||
// In-Memory-Cache: key = "kategorie|bboxFingerprint"
|
// In-Memory-Cache: key = "kategorie|bboxFingerprint"
|
||||||
private val cache = mutableMapOf<String, List<ExplorerPoiResult>>()
|
private val cache = mutableMapOf<String, List<ExplorerPoiResult>>()
|
||||||
@@ -195,8 +197,10 @@ object ExplorerPoiClient {
|
|||||||
if (lastAutoLat == null || lastAutoLon == null || lastAutoZoom == null || lastAutoCategories == null) return true
|
if (lastAutoLat == null || lastAutoLon == null || lastAutoZoom == null || lastAutoCategories == null) return true
|
||||||
if (activeCategories != lastAutoCategories) return true
|
if (activeCategories != lastAutoCategories) return true
|
||||||
val moved = haversineM(centerLat, centerLon, lastAutoLat, lastAutoLon)
|
val moved = haversineM(centerLat, centerLon, lastAutoLat, lastAutoLon)
|
||||||
|
// v2.5.46: Zoom-Delta-Schwelle erhöht auf 1.2, damit minimales Reinzoomen
|
||||||
|
// (z. B. Pitch-Änderung oder sehr kleiner Zoom) keinen Reload auslöst.
|
||||||
val zoomDelta = kotlin.math.abs(zoom - lastAutoZoom)
|
val zoomDelta = kotlin.math.abs(zoom - lastAutoZoom)
|
||||||
return moved > MOVE_THRESHOLD_M || zoomDelta > 0.8
|
return moved > MOVE_THRESHOLD_M || zoomDelta > 1.2
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1212,6 +1212,8 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
val tourRoutesMap = runCatching { repository.tourRoutes.first() }.getOrDefault(emptyMap())
|
val tourRoutesMap = runCatching { repository.tourRoutes.first() }.getOrDefault(emptyMap())
|
||||||
val tourCoversMap = runCatching { repository.tourCovers.first() }.getOrDefault(emptyMap())
|
val tourCoversMap = runCatching { repository.tourCovers.first() }.getOrDefault(emptyMap())
|
||||||
|
// v2.5.48 — Echte TrackDraft-Objekte für Backup lesen (nicht gpsTracks).
|
||||||
|
val trackDraftsSnapshot = runCatching { repository.trackDrafts.first() }.getOrDefault(emptyList())
|
||||||
|
|
||||||
val input = de.waypointaudio.io.BackupExportManager.BackupInput(
|
val input = de.waypointaudio.io.BackupExportManager.BackupInput(
|
||||||
waypoints = _waypoints.value,
|
waypoints = _waypoints.value,
|
||||||
@@ -1221,6 +1223,7 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
|||||||
musicSettings = musicSettingsMap,
|
musicSettings = musicSettingsMap,
|
||||||
tourRoutes = tourRoutesMap,
|
tourRoutes = tourRoutesMap,
|
||||||
tourCovers = tourCoversMap,
|
tourCovers = tourCoversMap,
|
||||||
|
trackDrafts = trackDraftsSnapshot,
|
||||||
appVersionName = pkgInfo?.versionName ?: "",
|
appVersionName = pkgInfo?.versionName ?: "",
|
||||||
appVersionCode = versionCode
|
appVersionCode = versionCode
|
||||||
)
|
)
|
||||||
@@ -1366,6 +1369,13 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trackDrafts) {
|
if (trackDrafts) {
|
||||||
|
// v2.5.48 FIX: Echte TrackDraft-Objekte importieren statt gpsTracks-Map.
|
||||||
|
// trackDraftsToAdd enthält bereits neue UUIDs (vergeben im ImportManager).
|
||||||
|
payload.trackDraftsToAdd.forEach { draft ->
|
||||||
|
repository.addTrackDraft(draft)
|
||||||
|
}
|
||||||
|
// tracksToWrite ist für die Live-GPS-Track-Map (gpsTracksFlow).
|
||||||
|
// Bei neuem Format leer; bei altem Format auch leer (kein Import).
|
||||||
payload.tracksToWrite.forEach { (tour, points) ->
|
payload.tracksToWrite.forEach { (tour, points) ->
|
||||||
repository.saveGpsTrack(tour, points)
|
repository.saveGpsTrack(tour, points)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user