Compare commits

...
Author SHA1 Message Date
atsunatsu cd3eded77c chore: bump version to 4.4.6-ba7opf.9.11 2026-09-08 06:03:35 +08:00
atsunatsu 7d7f8f1eda feat(map): expanded QSO list — drop ordinal prefix, add per-QSO date
Expanded per-callsign rows previously read '2 · FO-29 · CW · V/U · 11:30Z'
(ordinal + summary, no date). Now each expanded line is
'yyyy-MM-dd · FO-29 · CW · V/U · 11:30Z' — the first QSO's date already
lives in the header row, so only subsequent QSOs carry a date prefix.
2026-09-08 05:57:27 +08:00
atsunatsu 11b46c6710 feat(map): tap worked cells from GRID_ZOOM_SUB (lines visible), labels from zoom 6.5
- Tap-to-show-QSOs now works as soon as 4-char grid lines appear (zoom 6),
  not only when 4-char labels show (was zoom 7). Selection outline already
  used GRID_ZOOM_SUB, so feedback is immediate.
- 4-char labels now appear from zoom 6.5 (was 7.0), so the sub-square name
  shows shortly after the lines appear instead of a full zoom level later.
2026-09-08 05:54:06 +08:00
atsunatsu da1facac64 fix(map): world-repeat copies no longer repaint in place — alpha stacking made worked cells brighter and opaque
Pixel analysis of the user's screenshot: worked-cell green measured RGB(57,160,74)
against background RGB(16,16,15), which matches alpha≈178 — i.e. the alpha=90
fill painted ~3 times in place. Root cause: at low zoom colRepeats>0 and every
turn loop shifted the cell by dLon=turn*360°, but projectionToX() normalizes
longitude back into [-180,180), so all turns collapsed onto the SAME screen x
and each cell/line/label was painted 2*colRepeats+1 times at one spot.

Fix: keep the turn offset as a pixel shift (turn * worldWidthPx) on the x
coordinate in ALL turn loops (worked fills at both zooms, vertical lines,
horizontal lines, labels). World-repeat copies now land on their true screen
positions instead of stacking on top of each other.
2026-09-08 05:46:08 +08:00
atsunatsu cb1f341ed9 fix(map): field-zoom grid lines no longer cut through worked cells — root cause of 'green too bright'
The previous fixes only handled sub-square zoom lines. At field zoom the 20°x10°
grid lines were drawn unconditionally: cellLabel(zoom<6) returns 2-char fields
which never match the 4-char workedGrids set, so every field line crossed every
worked 2°x1° square. Bright-yellow (alpha 160) lines through the green fill is
what read as 'green got brighter' — the fill color itself never changed.

Now both line passes iterate the finer 2°x1° sub-grid (forced 4-char labels via
GRID_ZOOM_SUB) and skip segments bordering worked cells, at field zoom and
sub-square zoom alike. Revert the alpha softening (c2e496c4) — the fill was
never the problem; also restores worked-cell labels (7afce7ee revert).
2026-09-08 05:28:12 +08:00
atsunatsu c2e496c406 fix(map): soften worked-cell fill at sub-square zoom, restore cell labels
Reverting the label-skip from 7afce7ee: worked cells keep their 4-char
labels. The 'green too bright' report is an area effect, not an overlay
overdraw — the same alpha=90 fill that reads as a small patch at field zoom
covers half the viewport at sub-square zoom. Use a softer fill (alpha 50)
for the sub-square branch, field zoom unchanged.
2026-09-08 05:02:05 +08:00
3 changed files with 103 additions and 47 deletions

No files matched your search

