Compare commits

...
Author SHA1 Message Date
atsunatsu d7d4a752ed chore: bump version to 4.4.7-ba7opf.2 (versionCode 472) 2026-09-15 01:49:33 +08:00
atsunatsu 6a3ab54226 fix(map): '1 QSO' singular never matched on zh systems
Android plurals have no 'one' category in Chinese (zh plural rules only
contain 'other'), so a single QSO rendered as '1 QSOs' on Chinese-locale
devices. Replace the plurals resource with explicit singular/plural
strings selected in code.
2026-09-15 01:34:04 +08:00
atsunatsu 3b3579696b fix(map): worked grids follow the selected operated grid; add All option
Switching the operated-grid selector now re-filters the green worked
cells on the map (the overlay receives the per-grid worked set instead
of the global one, on every recomposition via setGridMode). Previously
only the VUCC progress count changed while the map stayed green for all
operated grids.

The selector gains an 'All (n)' entry at the bottom of the list: it
selects no operated grid (null), showing every worked grid combined and
restoring the global VUCC count.
2026-09-15 00:25:12 +08:00
atsunatsu 826f44829b fix(map): polar field-row grid lines vanish at field zoom
Maidenhead row R (80-90N) and row A (-90..-80S) vertical lines were drawn
with projectionToY(±90). Web Mercator diverges there: osmdroid's
TileSystemWebMercator yields y01=±Infinity -> Long.MIN/MAX_VALUE pixel
coordinates (verified via JVM test: lat=90 -> -9.2e18, lat=-90 -> +9.2e18).
The canvas cannot rasterize such coordinates, so the polar rows' grid
lines silently disappeared at field zoom (2-char labels).

Fix: clamp the projected latitude to the Mercator limit ±85.0511 in
projectionToY(). The polar rows then project onto the screen edge and
their lines render (the sliver beyond 85.05 is off-screen anyway). Row A
kept its symmetric handling; row R now uses the same path.
2026-09-14 21:24:35 +08:00
5 changed files with 58 additions and 23 deletions

No files matched your search

