Weather: JMA area forecast on tapping a place - #9
Merged
Conversation
Tap the rider dot or a dropped pin for a menu (weather, or clear the pin)
leading to JMA's area forecast for that spot: today onward in JMA's own
wording, highs and lows, 6-hourly rain chances, wind and waves, the prose
outlook, and any warning headline. The radar says what the rain is doing
in the next hour; this says whether to set off at all.
JMA keys forecasts by area code, never coordinates, and the codes aren't
derivable from the JIS prefecture code - Hokkaido splits seven ways,
Kagoshima's mainland office is 460100, Okinawa splits four. So
tool/prep_forecast_areas.dart walks JMA's own area.json (class20 ->
class15 -> class10 -> office) against the municipality table already in
the repo and bakes a lookup; resolution at runtime is
nearest-municipality, which gets the subdivision (千葉県北東部, not just
"Chiba") - a rider in south Chiba shouldn't read Chiba city's forecast.
Two things the response shape forces:
- Series are identified by the fields their areas carry, not by
position. JMA has reordered timeSeries before, and inland offices omit
waves entirely.
- Temperatures are paired by clock time, not index. A midday issue
echoes the day's high into the already-past 00:00 low slot, so reading
them positionally reports "high 35, low 35"; the 00:00 slot is the
low, 09:00 the high, and a low equal to the high is dropped rather
than shown as fact.
In English mode the report translates on device like closures do, with
one deliberate exception: place names use JMA's own English ("North-
eastern Region", "Sapporo City"), baked into the generated table.
Machine translation is at its worst exactly where JMA is already
authoritative. The Japanese shows the moment it arrives and the English
swaps in behind it, so a first-run ~30 MB model download never blocks
the forecast, and a failed translation degrades to Japanese rather than
to an error - the same contract the closure list has had.
ClosureTranslator._translate becomes public translateText; the
translator instance is shared through the controllers because it owns
the downloaded model and the string cache, and a second one would mean a
second download.
Forecast fetches get their own AssetKind so they group in the debug
sheet instead of falling into "Other".
The sheet listed each day vertically; the linked JMA page puts the same data in two tables, and the tabular form is easier to read a decision out of. So: - Near-term table: a column per day carrying the icon and temperatures, with rain chance below it as a grid of fixed 00-06/06-12/12-18/18-24 blocks. Fixed rather than derived - JMA drops a block once it has passed, so derived columns would shuffle between morning and afternoon. A dropped block reads as "–", never "0": "no longer forecast" and "0% chance" are different claims. - Week ahead: the 週間予報 block, which was previously parsed and discarded. Different shape to the short-term one - one area for the whole office, temperatures at a single observation point, no wording - and it carries JMA's own A/B/C confidence grade, which is the difference between planning on a forecast and hoping. JMA's wording is deliberately *not* a table cell. 「くもり時々晴れ夜のは じめ頃一時雨所により夕方から雷を伴い激しく降る」in a column two fingers wide is a vertical smear; as a sentence under the table it stays the most informative thing on the sheet. Both tables scroll inside their own box, so the sheet never scrolls sideways. Icons come from JMA's 3-digit 天気コード, bucketed by leading digit (1 晴れ, 2 くもり, 3 雨, 4 雪). JMA publishes ~130 codes but no machine-readable table - there is no telop.json, only the page's JavaScript - so the rule was checked against live data from ten offices spread across the country: 87 of 87 periods agreed with JMA's own wording. Four buckets on purpose, with the wording carrying the rest; an absent code draws a dash rather than a guess. Only days the detailed block put *numbers* against are held back from the weekly table. Its last day is routinely wording alone - no temperatures, no rain chance - and the weekly block does have those, so treating "is mentioned above" as "is covered above" threw them away. Caught against live data, not in review. Five blocks that look alike at a glance - two tables of numbers, two runs of Japanese prose, and the attribution - now each sit under a quiet heading with a rule above it, built by one _Section wrapper so they stay consistent by construction. Sections JMA didn't send drop out entirely rather than leaving a rule and a title over nothing. Test infrastructure: revealInSheet now calls ensureVisible after scrolling. Being in the tree is not being on screen - a ListView builds past the viewport - so a tap aimed below the fold landed on nothing. That was a latent flake in the existing tests, not something the tables introduced.
TokyoDanInJapan
added a commit
that referenced
this pull request
Jul 25, 2026
`gh pr edit <n> --body ...` leaves the PR untouched. It asks for projectCards in the same GraphQL query it uses to edit, so GitHub's Projects-classic deprecation error aborts the mutation - and because the error talks about Projects rather than the edit, it reads as a warning about something you didn't ask for. Cost a silent no-op on #9 that was only caught by reading the PR back afterwards. CLAUDE.md didn't exist yet; this starts it with the workaround (gh api -X PATCH, which doesn't touch Projects) and, more importantly, the habit of verifying the write landed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tap the rider dot or a dropped pin → a menu (Weather here / Clear pin) →
JMA's area forecast for that spot, laid out the way the JMA page lays it out.
The radar says what the rain is doing in the next hour; this says whether to
set off at all.
Keyless, like every other source in the app.
Resolving a position to a forecast
JMA keys forecasts by area code, never by coordinates, and the codes aren't
derivable from the JIS prefecture code — Hokkaido splits seven ways, Kagoshima's
mainland office is
460100, Okinawa splits four.tool/prep_forecast_areas.dartwalks JMA's own
area.json(class20 → class15 → class10 → office) against themunicipality table already in the repo and bakes a 1716-row lookup. Runtime
resolution is nearest-municipality, which gets the subdivision (千葉県北東部,
not just "Chiba") — a rider in south Chiba shouldn't read Chiba city's forecast.
Verified against live JMA across four regions: Chiba → 北東部 via 長柄町 (4 km),
渋峠 → 長野北部, Sapporo → 石狩地方/札幌管区気象台, Naha → 本島中南部/沖縄気象台.
Things the data forces
JMA has reordered
timeSeriesbefore; inland offices omit waves entirely.day's high into the already-past 00:00 low slot, so reading positionally
reports "high 35, low 35". A low equal to the high is dropped, not shown.
would shuffle between morning and afternoon. A dropped block reads
–, never0— "no longer forecast" and "0% chance" are different claims.block's last day is routinely wording alone, and the weekly block does carry
its temps and rain chance; treating "mentioned above" as "covered above" threw
them away. Caught against live data, not in review.
Icons
From JMA's 3-digit 天気コード, bucketed by leading digit (1 晴れ, 2 くもり,
3 雨, 4 雪). JMA publishes ~130 codes but no machine-readable table — there is
no
telop.json, only the page's JavaScript — so the rule was checked againstlive data from ten offices spread across the country: 87 of 87 periods
agreed with JMA's own wording. Four buckets on purpose, with JMA's wording
carrying what the icon drops. An absent code draws a dash rather than a guess.
Translation
English mode translates on device like closures do, except place names use
JMA's own English ("North-eastern Region", "Sapporo City"), baked into the
generated table. Machine translation is at its worst exactly where JMA is
already authoritative. A test asserts the translator is never asked for 北東部
or 長柄町, so a regression that quietly routed place names through ML Kit fails
rather than just getting worse.
The Japanese shows the moment it arrives and the English swaps in behind it —
the first translation ever can be a ~30 MB model download, and someone deciding
whether to set off shouldn't watch a spinner while the forecast is already in
hand. A failed translation degrades to Japanese, the same contract the closure
list has had.
ClosureTranslator._translatebecomes publictranslateText; thetranslator instance is shared because it owns the model and the string cache.
Also
AssetKind, so they group in the debug sheet.marks a position and a bigger dot would claim precision the fix doesn't have,
but a 22 px target on a moving bike is not one.
Verification
flutter analyzeclean, 145/145 tests (43 new), release APK builds.Three bugs the tests caught before they shipped:
FutureBuildersubscribed, which Flutterreports as an unhandled async error — a red screen over the map in debug.
setStateas the first statement of anasyncmethod called frominitStateruns synchronously, i.e. during the build mounting that widget. Harmless
today only by accident.
revealInSheetneededensureVisible: being in the tree isn't being onscreen, so a tap aimed below the fold landed on nothing. A latent flake.