@@ -142,8 +142,13 @@ class MaidenheadGridOverlay : Overlay() {
if (yTop == null || yBottom == null) continue
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
// World-repeat copies: keep the turn offset in pixels
// (colRepeats is 0 at this zoom today, but the same
// alpha-stacking fix applies if it ever becomes > 0).
val xLeftBase = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRightBase = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
val xLeft = xLeftBase + turn * worldWidthPx.toFloat()
val xRight = xRightBase + turn * worldWidthPx.toFloat()
if (xRight < 0f || xLeft > canvas.width) continue
if (cellLabel(lat, lon, zoom) in workedGrids) {
canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint)
@@ -153,14 +158,22 @@ class MaidenheadGridOverlay : Overlay() {
} else {
for (grid in workedGrids) {
val cell = gridCellBounds(grid) ?: continue
// World-repeat copies: a 360° turn shifts the cell by a
// full world width in pixels. projectionToX() normalizes
// longitude back into [-180,180), so WITHOUT adding the
// turn*worldWidthPx offset every copy lands on the SAME
// screen x and the cell gets painted 2*colRepeats+1 times
// in place — alpha stacks and the green turns brighter and
// more opaque. Keep the world-width offset on the x.
for (turn in -colRepeats..colRepeats) {
// Shift the cell by whole world turns to cover repeats.
val dLon = turn * 360.0
if (cell.lonRight + dLon <= leftLon || cell.lonLeft + dLon >= rightLon) continue
val yTop = projectionToY(projection, cell.latTop) ?: continue
val yBottom = projectionToY(projection, cell.latBottom) ?: continue
val xLeft = projectionToX(projection, cell.lonLeft + dLon, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, cell.lonRight + dLon, centerLon, worldWidthPx) ?: continue
val xLeftBase = projectionToX(projection, cell.lonLeft, centerLon, worldWidthPx) ?: continue
val xRightBase = projectionToX(projection, cell.lonRight, centerLon, worldWidthPx) ?: continue
val xLeft = xLeftBase + turn * worldWidthPx.toFloat()
val xRight = xRightBase + turn * worldWidthPx.toFloat()
if (xRight < 0f || xLeft > canvas.width) continue
if (yBottom < 0f || yTop > canvas.height) continue
canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint)
@@ -169,15 +182,19 @@ class MaidenheadGridOverlay : Overlay() {
}
}
// Vertical lines (meridians), segmented per cell row so that worked
// grid cells stay line-free (same look as field-zoom fills: green
// patches without square grid lines). Only relevant at sub-square
// zoom; at field zoom cellLabel() yields 2-char fields which never
// match the 4-char workedGrids, so all lines draw.
// Vertical lines (meridians), segmented per sub-square row so that
// worked grid cells stay line-free at BOTH zoom levels. At sub-square
// zoom cellLat == SUB_SQUARE_LAT so each segment is one cell edge; at
// field zoom a single 10° cell row spans ten 1° sub-rows, and the
// worked cells are 2°x1° squares, so we must iterate the finer
// sub-square grid to decide which field-line segments to skip.
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon
val x = projectionToX(projection, lon, centerLon, worldWidthPx)
if (x == null) continue
// World-repeat copies: keep the turn offset in pixels, otherwise
// every turn normalizes to the same x and the line paints over
// itself 2*colRepeats+1 times (alpha stacking).
val xBase = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val x = xBase + turn * worldWidthPx.toFloat()
for (row in firstRow..lastRow) {
val lat = row * cellLat
if (lat < -90.0 || lat >= 90.0) continue
@@ -185,18 +202,33 @@ class MaidenheadGridOverlay : Overlay() {
if (topLatCell > 90.0) continue
val yTop = projectionToY(projection, topLatCell) ?: continue
val yBottom = projectionToY(projection, lat) ?: continue
// This meridian is a border of the cell on its right (col)
// and of the cell on its left (col-1); skip the segment if
// either cell is a worked grid.
val workedHere =
cellLabel(lat, lon, zoom) in workedGrids ||
cellLabel(lat, lon - cellLon, zoom) in workedGrids
if (workedHere) continue
canvas.drawLine(x, yTop, x, yBottom, linePaint)
if (cellLon == SUB_SQUARE_LON && cellLat == SUB_SQUARE_LAT) {
// Sub-square zoom: one segment == one cell edge.
val workedHere =
cellLabel(lat, lon, zoom) in workedGrids ||
cellLabel(lat, lon - cellLon, zoom) in workedGrids
if (workedHere) continue
canvas.drawLine(x, yTop, x, yBottom, linePaint)
} else {
// Field zoom: split the 10° row into 1° sub-rows and skip
// the segments bordering a worked 2°x1° square.
for (sub in 0 until (cellLat / SUB_SQUARE_LAT).toInt()) {
val subLat = lat + sub * SUB_SQUARE_LAT
val ySubTop = projectionToY(projection, subLat + SUB_SQUARE_LAT) ?: continue
val ySubBottom = projectionToY(projection, subLat) ?: continue
// Force 4-char labels: at field zoom cellLabel(zoom)
// returns 2-char fields which never match workedGrids.
val workedHere =
cellLabel(subLat, lon, GRID_ZOOM_SUB) in workedGrids ||
cellLabel(subLat, lon - SUB_SQUARE_LON, GRID_ZOOM_SUB) in workedGrids
if (workedHere) continue
canvas.drawLine(x, ySubTop, x, ySubBottom, linePaint)
}
}
}
}
// Horizontal lines (parallels), segmented per cell column so worked
// cells have no lines through them either.
// Horizontal lines (parallels), segmented per sub-square column so
// worked cells have no lines through them at either zoom level.
for (row in firstRow..lastRow) {
val lat = row * cellLat
if (lat <= -90.0 || lat >= 90.0) continue
@@ -204,16 +236,38 @@ class MaidenheadGridOverlay : Overlay() {
if (y == null) continue
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
// World-repeat copies: keep the turn offset in pixels (same
// alpha-stacking fix as the vertical lines above).
val xLeftBase = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRightBase = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
val xLeft = xLeftBase + turn * worldWidthPx.toFloat()
val xRight = xRightBase + turn * worldWidthPx.toFloat()
if (xRight < 0f || xLeft > canvas.width) continue
// This parallel borders the cell above (row) and below
// (row-1); skip the segment if either is worked.
val workedHere =
cellLabel(lat, lon, zoom) in workedGrids ||
cellLabel(lat - cellLat, lon, zoom) in workedGrids
if (workedHere) continue
canvas.drawLine(xLeft, y, xRight, y, linePaint)
if (cellLon == SUB_SQUARE_LON && cellLat == SUB_SQUARE_LAT) {
// Sub-square zoom: one segment == one cell edge.
val workedHere =
cellLabel(lat, lon, zoom) in workedGrids ||
cellLabel(lat - cellLat, lon, zoom) in workedGrids
if (workedHere) continue
canvas.drawLine(xLeft, y, xRight, y, linePaint)
} else {
// Field zoom: split the 20° field column into 2° sub-columns
// and skip the segments bordering a worked 2°x1° square.
for (sub in 0 until (cellLon / SUB_SQUARE_LON).toInt()) {
val subLon = lon + sub * SUB_SQUARE_LON
val xSubLeftBase = projectionToX(projection, subLon, centerLon, worldWidthPx) ?: continue
val xSubRightBase = projectionToX(projection, subLon + SUB_SQUARE_LON, centerLon, worldWidthPx) ?: continue
val xSubLeft = xSubLeftBase + turn * worldWidthPx.toFloat()
val xSubRight = xSubRightBase + turn * worldWidthPx.toFloat()
if (xSubRight < 0f || xSubLeft > canvas.width) continue
// Force 4-char labels (see vertical lines above).
val workedHere =
cellLabel(lat, subLon, GRID_ZOOM_SUB) in workedGrids ||
cellLabel(lat - SUB_SQUARE_LAT, subLon, GRID_ZOOM_SUB) in workedGrids
if (workedHere) continue
canvas.drawLine(xSubLeft, y, xSubRight, y, linePaint)
}
}
}
}
@@ -269,15 +323,14 @@ class MaidenheadGridOverlay : Overlay() {
if (yBottom < 0f || yTop > canvas.height) continue
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
// World-repeat copies: keep the turn offset in pixels (same
// alpha-stacking fix as the lines above).
val xLeftBase = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRightBase = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
val xLeft = xLeftBase + turn * worldWidthPx.toFloat()
val xRight = xRightBase + turn * worldWidthPx.toFloat()
if (xRight < 0f || xLeft > canvas.width) continue
val label = cellLabel(lat, lon, zoom)
// Worked cells stay label-free: a bright-yellow 4-char label
// centered on a green fill reads as "the green got brighter"
// and breaks consistency with field zoom, where 2-char labels
// sit at field centers and almost never overlap a worked cell.
if (label in workedGrids) continue
canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint)
}
}
@@ -362,8 +415,8 @@ class MaidenheadGridOverlay : Overlay() {
// on-screen size of each square.
const val GRID_ZOOM_SUB = 6.0
// Zoom at which the 4-char sub-square names appear (one level above the
// grid lines: zoom 6 = lines only, zoom 7+ = lines + names).
const val LABEL_ZOOM_SUB = 7.0
// grid lines: zoom 6 = lines only, zoom 6.5+ = lines + names).
const val LABEL_ZOOM_SUB = 6.5
const val MIN_LABEL_CELL_PX = 48f
const val MAX_OVERSHOOT_PX = 64
}
@@ -153,7 +153,9 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean {
if (!isGridMode || p == null) return false
val zoom = mapView.zoomLevelDouble
if (zoom < MaidenheadGridOverlay.LABEL_ZOOM_SUB) return false
// Allow tapping worked cells as soon as the 4-char grid LINES
// appear (GRID_ZOOM_SUB), not only when labels show (LABEL_ZOOM_SUB).
if (zoom < MaidenheadGridOverlay.GRID_ZOOM_SUB) return false
val grid = gridOfPoint(p.latitude, p.longitude) ?: return false
if (grid !in workedGrids) return false
selectedGrid = grid
@@ -345,12 +347,13 @@ private fun WorkedGridCallRow(
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 3.dp)
)
// Subsequent QSOs in this grid, revealed on expand.
// Subsequent QSOs in this grid, revealed on expand. No ordinal prefix;
// each line carries its own date (the first QSO's date already sits in
// the header row).
if (expanded && callQsos.size > 1) {
callQsos.drop(1).forEachIndexed { index, qso ->
val ordinal = "${index + 2}"
callQsos.drop(1).forEach { qso ->
Text(
text = "$ordinal " + qsoSummary(qso, isUtc),
text = formatDate(qso.epochMs, isUtc) + " · " + qsoSummary(qso, isUtc),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 3.dp)
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
#noinspection UnusedVersionCatalogEntry
appVersionCode = "461"
appVersionCode = "462"
#noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.6-ba7opf.9.10"
appVersionName = "4.4.6-ba7opf.9.11"
#noinspection UnusedVersionCatalogEntry
compileSdk = "37"
#noinspection UnusedVersionCatalogEntry