mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f7a087938 | ||
|
|
4b82332133 |
No files matched your search
@@ -84,6 +84,7 @@ class SettingsRepo(
|
||||
private val keyStateOfLightTheme = "stateOfLightTheme"
|
||||
private val keyStateOfNightMode = "stateOfNightMode"
|
||||
private val keyStateOfMapGrid = "stateOfMapGrid"
|
||||
private val keyStateOfMapFirstCall = "stateOfMapFirstCall"
|
||||
private val keyStationAltitude = "stationAltitude"
|
||||
private val keyStationLatitude = "stationLatitude"
|
||||
private val keyStationLongitude = "stationLongitude"
|
||||
@@ -546,6 +547,7 @@ class SettingsRepo(
|
||||
putBoolean(keyStateOfLightTheme, new.stateOfLightTheme)
|
||||
putBoolean(keyStateOfNightMode, new.stateOfNightMode)
|
||||
putBoolean(keyStateOfMapGrid, new.stateOfMapGrid)
|
||||
putBoolean(keyStateOfMapFirstCall, new.stateOfMapFirstCall)
|
||||
putBoolean(keyShouldSeeWarning, new.shouldSeeWarning)
|
||||
putBoolean(keyShouldSeeWhatsNew, new.shouldSeeWhatsNew)
|
||||
putString(keySstvMode, new.sstvMode)
|
||||
@@ -565,6 +567,7 @@ class SettingsRepo(
|
||||
stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false),
|
||||
stateOfNightMode = preferences.getBoolean(keyStateOfNightMode, false),
|
||||
stateOfMapGrid = preferences.getBoolean(keyStateOfMapGrid, false),
|
||||
stateOfMapFirstCall = preferences.getBoolean(keyStateOfMapFirstCall, false),
|
||||
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
|
||||
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true),
|
||||
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
|
||||
|
||||
@@ -60,6 +60,10 @@ data class OtherSettings(
|
||||
val stateOfLightTheme: Boolean,
|
||||
val stateOfNightMode: Boolean = false,
|
||||
val stateOfMapGrid: Boolean = false,
|
||||
/** Grid mode: label worked (green) cells with the FIRST callsign worked
|
||||
* in that grid instead of the Maidenhead code; non-worked cells get no
|
||||
* label at all. */
|
||||
val stateOfMapFirstCall: Boolean = false,
|
||||
val shouldSeeWarning: Boolean,
|
||||
val shouldSeeWhatsNew: Boolean,
|
||||
val sstvMode: String = "Auto",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
<string name="map_next">下一个</string>
|
||||
<string name="map_grid_mode">网格模式</string>
|
||||
<string name="map_satellite_mode">卫星模式</string>
|
||||
<string name="map_first_call_mode">首通呼号</string>
|
||||
<string name="map_azimuth">方位角:%.1f°</string>
|
||||
<string name="map_elevation">仰角:%.1f°</string>
|
||||
<string name="map_altitude">高度:%.0f km</string>
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
<string name="map_copyright" translatable="false">© OpenStreetMap contributors</string>
|
||||
<string name="map_grid_mode">Grid mode</string>
|
||||
<string name="map_satellite_mode">Satellite mode</string>
|
||||
<string name="map_first_call_mode">First call</string>
|
||||
<string name="map_azimuth">Azimuth: %.1f°</string>
|
||||
<string name="map_elevation">Elevation: %.1f°</string>
|
||||
<string name="map_altitude">Altitude: %.0f km</string>
|
||||
|
||||
+34
-3
@@ -52,6 +52,13 @@ class MaidenheadGridOverlay : Overlay() {
|
||||
color = Color.argb(220, 255, 224, 130)
|
||||
setShadowLayer(3f, 2f, 2f, Color.BLACK)
|
||||
}
|
||||
// 首通呼号标签: 比 4 字符网格代码略小, 以容纳更长的呼号(如 BG2GQG).
|
||||
private val firstCallPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
textSize = 20f
|
||||
style = android.graphics.Paint.Style.FILL
|
||||
color = Color.argb(230, 255, 224, 130)
|
||||
setShadowLayer(3f, 2f, 2f, Color.BLACK)
|
||||
}
|
||||
private val workedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = android.graphics.Paint.Style.FILL
|
||||
color = Color.argb(90, 76, 217, 100)
|
||||
@@ -79,6 +86,15 @@ class MaidenheadGridOverlay : Overlay() {
|
||||
/** Worked gridsquares (4-char, uppercase) to highlight, e.g. {"OL62", "PM95"}. */
|
||||
var workedGrids: Set<String> = emptySet()
|
||||
|
||||
/** Grid-mode first-call labels: label worked (green) cells with the first
|
||||
* callsign worked in that grid instead of the Maidenhead code; non-worked
|
||||
* cells get no label at all. Only meaningful at sub-square zoom. */
|
||||
var showFirstCallLabels: Boolean = false
|
||||
|
||||
/** Worked grid -> first callsign worked in it (earliest QSO by time).
|
||||
* Only read when [showFirstCallLabels] is on. */
|
||||
var firstCallsByGrid: Map<String, String> = emptyMap()
|
||||
|
||||
/**
|
||||
* Gridsquares the station operated from (4-char, uppercase) — drawn with
|
||||
* blue 45° stripes (GridMaster-style zebra), e.g. {"OL62", "PM95"}.
|
||||
@@ -348,6 +364,10 @@ class MaidenheadGridOverlay : Overlay() {
|
||||
// Field (2-char) labels show at every zoom, subject only to the pixel-
|
||||
// size check below; sub-square (4-char) labels only from LABEL_ZOOM_SUB
|
||||
// (one level above the grid lines, so zoom 6 shows lines but no names).
|
||||
// First-call labels replace the 4-char grid codes and only exist at
|
||||
// sub-square zoom; at field zoom NO labels are drawn at all (the user
|
||||
// requirement is that non-worked cells carry no grid characters).
|
||||
if (showFirstCallLabels && zoom < LABEL_ZOOM_SUB) return
|
||||
val showLabels = zoom >= LABEL_ZOOM_SUB || cellLat == FIELD_LAT
|
||||
if (!showLabels) return
|
||||
// Estimate on-screen cell height to avoid clutter at low zoom:
|
||||
@@ -358,8 +378,9 @@ class MaidenheadGridOverlay : Overlay() {
|
||||
val pixelsPerDegree = Math.abs(y2 - y1)
|
||||
if (pixelsPerDegree * cellLat < MIN_LABEL_CELL_PX) return
|
||||
|
||||
labelPaint.textAlign = Paint.Align.CENTER
|
||||
val fontMetrics = labelPaint.fontMetrics
|
||||
val activePaint = if (showFirstCallLabels) firstCallPaint else labelPaint
|
||||
activePaint.textAlign = Paint.Align.CENTER
|
||||
val fontMetrics = activePaint.fontMetrics
|
||||
val textHalfHeight = (fontMetrics.descent + fontMetrics.ascent) / 2f
|
||||
for (row in firstRow..lastRow) {
|
||||
val lat = row * cellLat
|
||||
@@ -386,7 +407,17 @@ class MaidenheadGridOverlay : Overlay() {
|
||||
val xRight = xRightBase + turn * worldWidthPx.toFloat()
|
||||
if (xRight < 0f || xLeft > canvas.width) continue
|
||||
val label = cellLabel(lat, lon, zoom)
|
||||
canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint)
|
||||
if (showFirstCallLabels) {
|
||||
// 首通呼号模式: 只有绿格(worked)标注该格第一个通联的呼号,
|
||||
// 非绿格空着不写网格字符.
|
||||
if (label in workedGrids) {
|
||||
firstCallsByGrid[label]?.let { call ->
|
||||
canvas.drawText(call, (xLeft + xRight) / 2f, yCenter, firstCallPaint)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,13 @@ private fun MapScreen(
|
||||
}
|
||||
m
|
||||
}
|
||||
// First callsign worked in each grid (earliest QSO by time), used by the
|
||||
// "首通呼号" label mode — derived from the same QSO store as the worked fills.
|
||||
val firstCallsByGrid: Map<String, String> = remember(uiState.workedGridQsos) {
|
||||
uiState.workedGridQsos.mapNotNull { (grid, qsos) ->
|
||||
qsos.minByOrNull { it.epochMs }?.let { grid to it.call }
|
||||
}.toMap()
|
||||
}
|
||||
// Selected operated grid for VUCC counting; defaults to the grid with the
|
||||
// most worked grids. Null when the store carries no per-QSO myGrid data
|
||||
// (requires a LoTW resync) — then VUCC falls back to the global count.
|
||||
@@ -337,7 +344,10 @@ private fun MapScreen(
|
||||
setGridMode(
|
||||
uiState.isGridMode, workedGrids, uiState.roamedGrids, view,
|
||||
uiState.stationPosition,
|
||||
centerOnStation = shouldCenter
|
||||
centerOnStation = shouldCenter,
|
||||
// 首通呼号标签只作用于 VUCC 视图(用户要求); "All"/其他奖状不过滤.
|
||||
showFirstCallLabels = uiState.showFirstCallLabels && selectedAward == AwardType.VUCC,
|
||||
firstCallsByGrid = firstCallsByGrid
|
||||
)
|
||||
if (!shouldCenter || uiState.stationPosition != null) {
|
||||
prevGridMode = uiState.isGridMode
|
||||
@@ -372,13 +382,27 @@ private fun MapScreen(
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
GridModeToggle(
|
||||
isGridMode = uiState.isGridMode,
|
||||
onToggle = { onAction(MapAction.ToggleGridMode(it)) },
|
||||
// Top-right: grid-mode pill + (grid mode only) the first-call
|
||||
// label pill, stacked vertically.
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
)
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
GridModeToggle(
|
||||
isGridMode = uiState.isGridMode,
|
||||
onToggle = { onAction(MapAction.ToggleGridMode(it)) }
|
||||
)
|
||||
// 首通呼号开关仅 VUCC 视图显示(与 VuccGridSelector 同条件).
|
||||
if (uiState.isGridMode && selectedAward == AwardType.VUCC) {
|
||||
FirstCallToggle(
|
||||
checked = uiState.showFirstCallLabels,
|
||||
onToggle = { onAction(MapAction.ToggleFirstCallLabels(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -800,6 +824,40 @@ private fun GridModeToggle(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格模式下的"首通呼号"开关: 打开后绿格(worked)的标签显示该格第一个通联的
|
||||
* 呼号, 非绿格不显示任何文字. 仅在网格模式下显示(与网格模式开关叠放).
|
||||
*/
|
||||
@Composable
|
||||
private fun FirstCallToggle(
|
||||
checked: Boolean,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
color = ComposeColor.Black.copy(alpha = 0.45f),
|
||||
shape = RoundedCornerShape(percent = 50),
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 12.dp, end = 6.dp, top = 2.dp, bottom = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.map_first_call_mode),
|
||||
color = ComposeColor.White,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(end = 2.dp)
|
||||
)
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onToggle,
|
||||
modifier = Modifier.scale(0.75f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Map overlay helpers
|
||||
@@ -843,7 +901,9 @@ private fun setGridMode(
|
||||
roamedGrids: Set<String>,
|
||||
mapView: MapView,
|
||||
stationPosition: GeoPos?,
|
||||
centerOnStation: Boolean = false
|
||||
centerOnStation: Boolean = false,
|
||||
showFirstCallLabels: Boolean = false,
|
||||
firstCallsByGrid: Map<String, String> = emptyMap()
|
||||
) {
|
||||
try {
|
||||
val gridOverlay = mapView.overlays[OVERLAY_GRID]
|
||||
@@ -851,6 +911,9 @@ private fun setGridMode(
|
||||
gridOverlay.isEnabled = gridMode
|
||||
gridOverlay.workedGrids = workedGrids
|
||||
gridOverlay.roamedGrids = roamedGrids
|
||||
// 首通呼号模式: 绿格标注第一个通联的呼号, 非绿格不显示标签.
|
||||
gridOverlay.showFirstCallLabels = showFirstCallLabels
|
||||
gridOverlay.firstCallsByGrid = firstCallsByGrid
|
||||
// ownGrid must be set on EVERY update — the position is available
|
||||
// regardless of whether this frame centers (centering happens only
|
||||
// on entry, but passing null here would wipe the bold outline).
|
||||
@@ -860,6 +923,8 @@ private fun setGridMode(
|
||||
isEnabled = gridMode
|
||||
this.workedGrids = workedGrids
|
||||
this.roamedGrids = roamedGrids
|
||||
this.showFirstCallLabels = showFirstCallLabels
|
||||
this.firstCallsByGrid = firstCallsByGrid
|
||||
this.ownGrid = ownGridOf(stationPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ data class MapState(
|
||||
val isLightUi: Boolean = false,
|
||||
val isUtc: Boolean = false,
|
||||
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. */
|
||||
val showFirstCallLabels: Boolean = false,
|
||||
val workedGrids: Set<String> = emptySet(),
|
||||
val workedGridQsos: Map<String, List<com.rtbishop.look4sat.core.domain.model.GridQso>> = emptyMap(),
|
||||
/** 4-char gridsquares the station operated from (LoTW MY_GRIDSQUARE) — blue stripes. */
|
||||
@@ -48,6 +51,7 @@ sealed interface MapAction {
|
||||
data class SelectItem(val item: OrbitalObject) : MapAction
|
||||
data class SelectDefaultItem(val catnum: Int) : MapAction
|
||||
data class ToggleGridMode(val value: Boolean) : MapAction
|
||||
data class ToggleFirstCallLabels(val value: Boolean) : MapAction
|
||||
data class SetVisible(val isVisible: Boolean) : MapAction
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,13 @@ class MapViewModel(
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
settingsRepo.otherSettings.collectLatest { settings ->
|
||||
_uiState.update { it.copy(isUtc = settings.stateOfUtc, isGridMode = settings.stateOfMapGrid) }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUtc = settings.stateOfUtc,
|
||||
isGridMode = settings.stateOfMapGrid,
|
||||
showFirstCallLabels = settings.stateOfMapFirstCall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
@@ -105,6 +111,7 @@ class MapViewModel(
|
||||
is MapAction.SelectItem -> selectSatellite(action.item)
|
||||
is MapAction.SelectDefaultItem -> selectDefaultSatellite(action.catnum)
|
||||
is MapAction.ToggleGridMode -> settingsRepo.updateOtherSettings { it.copy(stateOfMapGrid = action.value) }
|
||||
is MapAction.ToggleFirstCallLabels -> settingsRepo.updateOtherSettings { it.copy(stateOfMapFirstCall = action.value) }
|
||||
is MapAction.SetVisible -> isScreenVisible.value = action.isVisible
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "481"
|
||||
appVersionCode = "482"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.7-ba7opf.11"
|
||||
appVersionName = "4.4.7-ba7opf.11.1"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
|
||||
Reference in new issue
Block a user