@@ -122,10 +122,8 @@
<string name="map_visibility">能见度:%s</string> <string name="map_visibility">能见度:%s</string>
<!-- Worked-grid QSO dialog --> <!-- Worked-grid QSO dialog -->
<string name="grid_qso_calls_count">%d 个呼号 · %d 次通联</string> <string name="grid_qso_calls_count">%d 个呼号 · %d 次通联</string>
<plurals name="grid_qso_count"> <string name="grid_qso_count_one">%d QSO</string>
<item quantity="one">%d QSO</item> <string name="grid_qso_count_many">%d QSOs</string>
<item quantity="other">%d QSOs</item>
</plurals>
<string name="grid_qso_first">首通</string> <string name="grid_qso_first">首通</string>
<string name="grid_qso_no_sat">卫星未知</string> <string name="grid_qso_no_sat">卫星未知</string>
<string name="map_visible">日照中</string> <string name="map_visible">日照中</string>
@@ -147,10 +147,8 @@
<string name="map_visibility">Visibility: %s</string> <string name="map_visibility">Visibility: %s</string>
<!-- Worked-grid QSO dialog --> <!-- Worked-grid QSO dialog -->
<string name="grid_qso_calls_count">%d callsigns · %d QSOs</string> <string name="grid_qso_calls_count">%d callsigns · %d QSOs</string>
<plurals name="grid_qso_count"> <string name="grid_qso_count_one">%d QSO</string>
<item quantity="one">%d QSO</item> <string name="grid_qso_count_many">%d QSOs</string>
<item quantity="other">%d QSOs</item>
</plurals>
<string name="grid_qso_first">First</string> <string name="grid_qso_first">First</string>
<string name="grid_qso_no_sat">satellite unknown</string> <string name="grid_qso_no_sat">satellite unknown</string>
<string name="map_visible">Visible</string> <string name="map_visible">Visible</string>
@@ -428,7 +428,15 @@ class MaidenheadGridOverlay : Overlay() {
/** Y pixel for a latitude, or null when outside the viewport. */ /** Y pixel for a latitude, or null when outside the viewport. */
private fun projectionToY(projection: Projection, lat: Double): Float? { private fun projectionToY(projection: Projection, lat: Double): Float? {
val geo = org.osmdroid.util.GeoPoint(lat, 0.0) // Web Mercator is undefined beyond ±85.0511° (osmdroid clamps the world
// to TileSystemWebMercator limits). Projecting the polar field rows
// (lat=±90: Maidenhead row R = 80..90°N, row A = -90..-80°S) yields
// ±9.2e18 pixel coordinates that the canvas cannot rasterize, so the
// polar grid lines silently vanish. Clamp to the Mercator limit: the
// polar rows then project onto the screen edge and their lines are
// drawn (the off-limit sliver beyond 85.05° is invisible anyway).
val clamped = lat.coerceIn(-MAX_MERCATOR_LAT, MAX_MERCATOR_LAT)
val geo = org.osmdroid.util.GeoPoint(clamped, 0.0)
val p = projection.toPixels(geo, null) val p = projection.toPixels(geo, null)
return p.y.toFloat() return p.y.toFloat()
} }
@@ -484,6 +492,9 @@ class MaidenheadGridOverlay : Overlay() {
const val FIELD_LON = 20.0 const val FIELD_LON = 20.0
const val SUB_SQUARE_LAT = 1.0 const val SUB_SQUARE_LAT = 1.0
const val SUB_SQUARE_LON = 2.0 const val SUB_SQUARE_LON = 2.0
// Web Mercator latitude limit (osmdroid TileSystemWebMercator): projecting
// beyond it produces ±9.2e18 pixel coordinates the canvas cannot draw.
const val MAX_MERCATOR_LAT = 85.05112877980658
// Zoom at which the 2°x1° sub-square grid lines/fills appear. // Zoom at which the 2°x1° sub-square grid lines/fills appear.
// 5.0 → 6.0: at zoom 5 a full field spans too little screen width and // 5.0 → 6.0: at zoom 5 a full field spans too little screen width and
// the sub-square grid is too dense to read; 6 roughly doubles the // the sub-square grid is too dense to read; 6 roughly doubles the
@@ -62,7 +62,6 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color as ComposeColor import androidx.compose.ui.graphics.Color as ComposeColor
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -207,8 +206,13 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
if (p.type == AwardType.VUCC) p.copy(workedKeys = vuccGrids, count = vuccGrids.size) else p if (p.type == AwardType.VUCC) p.copy(workedKeys = vuccGrids, count = vuccGrids.size) else p
} }
} }
// Attach the tap listener whenever grid mode / worked grids change. // Worked grids drawn on the map: filtered by the selected operated grid
val workedGrids = uiState.workedGrids // (null = all operated grids). Drives both the green fills and the tap
// listener below, so switching the selector changes which cells are green.
val workedGrids = remember(uiState.workedGrids, vuccByMyGrid, selectedMyGrid) {
if (selectedMyGrid == null) uiState.workedGrids
else vuccByMyGrid[selectedMyGrid] ?: emptySet()
}
val isGridMode = uiState.isGridMode val isGridMode = uiState.isGridMode
DisposableEffect(isGridMode, workedGrids) { DisposableEffect(isGridMode, workedGrids) {
val receiver = object : org.osmdroid.events.MapEventsReceiver { val receiver = object : org.osmdroid.events.MapEventsReceiver {
@@ -297,7 +301,7 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
// stationPosition would swallow the centering forever. // stationPosition would swallow the centering forever.
val shouldCenter = uiState.isGridMode && !prevGridMode val shouldCenter = uiState.isGridMode && !prevGridMode
setGridMode( setGridMode(
uiState.isGridMode, uiState.workedGrids, uiState.roamedGrids, view, uiState.isGridMode, workedGrids, uiState.roamedGrids, view,
uiState.stationPosition, uiState.stationPosition,
centerOnStation = shouldCenter centerOnStation = shouldCenter
) )
@@ -322,11 +326,12 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
} }
} }
// Top-left: operated-grid selector for VUCC counting (grid mode only). // Top-left: operated-grid selector for VUCC counting (grid mode only).
val myGrid = selectedMyGrid // Shown whenever per-grid QSO data exists; "All" (null) is available.
if (uiState.isGridMode && selectedAward == AwardType.VUCC && myGrid != null) { if (uiState.isGridMode && selectedAward == AwardType.VUCC && vuccByMyGrid.isNotEmpty()) {
VuccGridSelector( VuccGridSelector(
options = vuccByMyGrid, options = vuccByMyGrid,
selected = myGrid, allCount = uiState.workedGrids.size,
selected = selectedMyGrid,
onSelect = { selectedMyGrid = it }, onSelect = { selectedMyGrid = it },
modifier = Modifier modifier = Modifier
.align(Alignment.TopStart) .align(Alignment.TopStart)
@@ -440,7 +445,10 @@ private fun WorkedGridCallRow(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Text( Text(
text = pluralStringResource(R.plurals.grid_qso_count, callQsos.size, callQsos.size), // Plurals resources don't work for zh (only 'other' matches), so
// pick the singular/plural string explicitly.
text = if (callQsos.size == 1) stringResource(R.string.grid_qso_count_one, 1)
else stringResource(R.string.grid_qso_count_many, callQsos.size),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = androidx.compose.ui.text.style.TextAlign.Center, textAlign = androidx.compose.ui.text.style.TextAlign.Center,
@@ -638,12 +646,13 @@ private fun AwardChipsRow(
@Composable @Composable
private fun VuccGridSelector( private fun VuccGridSelector(
options: Map<String, Set<String>>, options: Map<String, Set<String>>,
selected: String, allCount: Int,
onSelect: (String) -> Unit, selected: String?,
onSelect: (String?) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
// Most-worked grid first; selection is made on tap. // Most-worked grid first; selection is made on tap. "All" sits last.
val sorted = remember(options) { options.entries.sortedByDescending { it.value.size } } val sorted = remember(options) { options.entries.sortedByDescending { it.value.size } }
Surface( Surface(
color = ComposeColor.Black.copy(alpha = 0.45f), color = ComposeColor.Black.copy(alpha = 0.45f),
@@ -658,7 +667,8 @@ private fun VuccGridSelector(
.padding(start = 10.dp, end = 6.dp, top = 4.dp, bottom = 4.dp) .padding(start = 10.dp, end = 6.dp, top = 4.dp, bottom = 4.dp)
) { ) {
Text( Text(
text = "$selected (${options[selected]?.size ?: 0})", text = if (selected == null) "All ($allCount)"
else "$selected (${options[selected]?.size ?: 0})",
color = ComposeColor.White, color = ComposeColor.White,
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
@@ -691,6 +701,24 @@ private fun VuccGridSelector(
) )
} }
} }
// "All" always listed last: shows every worked grid combined.
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
onSelect(null)
expanded = false
}
.padding(horizontal = 10.dp, vertical = 3.dp)
) {
Text(
text = "All ($allCount)",
color = if (selected == null) MaterialTheme.colorScheme.primary else ComposeColor.White,
fontSize = 12.sp,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
} }
} }
} }
+2 -2
View File
@@ -1,8 +1,8 @@
[versions] [versions]
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionCode = "471" appVersionCode = "472"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.7-ba7opf.1" appVersionName = "4.4.7-ba7opf.2"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
compileSdk = "37" compileSdk = "37"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry