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.
This commit is contained in:
atsunatsu committed 2026-09-08 05:46:08 +08:00
1 parent cb1f341ed9
commit da1facac64
1 file changed
+39 -13
@@ -142,8 +142,13 @@ class MaidenheadGridOverlay : Overlay() {
if (yTop == null || yBottom == null) continue if (yTop == null || yBottom == null) continue
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) { for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue // World-repeat copies: keep the turn offset in pixels
val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue // (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 (xRight < 0f || xLeft > canvas.width) continue
if (cellLabel(lat, lon, zoom) in workedGrids) { if (cellLabel(lat, lon, zoom) in workedGrids) {
canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint) canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint)
@@ -153,14 +158,22 @@ class MaidenheadGridOverlay : Overlay() {
} else { } else {
for (grid in workedGrids) { for (grid in workedGrids) {
val cell = gridCellBounds(grid) ?: continue 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) { for (turn in -colRepeats..colRepeats) {
// Shift the cell by whole world turns to cover repeats.
val dLon = turn * 360.0 val dLon = turn * 360.0
if (cell.lonRight + dLon <= leftLon || cell.lonLeft + dLon >= rightLon) continue if (cell.lonRight + dLon <= leftLon || cell.lonLeft + dLon >= rightLon) continue
val yTop = projectionToY(projection, cell.latTop) ?: continue val yTop = projectionToY(projection, cell.latTop) ?: continue
val yBottom = projectionToY(projection, cell.latBottom) ?: continue val yBottom = projectionToY(projection, cell.latBottom) ?: continue
val xLeft = projectionToX(projection, cell.lonLeft + dLon, centerLon, worldWidthPx) ?: continue val xLeftBase = projectionToX(projection, cell.lonLeft, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, cell.lonRight + dLon, 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 (xRight < 0f || xLeft > canvas.width) continue
if (yBottom < 0f || yTop > canvas.height) continue if (yBottom < 0f || yTop > canvas.height) continue
canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint) canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint)
@@ -177,8 +190,11 @@ class MaidenheadGridOverlay : Overlay() {
// sub-square grid to decide which field-line segments to skip. // sub-square grid to decide which field-line segments to skip.
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) { for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon val lon = col * cellLon
val x = projectionToX(projection, lon, centerLon, worldWidthPx) // World-repeat copies: keep the turn offset in pixels, otherwise
if (x == null) continue // 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) { for (row in firstRow..lastRow) {
val lat = row * cellLat val lat = row * cellLat
if (lat < -90.0 || lat >= 90.0) continue if (lat < -90.0 || lat >= 90.0) continue
@@ -220,8 +236,12 @@ class MaidenheadGridOverlay : Overlay() {
if (y == null) continue if (y == null) continue
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) { for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue // World-repeat copies: keep the turn offset in pixels (same
val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue // 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 if (xRight < 0f || xLeft > canvas.width) continue
if (cellLon == SUB_SQUARE_LON && cellLat == SUB_SQUARE_LAT) { if (cellLon == SUB_SQUARE_LON && cellLat == SUB_SQUARE_LAT) {
// Sub-square zoom: one segment == one cell edge. // Sub-square zoom: one segment == one cell edge.
@@ -235,8 +255,10 @@ class MaidenheadGridOverlay : Overlay() {
// and skip the segments bordering a worked 2°x1° square. // and skip the segments bordering a worked 2°x1° square.
for (sub in 0 until (cellLon / SUB_SQUARE_LON).toInt()) { for (sub in 0 until (cellLon / SUB_SQUARE_LON).toInt()) {
val subLon = lon + sub * SUB_SQUARE_LON val subLon = lon + sub * SUB_SQUARE_LON
val xSubLeft = projectionToX(projection, subLon, centerLon, worldWidthPx) ?: continue val xSubLeftBase = projectionToX(projection, subLon, centerLon, worldWidthPx) ?: continue
val xSubRight = projectionToX(projection, subLon + SUB_SQUARE_LON, 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 if (xSubRight < 0f || xSubLeft > canvas.width) continue
// Force 4-char labels (see vertical lines above). // Force 4-char labels (see vertical lines above).
val workedHere = val workedHere =
@@ -301,8 +323,12 @@ class MaidenheadGridOverlay : Overlay() {
if (yBottom < 0f || yTop > canvas.height) continue if (yBottom < 0f || yTop > canvas.height) continue
for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) { for (turn in -colRepeats..colRepeats) for (col in firstCol..lastCol) {
val lon = col * cellLon val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue // World-repeat copies: keep the turn offset in pixels (same
val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue // 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 if (xRight < 0f || xLeft > canvas.width) continue
val label = cellLabel(lat, lon, zoom) val label = cellLabel(lat, lon, zoom)
canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint) canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint)