From ebe32ed15389b34616e0af842f35cb264728f7ba Mon Sep 17 00:00:00 2001 From: Iu_yang1 Date: Fri, 25 Sep 2026 14:42:29 +0800 Subject: [PATCH] feat(map): add TianDiTu and AutoTLE sources --- .../core/data/repository/SettingsRepo.kt | 42 ++++- .../look4sat/core/domain/model/Settings.kt | 16 +- .../look4sat/core/domain/source/Sources.kt | 2 + .../src/main/res/values-tr/strings.xml | 5 +- .../src/main/res/values-zh/strings.xml | 12 +- .../src/main/res/values/strings.xml | 13 +- .../look4sat/feature/map/MapScreen.kt | 145 ++++++++++++++++-- .../rtbishop/look4sat/feature/map/MapState.kt | 3 + .../look4sat/feature/map/MapViewModel.kt | 4 + .../feature/settings/MapSettingsDialog.kt | 119 ++++++++++++++ .../feature/settings/SettingsScreen.kt | 31 ++++ .../feature/settings/SettingsState.kt | 1 + .../feature/settings/SettingsViewModel.kt | 3 + 13 files changed, 371 insertions(+), 25 deletions(-) create mode 100644 feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/MapSettingsDialog.kt diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index 3c8de86b..39a7c228 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -25,6 +25,7 @@ import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationManagerCompat import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings import com.rtbishop.look4sat.core.domain.model.DatabaseState +import com.rtbishop.look4sat.core.domain.model.MapSource import com.rtbishop.look4sat.core.domain.model.OtherSettings import com.rtbishop.look4sat.core.domain.model.PassesSettings import com.rtbishop.look4sat.core.domain.model.RCSettings @@ -98,6 +99,8 @@ class SettingsRepo( private val keySstvMode = "sstvMode" private val keyLowElevation = "lowElevation" private val keyHighElevation = "highElevation" + private val keyMapSource = "mapSource" + private val keyTiandituKey = "tiandituKey" private val keyUseCustomTle = "useCustomTle" private val keyUseCustomTransceivers = "useCustomTransceivers" private val keyTleUrl = "tleUrl" @@ -107,6 +110,7 @@ class SettingsRepo( private val keySatelliteEnabled = "satelliteEnabled" private val keyTransceiversEnabled = "transceiversEnabled" private val keySatnogsTleSourceMigration = "satnogsTleSourceMigration" + private val keyAutoTleSourceMigration = "autoTleSourceMigration" private val separatorComma = "," private val separatorUrl = "\n" private val legacyCelestrakSatnogsUrl = @@ -597,7 +601,12 @@ class SettingsRepo( override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) { _otherSettings.update { current -> - val new = transform(current) + val new = transform(current).let { + it.copy( + mapSource = MapSource.normalize(it.mapSource), + tiandituKey = it.tiandituKey.trim() + ) + } preferences.edit { putBoolean(keyStateOfAutoUpdate, new.stateOfAutoUpdate) putBoolean(keyStateOfAutoLotwSync, new.stateOfAutoLotwSync) @@ -614,6 +623,8 @@ class SettingsRepo( putString(keySstvMode, new.sstvMode) putLong(keyLowElevation, new.lowElevation.toRawBits()) putLong(keyHighElevation, new.highElevation.toRawBits()) + putString(keyMapSource, new.mapSource) + putString(keyTiandituKey, new.tiandituKey) } new } @@ -634,7 +645,9 @@ class SettingsRepo( shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true), sstvMode = preferences.getString(keySstvMode, null) ?: "Auto", lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())), - highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())) + highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())), + mapSource = MapSource.normalize(preferences.getString(keyMapSource, null).orEmpty()), + tiandituKey = preferences.getString(keyTiandituKey, null).orEmpty().trim() ) //endregion @@ -656,6 +669,7 @@ class SettingsRepo( putString(keySatelliteEnabled, normalized.satelliteEnabled.joinToString(separatorComma)) putString(keyTransceiversEnabled, normalized.transceiversEnabled.joinToString(separatorComma)) putBoolean(keySatnogsTleSourceMigration, true) + putBoolean(keyAutoTleSourceMigration, true) } _dataSourcesSettings.value = normalized } @@ -666,7 +680,7 @@ class SettingsRepo( defaultUrls = Sources.satelliteDataUrls.values.filter { it.isNotBlank() }, legacyEnabledKey = keyUseCustomTle, legacyUrlKey = keyTleUrl - ).migrateSatnogsTleSource(), + ).migrateSatnogsTleSource().migrateAutoTleSources(), transceiversUrls = getDataSourceUrls( key = keyTransceiversUrls, defaultUrls = Sources.transceiversDataUrls.values.filter { it.isNotBlank() }, @@ -727,6 +741,28 @@ class SettingsRepo( return migrated } + private fun List.migrateAutoTleSources(): List { + if (preferences.getBoolean(keyAutoTleSourceMigration, false)) return this + val autoTleUrls = listOfNotNull( + Sources.satelliteDataUrls["BI4PYM AutoTLE (GitHub)"], + Sources.satelliteDataUrls["BI4PYM AutoTLE (Mirror)"] + ).filter { it.isNotBlank() } + val missingUrls = autoTleUrls.filterNot { containsSourceUrl(it) } + if (missingUrls.isEmpty()) { + preferences.edit { putBoolean(keyAutoTleSourceMigration, true) } + return this + } + + val migrated = this + missingUrls + val enabled = alignFlags(migrated, readEnabledFlags(keySatelliteEnabled)) + preferences.edit { + putString(keySatelliteUrls, migrated.joinToString(separatorUrl)) + putString(keySatelliteEnabled, enabled.joinToString(separatorComma)) + putBoolean(keyAutoTleSourceMigration, true) + } + return migrated + } + private fun List.containsSourceUrl(url: String): Boolean = any { isSameSourceUrl(it, url) } private fun isSameSourceUrl(first: String, second: String): Boolean = diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt index d2fe269d..e98d648e 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt @@ -75,9 +75,23 @@ data class OtherSettings( * ephemeris auto-update; only effective after at least one manual sync). */ val stateOfAutoLotwSync: Boolean = true, /** 指南针手动航向偏置(度, -180..180): 雷达方位 = 传感器方位 + 磁偏角 + 该偏置. */ - val compassOffsetDegrees: Float = 0f + val compassOffsetDegrees: Float = 0f, + val mapSource: String = MapSource.OSM, + val tiandituKey: String = "" ) +object MapSource { + const val OSM = "osm" + const val TIANDITU_VECTOR = "tianditu_vector" + const val TIANDITU_IMAGE = "tianditu_image" + private const val LEGACY_TIANDITU = "tianditu" + + fun normalize(source: String): String = when (source) { + OSM, TIANDITU_VECTOR, TIANDITU_IMAGE -> source + LEGACY_TIANDITU -> TIANDITU_VECTOR + else -> OSM + } +} data class DataSourcesSettings( val satelliteUrls: List, val transceiversUrls: List, diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt index d4c41022..b256bd2c 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt @@ -21,6 +21,8 @@ object Sources { val satelliteDataUrls = mapOf( "LAPAN-A2" to "https://www.kaggle.com/api/v1/datasets/download/muazamnugroho/lapan-a2-satellite-two-line-element-tle-dataset/LAPAN-A2_TLE_latest.txt", "R4UAB" to "https://r4uab.ru/satonline.txt", + "BI4PYM AutoTLE (GitHub)" to "https://raw.githubusercontent.com/BI4PYM/AutoTLE/refs/heads/master/AutoTLE.txt", + "BI4PYM AutoTLE (Mirror)" to "https://autotle.bi4pym.cn/AutoTLE.txt", "All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv", "SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le", "Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv", diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index df091a0a..f3de6b20 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -88,7 +88,7 @@ Önceki Sonraki - © OpenStreetMap contributors + © OpenStreetMap contributors Azimut: %.1f° Yükseklik açısı: %.1f° İrtifa: %.0f km @@ -209,7 +209,8 @@ \n• Alexandru Csete (Gpredict) \n• Dr T.S. Kelso (Celestrak) \n• Libre Space Foundation (SatNOGS) - \n• BG7NTA BG5JSU (CW decoding feature) + \n• BG7NTA BG5JSU (CW decoding feature) + \n• BH2VSQ BG5JSU (TianDiTu rendering support) Bu uygulama herhangi bir garanti sunmaz diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 712d2a40..7ccb8cb7 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -247,6 +247,15 @@ 启用 CAT 控制 其他设置 + 地图设置 + 配置地图 + 地图来源 + 离线 OSM 地图 + 天地图(矢量) + 天地图(影像) + 天地图访问密钥 + 使用天地图前,需先在天地图开发者控制台申请服务端应用 Key。 + 选择天地图需要填写访问密钥;保存密钥前将继续使用离线 OSM 地图。 以 UTC 时间显示 启用卫星数据自动更新 自动同步 LoTW 已确认网格 @@ -282,7 +291,8 @@ \n• Dr T.S. Kelso (Celestrak) \n• Libre Space Foundation (SatNOGS) \n• xdsopl 和 Robot36 贡献者! - \n• BG7NTA BG5JSU(CW解码功能) + \n• BG7NTA BG5JSU(CW解码功能) + \n• BH2VSQ BG5JSU(天地图渲染支持) 该应用程序不提供任何保修. diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index c8f77def..daeba1ab 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -128,7 +128,8 @@ Prev Next - © OpenStreetMap contributors + © OpenStreetMap contributors + © Tianditu Grid mode Satellite mode First call @@ -276,6 +277,15 @@ Check your network permission Other settings + Map settings + Configure map + Map source + Offline OSM + TianDiTu vector + TianDiTu imagery + TianDiTu access key + Request a server-side application key from the TianDiTu developer console before using TianDiTu maps. + A TianDiTu key is required. Offline OSM remains active until it is entered. Show pass time in UTC Enable automatic data update Auto-sync LoTW confirmed grids @@ -312,6 +322,7 @@ \n* Libre Space Foundation (SatNOGS) \n* xdsopl and Robot36 contributors! \n* BG7NTA BG5JSU (CW decoding feature) + \n* BH2VSQ BG5JSU (TianDiTu rendering support) The app comes with no warranty diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt index 9366f37e..63148708 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt @@ -85,6 +85,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.rtbishop.look4sat.core.domain.model.AwardCalculator import com.rtbishop.look4sat.core.domain.model.AwardProgress import com.rtbishop.look4sat.core.domain.model.AwardType +import com.rtbishop.look4sat.core.domain.model.MapSource import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.domain.predict.OrbitalObject import com.rtbishop.look4sat.core.domain.predict.OrbitalPos @@ -96,31 +97,40 @@ import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.isVerticalLayout import com.rtbishop.look4sat.core.presentation.layoutPadding +import org.osmdroid.tileprovider.MapTileProviderBasic +import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase import org.osmdroid.tileprovider.tilesource.XYTileSource import org.osmdroid.events.DelayedMapListener import org.osmdroid.events.MapListener import org.osmdroid.events.ScrollEvent import org.osmdroid.events.ZoomEvent import org.osmdroid.util.GeoPoint +import org.osmdroid.util.MapTileIndex import org.osmdroid.views.CustomZoomButtonsController import org.osmdroid.views.MapView import org.osmdroid.views.overlay.FolderOverlay import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polyline +import org.osmdroid.views.overlay.TilesOverlay +import java.net.URLEncoder // Overlay indices -private const val OVERLAY_GRID = 0 -private const val OVERLAY_STATION = 1 -private const val OVERLAY_TRACK = 2 -private const val OVERLAY_FOOTPRINT = 3 -private const val OVERLAY_POSITIONS = 4 -private const val OVERLAY_TERMINATOR = 5 -private const val OVERLAY_SUN = 6 -private const val OVERLAY_MOON = 7 -private const val OVERLAY_COUNT = 8 +private const val OVERLAY_TDT_LABELS = 0 +private const val OVERLAY_GRID = 1 +private const val OVERLAY_STATION = 2 +private const val OVERLAY_TRACK = 3 +private const val OVERLAY_FOOTPRINT = 4 +private const val OVERLAY_POSITIONS = 5 +private const val OVERLAY_TERMINATOR = 6 +private const val OVERLAY_SUN = 7 +private const val OVERLAY_MOON = 8 +private const val OVERLAY_COUNT = 9 private val minLat = MapView.getTileSystem().minLatitude private val maxLat = MapView.getTileSystem().maxLatitude +private val offlineTileSource = XYTileSource("tiles", 0, 6, 256, ".webp", emptyArray()) +private const val OFFLINE_MAX_ZOOM = 7.0 +private const val TIANDITU_MAX_ZOOM = 18.0 private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { strokeWidth = 3f style = Paint.Style.STROKE @@ -209,6 +219,8 @@ private fun MapScreen( val rotateMod = Modifier.rotate(180f) val timeString = uiState.mapData?.aosTime ?: "00:00:00" val isTimeAos = uiState.mapData?.isTimeAos ?: true + val usesTianditu = tiandituLayers(uiState.mapSource) != null && uiState.tiandituKey.isNotBlank() + val copyrightResId = if (usesTianditu) R.string.map_copyright_tianditu else R.string.map_copyright_osm // Tapped worked grid -> centered QSO dialog. Local UI state: the map is the // only consumer and it resets when leaving the page. @@ -300,6 +312,9 @@ private fun MapScreen( val firstPos = uiState.track?.firstOrNull()?.firstOrNull() ?: return@LaunchedEffect mapView.controller.animateTo(GeoPoint(firstPos.latitude, firstPos.longitude)) } + LaunchedEffect(uiState.mapSource, uiState.tiandituKey) { + configureTileSources(mapView, uiState.mapSource, uiState.tiandituKey) + } Column(modifier = Modifier.layoutPadding(), verticalArrangement = Arrangement.spacedBy(6.dp)) { val isVertical = isVerticalLayout() if (isGridMode) { @@ -382,7 +397,8 @@ private fun MapScreen( } uiState.mapData?.let { mapData -> if (!uiState.isGridMode) { - if (isVertical) MapDataCard(mapData) else MapDataCards(mapData) + if (isVertical) MapDataCard(mapData, copyrightResId) + else MapDataCards(mapData, copyrightResId) } } // Top-left: operated-grid selector for VUCC counting (grid mode only). @@ -761,11 +777,11 @@ private fun formatTime(epochMs: Long, isUtc: Boolean): String { // region Map data composables @Composable -private fun MapDataCard(data: MapData) { +private fun MapDataCard(data: MapData, copyrightResId: Int) { val textColor = MaterialTheme.colorScheme.primary val cardColors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest) Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = stringResource(R.string.map_copyright), fontSize = 14.sp) + Text(text = stringResource(copyrightResId), fontSize = 14.sp) Card(colors = cardColors) { Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { MapDataRow( @@ -803,7 +819,7 @@ private fun MapDataRow( } @Composable -private fun MapDataCards(data: MapData) { +private fun MapDataCards(data: MapData, copyrightResId: Int) { val cardColors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest) val paddingMod = Modifier .padding(horizontal = 8.dp, vertical = 4.dp) @@ -830,7 +846,7 @@ private fun MapDataCards(data: MapData) { } } Text( - text = stringResource(R.string.map_copyright), + text = stringResource(copyrightResId), fontSize = 14.sp, modifier = Modifier.align(Alignment.BottomCenter) ) @@ -1045,6 +1061,100 @@ private fun FirstCallToggle( // endregion +// region Tile sources +private var configuredTileMapView: MapView? = null +private var configuredTileSource: String? = null + +private fun configureTileSources(mapView: MapView, mapSource: String, tiandituKey: String) { + val key = tiandituKey.trim() + val layers = tiandituLayers(mapSource) + val sourceKey = if (layers == null || key.isBlank()) MapSource.OSM else "${layers.base}:$key" + if (configuredTileMapView === mapView && configuredTileSource == sourceKey) return + + configuredTileMapView = mapView + configuredTileSource = sourceKey + (mapView.overlays.getOrNull(OVERLAY_TDT_LABELS) as? TilesOverlay)?.onDetach(mapView) + + if (layers == null || key.isBlank()) { + mapView.setUseDataConnection(false) + mapView.setTileSource(offlineTileSource) + mapView.maxZoomLevel = OFFLINE_MAX_ZOOM + mapView.overlayManager.tilesOverlay.applyTileOverlayDefaults(createColorFilter()) + mapView.overlays[OVERLAY_TDT_LABELS] = FolderOverlay() + requestTileRefresh(mapView) + return + } + + mapView.setUseDataConnection(true) + mapView.setTileSource(TiandituTileSource(layer = layers.base, key = key)) + mapView.maxZoomLevel = TIANDITU_MAX_ZOOM + mapView.overlayManager.tilesOverlay.applyTileOverlayDefaults(null) + mapView.overlays[OVERLAY_TDT_LABELS] = TilesOverlay( + MapTileProviderBasic(mapView.context, TiandituTileSource(layer = layers.label, key = key)), + mapView.context + ).apply { + applyTileOverlayDefaults(null) + setUseDataConnection(true) + } + requestTileRefresh(mapView) +} + +private data class TiandituLayers(val base: String, val label: String) + +private fun tiandituLayers(mapSource: String): TiandituLayers? = when (MapSource.normalize(mapSource)) { + MapSource.TIANDITU_VECTOR -> TiandituLayers(base = "vec", label = "cva") + MapSource.TIANDITU_IMAGE -> TiandituLayers(base = "img", label = "cia") + else -> null +} + +private fun TilesOverlay.applyTileOverlayDefaults(colorFilter: ColorMatrixColorFilter?) { + setColorFilter(colorFilter) + setLoadingBackgroundColor(Color.TRANSPARENT) + setLoadingLineColor(Color.TRANSPARENT) +} + +private fun requestTileRefresh(mapView: MapView) { + mapView.post { + mapView.requestLayout() + mapView.invalidate() + } +} + +private fun buildTiandituWmtsUrl( + baseUrl: String, + layer: String, + zoom: Int, + row: Int, + col: Int, + encodedKey: String +): String = "$baseUrl?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=$layer" + + "&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=$zoom&TILEROW=$row&TILECOL=$col&tk=$encodedKey" + +private class TiandituTileSource( + private val layer: String, + key: String +) : OnlineTileSourceBase( + "tianditu-wmts-https-$layer", + 1, + TIANDITU_MAX_ZOOM.toInt(), + 256, + ".png", + Array(8) { index -> "https://t$index.tianditu.gov.cn/${layer}_w/wmts" }, + "Tianditu" +) { + private val encodedKey = URLEncoder.encode(key, Charsets.UTF_8.name()) + + override fun getTileURLString(pMapTileIndex: Long): String = buildTiandituWmtsUrl( + baseUrl = getBaseUrl(), + layer = layer, + zoom = MapTileIndex.getZoom(pMapTileIndex), + row = MapTileIndex.getY(pMapTileIndex), + col = MapTileIndex.getX(pMapTileIndex), + encodedKey = encodedKey + ) +} +// endregion + // region Map overlay helpers /** @@ -1463,7 +1573,6 @@ private fun rememberMapViewWithLifecycle( mapFilterViewModel: MapFilterViewModel, restoreViewport: Boolean ): MapView { - val tileSource = XYTileSource("tiles", 0, 6, 256, ".webp", emptyArray()) val context = LocalContext.current val isVertical = isVerticalLayout() val mapView = remember { @@ -1475,9 +1584,9 @@ private fun rememberMapViewWithLifecycle( // draws a full-height artifact line whenever its X range crosses // the screen at low zoom. The map must never repeat vertically. setVerticalMapRepetitionEnabled(false) - setTileSource(tileSource) + setTileSource(offlineTileSource) minZoomLevel = getMinZoom(resources.displayMetrics.heightPixels, isVertical) - maxZoomLevel = 7.0 + maxZoomLevel = OFFLINE_MAX_ZOOM // Restore the viewport the user left on the previous visit, but only // in grid mode: satellite mode intentionally keeps its original // behavior (default center, then follows the selected satellite). @@ -1522,6 +1631,8 @@ private fun clearMapCaches() { lastPositions = null lastAction = null lastMapView = null + configuredTileMapView = null + configuredTileSource = null } @Composable diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt index 6510ee34..ba9915e6 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt @@ -21,11 +21,14 @@ import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.domain.predict.OrbitalObject import com.rtbishop.look4sat.core.domain.predict.OrbitalPass import com.rtbishop.look4sat.core.domain.predict.OrbitalPos +import com.rtbishop.look4sat.core.domain.model.MapSource data class MapState( val mapData: MapData? = null, val isLightUi: Boolean = false, val isUtc: Boolean = false, + val mapSource: String = MapSource.OSM, + val tiandituKey: String = "", val isGridMode: Boolean = false, /** Grid mode: label worked (green) cells with the first callsign worked in * that grid instead of the grid code; non-worked cells get no label. */ diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt index dee6c078..179149d2 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt @@ -63,6 +63,8 @@ class MapViewModel( MapState( isLightUi = settingsRepo.otherSettings.value.stateOfLightTheme, isUtc = settingsRepo.otherSettings.value.stateOfUtc, + mapSource = settingsRepo.otherSettings.value.mapSource, + tiandituKey = settingsRepo.otherSettings.value.tiandituKey, orbitalPass = defaultPass ) ) @@ -82,6 +84,8 @@ class MapViewModel( _uiState.update { it.copy( isUtc = settings.stateOfUtc, + mapSource = settings.mapSource, + tiandituKey = settings.tiandituKey, isGridMode = settings.stateOfMapGrid, showFirstCallLabels = settings.stateOfMapFirstCall ) diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/MapSettingsDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/MapSettingsDialog.kt new file mode 100644 index 00000000..646e452a --- /dev/null +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/MapSettingsDialog.kt @@ -0,0 +1,119 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * 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.rtbishop.look4sat.feature.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.rtbishop.look4sat.core.domain.model.MapSource +import com.rtbishop.look4sat.core.domain.model.OtherSettings +import com.rtbishop.look4sat.core.presentation.R +import com.rtbishop.look4sat.core.presentation.SharedDialog + +@Composable +fun MapSettingsDialog( + settings: OtherSettings, + onDismiss: () -> Unit, + onSave: (mapSource: String, tiandituKey: String) -> Unit +) { + var selectedSource by rememberSaveable { + mutableStateOf(MapSource.normalize(settings.mapSource)) + } + var tiandituKey by rememberSaveable { mutableStateOf(settings.tiandituKey) } + + SharedDialog( + title = stringResource(R.string.prefs_map_title), + onDismissRequest = onDismiss, + onCancel = onDismiss, + onAccept = { onSave(selectedSource, tiandituKey) } + ) { padding -> + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = padding) + ) { + Text( + text = stringResource(R.string.prefs_map_source_title), + color = MaterialTheme.colorScheme.primary + ) + FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + MapSourceChip( + selected = selectedSource == MapSource.OSM, + label = stringResource(R.string.prefs_map_source_osm), + onClick = { selectedSource = MapSource.OSM } + ) + MapSourceChip( + selected = selectedSource == MapSource.TIANDITU_VECTOR, + label = stringResource(R.string.prefs_map_source_tianditu_vector), + onClick = { selectedSource = MapSource.TIANDITU_VECTOR } + ) + MapSourceChip( + selected = selectedSource == MapSource.TIANDITU_IMAGE, + label = stringResource(R.string.prefs_map_source_tianditu_image), + onClick = { selectedSource = MapSource.TIANDITU_IMAGE } + ) + } + Text( + text = stringResource(R.string.prefs_map_tianditu_key_help), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + OutlinedTextField( + value = tiandituKey, + onValueChange = { tiandituKey = it }, + label = { Text(stringResource(R.string.prefs_map_tianditu_key)) }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + if (selectedSource != MapSource.OSM && tiandituKey.isBlank()) { + Text( + text = stringResource(R.string.prefs_map_tianditu_key_required), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + } + } + } +} + +@Composable +private fun MapSourceChip(selected: Boolean, label: String, onClick: () -> Unit) { + FilterChip(selected = selected, onClick = onClick, label = { Text(label) }) +} diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index c7160fc4..b8770257 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -99,6 +99,7 @@ fun SettingsDestination() { @Composable private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) { var showUpdateChecker by rememberSaveable { mutableStateOf(false) } + var showMapSettings by rememberSaveable { mutableStateOf(false) } if (showUpdateChecker) { UpdateCheckerScreen( currentVersion = uiState.appVersionName, @@ -129,6 +130,17 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> } ) + if (showMapSettings) { + MapSettingsDialog( + settings = uiState.otherSettings, + onDismiss = { showMapSettings = false }, + onSave = { mapSource, tiandituKey -> + onAction(SettingsAction.UpdateMapSettings(mapSource, tiandituKey)) + showMapSettings = false + } + ) + } + // Dialogs if (dialogs.position) { PositionDialog( @@ -368,6 +380,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> onAction = onAction ) } + item { MapSettingsCard(onClick = { showMapSettings = true }) } item { CardCredits() } item(span = { GridItemSpan(maxLineSpan) }) { CardButton( @@ -558,6 +571,24 @@ private fun OtherCardPreview() = MainTheme { OtherCard(settings = values, onCompassCalibration = {}, onAction = {}) } +@Composable +private fun MapSettingsCard(onClick: () -> Unit) { + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)) { + Text( + text = stringResource(R.string.prefs_map_title), + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(6.dp)) + CardButton( + onClick = onClick, + text = stringResource(R.string.prefs_map_configure), + modifier = Modifier.fillMaxWidth() + ) + } + } +} + @Composable private fun OtherCard( settings: OtherSettings, diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt index 924badfc..4ac9b2e3 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt @@ -108,6 +108,7 @@ sealed interface SettingsAction { data class SetCompassOffset(val degrees: Float) : SettingsAction data class ToggleLightTheme(val value: Boolean) : SettingsAction data class ToggleNightMode(val value: Boolean) : SettingsAction + data class UpdateMapSettings(val mapSource: String, val tiandituKey: String) : SettingsAction // Remote control data class UpdateRC(val settings: RCSettings) : SettingsAction diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index 4bdeccb8..1f01b303 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -186,6 +186,9 @@ class SettingsViewModel( } is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) } is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) } + is SettingsAction.UpdateMapSettings -> settingsRepo.updateOtherSettings { + it.copy(mapSource = action.mapSource, tiandituKey = action.tiandituKey) + } // Remote control & data sources is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings) is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)