Repository navigation
feat(web): export the repeaters heard in view as a 1200×1200 PNG - #725
Conversation
8c87b29 to
7a7fb80
Compare
7a7fb80 to
1da3278
Compare
1da3278 to
6b1de2d
Compare
|
Amended (6b1de2d) after a self-review:
Web: 783 tests, eslint, |
|
Review of this PR's own commit (6b1de2d): two points to change, small ones below. Found by reading the code, not run. 1. A relay-hash star can be printed with a bare resolver name (change requested) 2. 3. Small ones
|
…tion A Discover reply carries a prefix of the responder's key. The node layer paired it with the registry node whose key it alone starts (efiten#661); the reach stars did not, and looked the prefix up as a whole key, which it never is. So the repeater drew its ▲ and, beside it, a star hung from its RSSI estimate with the prefix as its name. The pairing is now one function, registryMatcher in nodelayer.js, which groupSenderPointsForNodes and coverageStars both use; AGENTS.md §7 names both. On the data of 8 Aug to 7 Sep around Nijmegen, 63 of 71 stars now hang from their ▲, where 5 of 66 did. Closes efiten#723 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6b1de2d to
81324ca
Compare
A hunter could not pass on what a drive heard: the reach layer draws every repeater's star, but the only thing to share was a URL that needs an account and the same filters. An Export button in the top bar opens a sheet with the first export, Repeaters heard. Below 900px the button is in the menu: between 641 and 900px its label wrapped the bar into another row. It draws the stars the reach layer would draw for the view, from the same registry slice, window and attribution, on the light theme: every repeater in its own hue with each ray's strength as a gradient within it, a dot per hearing, the ▲ or ● it hangs from, the names that fit, the mapped cells and the route in grey, and a band with the window, the hunters and three numbers. A second MapLibre instance renders offscreen, so the live map is untouched. hexgrid.js is copied whole from the app and pinned. Decided with Kasper on mockups from real data; recorded in docs/2026-09-27-export-repeaters-heard.md. Closes efiten#666 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81324ca to
94c0bd9
Compare
|
Pushed 94c0bd9 for the review, rebuilt on #724 (82588a0). Per point:
Web: 789 tests, eslint, Playwright 301 passed. #726 and #729 are rebuilt on this one next. |
What the map shows at a zoom is now a share the style eases, where it was a step in JS. App and website draw their cells by one rule, the server's. ## What was there Five rules changed the map in one frame when a zoom threshold was crossed: the hex resolution bands, the outlines of earlier rides, the cell names, and on the website the server's resolution per zoom level. Hex and points together was a manual view, `both`. The app binned in ten bands of a fixed size, the server in one size per zoom level. ## What this PR does The numbers are Kasper's. He picked them on 29 September 2026 in a mockup with 4559 receptions between Nijmegen and Arnhem, by pinching through it on a phone, in 2D and in 3D. They are recorded on #634 and in `docs/2026-09-29-zoom-transitions.md`. | | 2D | 3D | |---|---|---| | Points of this ride | fade in from zoom 13.25 to 18.25 | one step at zoom 19 | | Hex | stays at 100% | 95% of the bar height from zoom 19 | | Earlier rides (app) | fade in from zoom 11.25 to 18.75 | with the points, at zoom 19 | | Cell names (app) | fade in from zoom 15.5 to 16.5 | not drawn in 3D, as before | | Two cell sizes | blend over 0.95 zoom level | blend over 0.75 zoom level | - **One hex system.** `app/src/hexgrid.js` takes the server's sizes: one per zoom level, about 28 px tall. The ten bands in metres are gone. - **Two cell sizes at a time.** A size hands over to its neighbour around every change, and the two shares add up to 1. Two hex layers hold them, the even resolutions in one and the odd in the other. The pair moves up at a whole zoom, where the size that stays is at 100% and the other two are at 0. - **The website asks for two sizes**, one heatmap request each. Where the server answers both with the same size, that size is drawn once and stays in full. - **Auto replaces Both**, in the app's view cycle and in the website's segmented control. A stored view or a link that says `both` reads as auto. - **In 3D the pillars wait for zoom 19.** Up to there the hex is drawn as bars, also in auto. #266 drew it flat under the pillars. - **Nothing is built for a share of 0.** In auto the points are not built below their zoom, and the website does not fetch them. - **A cell names one node.** A cell is 28 px tall and about 24 wide at every zoom, which holds four characters. The label is the prefix of the node heard last, with the count of the others under it. It was three prefixes on one line. `zoomfade.js` holds the numbers and builds the expressions. It is one file on both surfaces, pinned in `web/parity.test.js`. ## Measured in MapLibre 4.7.1 - `['zoom']` is only accepted as the input of a top-level `interpolate` or `step`. The per-feature value (`['get', 'op']`, `['get', 'height']`) rides in each stop's output. - A `fill-extrusion` of height 0 still draws an opaque polygon on the ground. With several cell sizes stacked, the ones with nothing to show covered the one that had. A layer with nothing to show is switched off by a `step` on `fill-extrusion-opacity`. - In 3D a share is spent on the bar's height. Alpha dissolves toward black, as `docs/2026-09-04-new-versus-old-on-the-map.md` records. ## Verification - app vitest 1661 of 1661, web vitest 810 of 810, eslint clean in both, the app builds. - `zoomfade.test.js`: the shares at their ends and midpoints, the pair adding up to 1 at every 0.05 zoom for both widths, no third size anywhere, and stops that rise in every expression for every resolution and view. - `web/e2e/zoomfade.spec.js`: five cases. Two fail when the rule is taken out: asking for one size only, and fetching the points at every zoom. - The app's map, fed the 4559 receptions through a throwaway page: at zoom 13.3 two sizes are up (646 and 1042 cells), no points are built below 13.25, in 3D the pillars are built from zoom 19 (1882 after the collapse). No map errors. - Full e2e locally: 300 of 302 at a load average between 26 and 48. The two that failed, `targetpicker.spec.js:198` and `ui.spec.js:502`, depend on timing. The second passed 4 of 4 when run alone. - Two existing e2e cases changed with the rules: the click on a point inside a cell runs at a zoom where auto shows points, and the 3D case asserts bars and no pillars below zoom 19. Not verified: the motion itself. The browser pane was in the background, so the checks are still frames and measurements. ## Not in this PR - **The server is unchanged.** It stops at resolution 18. The app goes to 21, so a cell stays 28 px up to zoom 20 for the walk-in. On the website a cell grows past 28 px above zoom 17. Kasper chose this on 29 September 2026. - **The noise layer (#410) draws one size**, the one in full at the zoom, without a blend. - **The exports (#725, #726, #729) add `web/hexgrid.js`.** This PR does not touch that file. Whichever merges second has to check the cell sizes agree. Closes #634 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: efiten <erwin.fiten@gmail.com>
Resolves the overlap with efiten#732 (narrow line 640 -> 767) and efiten#734 (hexgrid sizes, zoomfade parity): - barnarrow.js keeps efiten#725's per-control breakpoint (Export leaves below 900px) with NARROW_PX = 767; NARROW_MEDIA is built from it. - barnarrow.test.js: the stylesheet-width test accepts NARROW_MEDIA's width or a width a control leaves earlier at (Export's 900). - parity.test.js lists both zoomfade.js and hexgrid.js; web/hexgrid.js is recopied from app/src/hexgrid.js after efiten#734. - style.css and export.spec.js comments say 768 instead of 640.
🤖 I have created a release *beep* *boop* --- <details><summary>app: 1.30.0</summary> ## [1.30.0](app-v1.29.1...app-v1.30.0) (2026-09-30) ### Features * **app,web:** ease what the map shows by zoom, on one hex system ([#734](#734)) ([4c2a57b](4c2a57b)) * **app,web:** name a relay on the map the way the app does ([#733](#733)) ([a9aa01a](a9aa01a)) * **app:** send one broker to several collectors under one switch ([#722](#722)) ([8c6da30](8c6da30)) * **web:** export the repeaters heard in view as a 1200×1200 PNG ([#725](#725)) ([d6186bc](d6186bc)) ### Bug Fixes * **app,web:** hang a Discover-heard repeater's star from its own position ([#724](#724)) ([15a6ada](15a6ada)) * **app,web:** put the caret in the target picker's search when it opens ([#721](#721)) ([6bd899c](6bd899c)) * **app:** drop the float window's previous and next buttons ([#717](#717)) ([c957ff3](c957ff3)) * **app:** give the sound the playback buffer instead of the smallest ([#712](#712)) ([dee5484](dee5484)) * **app:** hide Chrome's pause on the fullscreen readout ([#715](#715)) ([836d91e](836d91e)) * **app:** keep a space under the HUD on Android ([#710](#710)) ([2754716](2754716)) * **app:** let the fullscreen readout turn with the phone ([#718](#718)) ([fae3749](fae3749)) * **app:** put the transmit pop under the receptions it sat over ([#738](#738)) ([2bfc5fa](2bfc5fa)) * **server:** keep the node registry in memory on a loop of its own ([#736](#736)) ([4f3e988](4f3e988)) * **web:** keep the map bar on one row between 641 and 767 px ([#732](#732)) ([c44dc1d](c44dc1d)) </details> <details><summary>server: 1.10.1</summary> ## [1.10.1](server-v1.10.0...server-v1.10.1) (2026-09-30) ### Bug Fixes * **server:** keep the node registry in memory on a loop of its own ([#736](#736)) ([4f3e988](4f3e988)) * **server:** read a refused frame again when the message-id decoder changes ([#737](#737)) ([8586c71](8586c71)) </details> <details><summary>web: 1.26.0</summary> ## [1.26.0](web-v1.25.1...web-v1.26.0) (2026-09-30) ### Features * **app,web:** ease what the map shows by zoom, on one hex system ([#734](#734)) ([4c2a57b](4c2a57b)) * **app,web:** name a relay on the map the way the app does ([#733](#733)) ([a9aa01a](a9aa01a)) * **app:** send one broker to several collectors under one switch ([#722](#722)) ([8c6da30](8c6da30)) * **web:** export the repeaters heard in view as a 1200×1200 PNG ([#725](#725)) ([d6186bc](d6186bc)) ### Bug Fixes * **app,web:** hang a Discover-heard repeater's star from its own position ([#724](#724)) ([15a6ada](15a6ada)) * **app,web:** put the caret in the target picker's search when it opens ([#721](#721)) ([6bd899c](6bd899c)) * **app:** drop the float window's previous and next buttons ([#717](#717)) ([c957ff3](c957ff3)) * **app:** give the sound the playback buffer instead of the smallest ([#712](#712)) ([dee5484](dee5484)) * **app:** hide Chrome's pause on the fullscreen readout ([#715](#715)) ([836d91e](836d91e)) * **app:** keep a space under the HUD on Android ([#710](#710)) ([2754716](2754716)) * **app:** let the fullscreen readout turn with the phone ([#718](#718)) ([fae3749](fae3749)) * **app:** put the transmit pop under the receptions it sat over ([#738](#738)) ([2bfc5fa](2bfc5fa)) * **server:** keep the node registry in memory on a loop of its own ([#736](#736)) ([4f3e988](4f3e988)) * **web:** keep the map bar on one row between 641 and 767 px ([#732](#732)) ([c44dc1d](c44dc1d)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Closes #666
Builds on #724: the first commit is that PR (a Discover-heard repeater's star under its own ▲). Review the second; it rebases to one commit once #724 is in.
What
An Export button in #bar, next to Start mapping, opens a sheet with the first export of #665: Repeaters heard, the map as it stands (filters, window, hunters) as a 1200×1200 PNG, saved as
mesh-hunter-repeaters-heard-YYYY-MM-DD.png. Below 640px the button moves into the menu with Start mapping and Log in (barnarrow.js, #561).The picture shows the stars the reach layer would draw for the view, from the same registry slice, window receptions and attribution, whether the layer is on or not:
assignHues), each ray's strength as a gradient within it: strong full and thick, weak lighter and thinner, with the map's width and opacity (rayStyle);Decided with Kasper on mockups rendered from a real
/api/pointsexport; what was tried and not chosen (signal-tier colours, #720's hex fill over every repeater) is indocs/2026-09-27-export-repeaters-heard.md.How
exportheard.js: the rules (which stars, the numbers, the route, the cells, label placement, the texts), unit-tested.exportrender.js: a second MapLibre instance offscreen at 1200×1000 withpreserveDrawingBuffer(the live map is untouched), then a canvas for the glyphs, names, scale bar and band. The bare background stands in when the hosted style does not load.exportsheet.js: the sheet, on the.lc-modalshell with the settings sheet's focus rules.hexgrid.jsis copied whole from the app and added to the parity list, which web: one repeater's reach, heard and not heard, cannot be passed on as a picture #720 needs too.Verification
exportheard.test.js(18): what is drawn, the 30 km rule, the numbers, the route breaks, the cells, label placement, tint, window text, hunters, file name, scale bar. Five rules mutated, each caught.e2e/export.spec.js: a member downloads a PNG of 1200×1200 with the right name; a guest sees the reason and a disabled button; Escape closes and hands focus back. Red with the sheet unwired.🤖 Generated with Claude Code