- Match prefill now positions the LazyColumn via its initial state
(queryMutualPasses(initialScrollIndex = 1)) instead of a post-layout
scrollToItem, so the page reliably opens with the time-range card at the
top while the 24h query runs.
- The install-time what's new dialog text now scrolls inside a bounded box
(max 360dp) instead of overflowing the fixed dialog frame.
- Grid-QSO dialog gains a Match action (radio-tower icon + label) in its
header; tapping it navigates to the match page pre-filled with the tapped
grid as the opposite station and a 24h time range, auto-starts the query,
and scrolls the page straight to the time-range card.
- Every grid is now tappable (previously only worked grids opened the
dialog); unworked grids show an empty QSO list instead of a hint.
The 11.2 gate only relaxed the pre-return zoom check, but the showLabels
gate below it still required LABEL_ZOOM_SUB (6.5), so first-call labels
stayed hidden in the 6.0..6.5 band where the 4-char grid lines render.
- Gate showLabels on GRID_ZOOM_SUB (6.0) when showFirstCallLabels is on.
- Relax MIN_LABEL_CELL_PX (48) to 0.6x for first-call mode so 1° cells
(~45 px on the equator at zoom 6.0) are not filtered out.
Normal grid-code labels are unchanged (still LABEL_ZOOM_SUB).
- pass_whatsnew_message rewritten as a single line with escaped \n bullets:
real newlines in the value are invalid for aapt2 ('not a valid string').
- Escape the apostrophe in "map's"; mark grid_qso_calls_count
formatted=false (two %d + '·' triggered multiple-substitutions).
- values-si: remove stray comma after map_altitude </string>.
All were latent resource errors surfaced by the forced recompile.
- Pass history (Iu-yang1 fork): PassesSettings.hoursBefore look-back window;
second 'Time before' slider in the pass filter; completed passes stay
listed (progress clamped to [0,1]) in the history window.
- Compass calibration (Iu-yang1 fork): OtherSettings.compassOffsetDegrees,
CompassAccuracy from the magnetic sensor, calibration dialog with
figure-eight accuracy meter, corrected heading and manual offset field;
RadarViewModel applies the offset.
- What's New dialog now shows the cumulative 4.4.6->4.4.7 changelog and
follows the system language (values/ + values-zh/).
- pass_filter_history_hours localized in es/ru/si/tr/uk.
Move the first-call label gate from LABEL_ZOOM_SUB (6.5) to GRID_ZOOM_SUB
(6.0): worked (green) cells are labelled with their first callsign as soon
as the 4-char grid lines render, not only at the name-label zoom.
Add a 'first call' pill toggle below the grid-mode switch (map top-right,
visible only in grid mode + VUCC award). When on, worked (green) cells are
labelled with the first callsign worked in that grid (earliest confirmed
QSO by time) instead of the Maidenhead code; non-worked cells get no label
at all. Persisted via OtherSettings.stateOfMapFirstCall (full MVI chain).
The feature is VUCC-only: All and other award overlays never draw it.
Day-cell numbers now count the most recent consecutive reports sharing the
newest status (counting back from the newest report, stopping at the first
different status), instead of the total report count of the day. Empty gaps
do not break the streak; statuses compare by exact report text; counting
does not cross day boundaries.
- SatDay gains streakCount (computed in AmSatRepository.buildStatuses)
- DayCell renders day.streakCount instead of slot count sum
- Add real org.json to core:data unit tests (android.jar stub cannot parse)
- Add 2 streak tests: mixed-status slot breaks, all-same-status counts all
The satellite search only matched exact continuous substrings, so names with
dashes, spaces or brackets (AO-7, ISS (ZARYA), FO-29) could not be found by
queries without those separators. Now the query is normalized (lowercased,
non-alphanumeric separators stripped) and split into space-separated tokens,
and every token must appear in the normalized satellite name. This makes
'ao7' match 'AO-7 (AMSAT-OSCAR 7)' and 'iss zarya' match 'ISS (ZARYA)'.
Numeric queries still match catnum exactly. Covered by 8 new unit tests.
The mutual pass list LazyColumn reset to the top on every return to the page.
Scroll position is now stored in the Activity-scoped MutualViewModel and
restored on re-entry; a new query (queryGeneration bump) rebuilds the list at
the top so stale positions never leak into fresh results.
The osmdroid map was recreated on every return to the Map page, resetting
center/zoom to the default and forcing grid mode back onto the station grid.
Now the viewport (center + zoom) is saved in the Activity-scoped
MapFilterViewModel only in grid mode and restored on re-entry, so the user
keeps exactly where they left off. Satellite mode keeps its original
follow-the-satellite behavior; cold-start entries and in-page grid toggles
still center on the station grid as before.
The award chip (VUCC/DXCC/WAJA/...) now lives in an Activity-scoped
MapFilterViewModel instead of a per-composition remember, so switching
to another page and back keeps the last selection; it resets to VUCC
only once per process (cold start). Removes the effect that forced
VUCC on every grid-mode entry.
The stored sync cursor was 'yyyyMMdd', which LoTW's qso_qslsince silently
ignores (measured live: it falls back to the system-supplied default).
Write a full UTC 'yyyy-MM-dd HH:mm:ss' cursor instead — the documented
format LoTW honors — so incremental pulls are exact (second precision)
and the settings page can show the last successful sync time on the LoTW
card, formatted like the ephemeris update time (prefs_updated_title).
Legacy yyyyMMdd cursors are normalized for the API, the gate and the
display (lotwCursorApi/lotwCursorDate/lotwCursorEpochMs).
Mirrors the ephemeris auto-update check: on every app start, if LoTW
credentials are configured, the new 'auto-sync LoTW grids' toggle is on,
at least one manual sync was done before, and today has not synced yet
(ARRL rate limit, ~once a day), pull confirmations since the last sync
and merge them. Same callsign -> incremental merge; callsign change ->
full replace. Manual dialog and automatic path share the orchestration
(mode resolution, gate, merge/persist) moved to core:domain
LoTWGridSync.kt; LoTWSyncMode moved to core:domain. 8 unit tests.
- Cancel: the dialog's cancel/back while syncing now aborts the download job
(cooperative cancellation in the stream reader, CancellationException
propagated, nothing written to storage). An accidental full sync can be
stopped immediately.
- Progress: the size estimate is conservative (900 B/record vs measured 720)
so the bar never hits 100% before the download truly ends; storage writes
moved to the IO dispatcher (multi-MB per-grid QSO JSON used to freeze the
main thread after download); when the bar caps, the UI switches to
'Processing downloaded data…' instead of looking stuck.
- Removed the outdated '3-10 seconds' hint (misleading for large accounts).
Stripe spacing now scales as 44px * 2^(zoom-7) instead of a fixed 44px on
screen: at the map's max zoom (7.0) the stripes keep the established look,
and at lower zooms they shrink with the map, so stripes stay pinned to the
map (same stripes-per-cell at every level) — denser on screen when zoomed
out, sparser when zoomed in.
- SharedDialog title gets start padding when only a cancel button is shown
(was flush against it); dialogs with both buttons unchanged.
- LoTW dialog: full-sync button bottom-left, incremental-merge bottom-right
(SpaceBetween instead of side-by-side).
- Pass card header: radar icon button enlarged to 48dp touch target with a
24dp icon, spanning both header rows vertically (name/time + elevations);
the pass info column narrows so the Opposite elevation shifts left.
- RadarDestination no longer auto-returns instantly when the mutual pass has
already ended — the auto-return now only runs while the pass is actually in
progress. Previously the collapsed-card radar shortcut appeared broken
('flash back to Mutual') whenever the computed pass had finished.
- Fakes: add the 4 last-sync bookkeeping members missed by the LoTW commit.
Icon-only radar button at the right end of the collapsed pass header (always
visible, not only when expanded); the pass info shifts left. The button's tap
is consumed and does not toggle the card expansion.
Two explicit sync buttons in the LoTW dialog:
- Full sync: qso_qslsince=2000-01-01, fresh report replaces all stored data.
- Incremental merge: since=last successful sync date, merged into the stored
set (grids union, QSOs deduped by call + time). First sync / callsign change
automatically falls back to full replace so no stale grids from another
account linger.
Progress & estimates:
- Report body is now streamed; the header's <APP_LoTW_NUMREC> gives the real
QSL count, total size is estimated as NUMREC * 720 B (measured), and the UI
shows a determinate bar with 'N QSOs, ~X s remaining' once known.
- Pre-sync hint shows the measured rate (~8 s per 100 QSOs).
- readTimeout raised to 600 s (stall timeout, not a total cap — huge accounts
legitimately take 30+ min at ARRL's ~10 KB/s stream rate); connectTimeout
raised to 60 s (a busy server took 36.5 s to accept a connection).
Also: ISettingsRepo gains last-sync date/callsign bookkeeping, and the mutual
test fake implements the new members.
DayCell displayed the count of the newest non-gray slot only; it now sums the
counts of all 12 slots of the day. The tile color still reflects the newest
reported status.
Grid mode is toggled from the map page pill (MapAction.ToggleGridMode writes
stateOfMapGrid); the settings-page switch was a duplicate entry point. Removes
the SwitchRow, the ToggleMapGrid action and its ViewModel branch, and trims
the OtherCard height by one row.
The expanded option rows used fillMaxWidth inside a full-screen Box, which
stretched the selector's dark surface across the whole map. The Column now
sizes to its widest child (IntrinsicSize.Max) so the box is just wide enough
for the label, count and expand arrow.
The pass card time range and the elevation-curve X-axis labels were hardcoded
to the device timezone. MutualUiState now carries isUtc, initialized and kept
in sync with OtherSettings.stateOfUtc (same pattern as PassesViewModel), and
both the card timeFormat and the chart axis use UTC when the setting is on.
Also fixes pre-existing test-fake compile breakage: FakeSettingsRepo built
OtherSettings with positional args missing shouldSeeWhatsNew, and never
implemented the wavelogSettings/lotwSettings/worked-grids members added by
later interface extensions.
Root cause: LocalElevationThresholds was provided inside MainScreen, but
RadarDestination is a root-level sibling entry in NavRoot's NavDisplay, so
it fell outside the CompositionLocal scope and always used the default
15/45 thresholds.
Fix: hoist the CompositionLocalProvider to NavRoot so both the tab content
and the root-level RadarDestination read the user's low/high elevation
thresholds. Also affects upstream (same navigation structure).
WAZ/DXCC award rings are stored with the closing edge implicit (the renderer
close()s single-segment rings). Rings that straddle ±180° are split into
multiple segments, and multi-segment rings must not be close()d (each close()
would draw a spurious chord across the map), so the implicit closing edge
was never drawn -> the shared boundary between WAZ zones 31/32 (165E,-8 ->
180,-12), and the closing edges of Russia's main ring / Fiji / Russia's
Chukotka tip in DXCC, stayed open.
Fix: store every ring explicitly closed (append first point). splitRingAt
Antimeridian then cuts the closing edge at ±180 exactly like any other edge,
so each segment ends on the correct side and the boundary lines render.
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.
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.
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.
- QSO dialog: each callsign row shows 'n QSO'/'n QSOs' (plurals) centered
between callsign and date, styled like the date.
- Grid mode + VUCC filter: top-left selector picks the operated grid the
VUCC count is computed from (defaults to the most-worked grid), with a
collapsible list of all operated grids; arrow style matches the QSO
dialog rows.
GridQso gains myGrid (ADIF MY_GRIDSQUARE) captured per QSO during LoTW
parsing (handles alphabetical field order + EOR reset) and persisted as
JSON key 'mg' (absent in pre-myGrid data -> null). Legacy syncs keep the
global VUCC count; one resync unlocks per-grid counting.
Port of upstream PR #252 (wty2019wty): combine otherSettings.stateOfUtc
into initRepository so an existing AOS time window is reapplied in the
newly selected timezone right after toggling UTC, instead of waiting for
the next selection/position change.
- DB v2 migration: entries.ndot column so hasDecayed() actually works after
reload (parsed ndot was dropped on write, making the decay detection dead
code); radios.isCustom column to mark manually imported transceivers.
- updateTransceiversFromFile marks imported radios isCustom=true; remote
updates now deleteManagedRadios() then insert the fresh snapshot, keeping
manual imports (no source can refresh them, so nothing would bring them back).
- Wire MIGRATION_1_2 into MainContainer (fallbackToDestructiveMigration=false
would otherwise crash on upgrade).
- PassesViewModel: use the DEVICE timezone for the natural-day anchor in
findTodaySunRiseSet instead of the display timezone. With UTC enabled the
anchor was UTC midnight = 08:00 local for UTC+8 stations, when the sun is
already up — tripping the white-night guard and rendering '--:--'.
- SatelliteRepo.isAosInRange: follow the UTC display toggle so the pass time
window is interpreted in UTC when UTC mode is on. Previously the list
showed UTC times while the filter silently used local time, off by the
timezone offset (8h for China).
- wapc.json: add name_en for all 34 provinces (Beijing/Guangdong/...)
- waja.json: add name_en for all 47 prefectures (Gifu/Kyoto/...)
- AwardBoundaryOverlay: render English label on non-Chinese UI,
fall back to source name when name_en is absent
- bump version to 4.4.6-ba7opf.10.4 (469)
The radar page is a full-screen destination; the transceiver list had no
bottom inset padding, so on gesture navigation the translucent system bar
floated over the CW decoder's decoded-text output at the end of the list.
Add navigationBars height as LazyColumn contentPadding (no-op on
three-button nav where the window is already resized).
- Zangnan (Arunachal Pradesh): China ring south edge moved from the de-facto
line to the official claim (DataV Tibet south arc); India ring's Arunachal
protrusion removed so the region shows as China (both rings share the new
boundary). Verified: Zangnan points in China ring only, Assam stays India.
- Karakoram corridor / Shaksgam valley (China-controlled since 1963): the
coarse 13-pt west segment (Karakoram Pass -> Afghan/Tajik corner) replaced
with the official Xinjiang south arc (232 pts) — the valley is now inside
the China ring. Aksai Chin was already correct; no other entity deviates
from the official map (SCS islands follow their own DXCC entities).
Field zoom (2-char labels) now draws the station's own 2°x1° square outline
on top of the field grid, so the operator always sees their exact grid.
Polar labels + order-independent LoTW parsing fixes ride along in this release.
parseConfirmedGrids and parseRoamedGrids gated grid fields on propMode == SAT
at the moment the field line was read. LoTW emits ADIF fields alphabetically
(GRIDSQUARE/MY_GRIDSQUARE before PROP_MODE), so the gate was never satisfied
on real reports: the worked set AND the roamed set silently came back empty.
Both parsers now buffer the record and decide at <EOR>, matching the proven
parseConfirmedGridQsos pattern. Regression tests cover the alphabetical order.
- Polar bands (80..90 / -90..-80) extend past the map's latitude limit, so
their label center fell off-screen and the labels never rendered. Labels are
now centered in the VISIBLE part of the cell; fully off-screen cells are
still skipped. Verified with a projection-accurate simulation render: Arctic
band shows IR/JR, Antarctic band shows IA/JA, ±80 lines intact.
- Own-grid bold outline is drawn only at sub-square zoom (zoom >= 6): at field
zoom (2-char labels) the current grid is no longer marked (user request).
The ownGrid-set-every-frame fix stays, so the outline still survives map
refreshes at sub-square zoom.
- Bold outline now uses the same yellow as the regular grid lines
(argb(160,255,224,130)) at 6f stroke — no color change, just thicker.
- Passes top bar matches upstream exactly: left = radios dialog
(ic_radios -> ToggleRadiosDialog), right = passes filter dialog
(ic_filter -> TogglePassesDialog). The calendar button stays only on the
radar page (upstream behavior); removes the now-unreachable
PassesAction.AddToCalendar wiring.
- Version bumped to 4.4.6-ba7opf.10.1.
setGridMode only received stationPosition on the grid-mode entry frame (for
centering); every later recomposition (map refreshes once per second) passed
null, so gridOverlay.ownGrid was reset to null and the bold outline vanished.
Split the concerns: ownGrid is now derived from uiState.stationPosition on
EVERY update, while centering stays gated by centerOnStation (= entry only).
The station's own gridsquare is in the roamed set (LoTW MY_GRIDSQUARE includes
the home grid). Exclude it from the blue stripe overlay at both zoom levels so
the current cell shows only its bold cyan outline, matching the user's request.
chore: bump version to 4.4.6-ba7opf.10
Restores the original repo's calendar function (ic_calendar → AddToCalendar via
CalendarContract ACTION_INSERT) to the top-right of both pages, replacing the
map buttons:
- Radar page: back to upstream's addToCalendar (current pass name/aos/los).
- Passes page: new PassesAction.AddToCalendar wired through PassesViewModel +
container.provideAddToCalendar() (next pass name/aos/los); left button back
to the pass filter (reverts the earlier radios-button swap).
The map button is gone from both top bars — the map is now a bottom-nav tab.
Also drops the now-dead openMapRequest/navigateToMap navigation chain in
NavRoot/MainScreen that only the radar map button used.
Brings in libnativedecoderjni.so for arm64-v8a (from Iu-yang1/Look4Sat-FT4,
the Morse Expert 1.15 official Play arm64 split) and lifts the ABI pin from
armeabi-v7a-only to armeabi-v7a + arm64-v8a in both feature/cw and app. 64-bit
devices now load the native decoder natively instead of falling back to ARM32
(or crashing when the arm64 lib was missing).
- Map moves from the passes-page top-right button to the bottom navigation
bar at position 4 from the left (Satellites, Passes, AMSAT, Map, Mutual,
Settings), matching the upstream nav order.
- Passes top bar restored to the original repo layout: left = radios dialog
(ic_radios), right = passes filter/schedule dialog (ic_filter) — the map
button is gone from the top bar.
- Worked-grid QSO dialog expand arrow enlarged from bodySmall to 18sp.
- Grid mode now opens with the VUCC award chip selected by default (re-asserted
on every grid-mode entry; the user's in-mode chip choice is preserved).
- The station's own grid outline is now drawn at every zoom level: 2-char field
border below the sub-square zoom, 4-char square border above it (was hidden
at the default entry zoom), stroke widened to 6f.
- Grids the account operated from (LoTW <MY_GRIDSQUARE>, satellite QSOs only)
are covered with GridMaster-style blue 45° zebra stripes. The stripe paint is
fully opaque so a worked+roamed cell shows pure-blue stripes with green
between them — the two colors never alpha-blend into a teal/green mix.
- LoTW sync now parses and persists the roamed set (LoTWResult.Success +
SettingsRepo key 'roamedGrids'), plumbed through MapState/MapViewModel into
MaidenheadGridOverlay. Unit tests cover MY_GRIDSQUARE parsing (SAT-only,
GRIDSQUARE non-leak, 4-char truncation).
osmdroid's vertical (Y) map repetition wraps points below the viewport up to
the top edge. Antarctica's full-circle ring then draws a full-height artifact
line whenever its X range crosses the screen at low zoom. The map must never
repeat vertically, so disable setVerticalMapRepetitionEnabled.
splitRingAtAntimeridian used to drop any edge crossing ±180°, leaving
visible gaps in WAZ zone outlines (zone 31's 40°N Pacific boundary,
zone 1/19's polar arcs, zone 32's Antarctic arc). The edge is now split
in two at ±180° with interpolated latitude, each half kept in its own
segment, so outlines close across the map's left/right edges.
normalizeLon now maps to (-180, 180] so edges ENDING on the
antimeridian are not mistaken for crossings.
- projectionToY clamps latitude to Web Mercator max (±85.05) so raw -90
polar vertices (Antarctica ring, WAZ zones 29/30/32/39) no longer project
to infinity, removing vertical pole spikes
- traceSegment unwraps longitude deltas per-vertex (continuous with previous
point) instead of folding each vertex independently — a full-circle ring
(Antarctica, span ~358°) no longer jumps ±180° mid-ring to draw a line
across the whole map
- closing chord now folds to the nearest world copy (short-side seal at the
antimeridian) instead of a full-width chord
- AwardCalculator: DXCC chip now listed before VUCC
- wapc/dxcc assets: add Hong Kong & Macau labels (forceLabel) and MO boundary in dxcc
- AwardBoundaryOverlay: rings split at antimeridian no longer close() per segment — removes spurious Russia cross-continent chord
- AwardCalculator: Macau DXCC entity 330 -> 152 (+test)
- Settings LoTW dialog: add sync duration hint (zh/en)
LoTW returns STATE as 'CODE // Name' (verified with a real report 2026-09),
e.g. 'GD // Guangdong' (China), '34 // Tottori-ken' (Japan), 'CA // California'
(US). The award calculator matched on the bare CODE, so every mainland China
province (WAPC), all Japan prefectures (WAJA) and US states (WAS) failed —
WAPC only showed HK/MO/TW (entity branch, no STATE needed) and WAJA was empty.
Normalize STATE once at parse time: strip the ' // Name' suffix so the stored
GridQso.state is the clean code consumed by persistence and AwardCalculator.
Add a test covering China/Japan/US formats.
DXCC (Russia/Fiji/Antarctica) and WAZ (Pacific CQ zones 1/19/31/32) polygon
rings cross ±180. The old overlay normalized every vertex to [-180,180)
independently, folding far-side vertices onto the opposite screen edge and
drawing a spurious line sweeping across the whole map (esp. at low zoom:
DXCC horizontal/vertical lines, WAZ extra long edges).
Split each ring into contiguous segments at the antimeridian, then project
each segment anchored on its first vertex with continuous longitude offsets
(no per-vertex fold). Ring geometry stays intact while panning/zooming.
The .9.11 skip logic removed every line segment bordering a worked
gridsquare, which also erased the shared edges between adjacent worked
cells — adjacent greens merged into one unreadable patch. Revert to
drawing the full Maidenhead grid unconditionally; the green fills stay
on top exactly as before.
- Grid-mode top bar replaced with award filter chips (All/DXCC/VUCC/
WAPC/WAJA/WAZ/WAS) showing live progress (count/target)
- New AwardBoundaryOverlay renders real administrative boundaries
(provinces/prefectures/CQ zones/states/countries) with the VUCC visual
language: yellow boundary lines, green fill for worked regions, labels
- Boundary assets packaged from public GeoJSON (DataV China, Japan land,
HB9HIL CQ zones, Natural Earth states/countries) with ADIF match keys:
WAPC province codes, WAJA ADIF prefecture numbers, US state abbrs,
ARRL DXCC entity codes
- GridQso extended with dxcc/country/cqz/state parsed from LoTW report
(qso_qsldetail); AwardCalculator derives six-award progress; STATE is
interpreted per DXCC entity (ADIF 'STATE depends on DXCC')
- Grid-mode toggle pill floated at map top-right (Grid mode / Satellite mode)
- New tests: AwardCalculatorTest (9 cases), LoTW award-field parsing (3 cases)
Since v4.4.6-ba7opf.9.9 releases are uploaded as Look4Sat-<version>.apk (no
'-release' suffix), but parseRelease still built the download URL as
Look4Sat-<version>-release.apk. Update check succeeded (page parses) while the
download 404'd ('下载失败请重试'). Align the generated URL with the actual
asset name; mirror-prefixed URLs fixed too.
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.
- 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.
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.
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).
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.