23 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a7930ca0ea |
Merge branch 'bundler-2/whiteboard' into bundler-2/handoff
# Conflicts: # internal/check-whiteboard-projection.mjs # library/maps/whiteboard-map.js # library/maps/whiteboard-projection.js # library/scripts/build-language-tree.mjs |
||
|
|
bfc99ff827 |
library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver
Every reference to the Driver in library/ is reworded: the engineering reason stays, the speaker and the quote go, and a decision still open reads "provisional, pending a product decision". Comments only: each changed file is identical after stripping comments (JavaScript, template and CSS, including CSS comments inside the style strings shadow-DOM components inject). Those style-string comments do ship, so several bundles change by exactly those comment bytes. |
||
|
|
63276767cf |
The poster credited Mapbox for tiles OpenFreeMap had served
The footer's attribution is a licence statement, so it has to be true rather than merely present. posterDefaults ships "(c) Mapbox (c) OpenStreetMap" and any caller that did not override it printed exactly that -- including country-posters, whose maps run on the fleet's MapLibre default, where Mapbox serves nothing at all and OpenFreeMap, OpenMapTiles and OpenStreetMap are the three parties whose licences actually apply. research-map had a hand-written pair for the two providers, which was right for research-map and helped nobody else. The obvious approach is to read the style's sources. Checked against the live style rather than assumed, and it does not work: tiles.openfreemap.org/styles/ liberty declares two sources and NEITHER carries an attribution field. The credit lives in the TileJSON the vector source points at, which the engine fetches and merges into the SOURCE OBJECT once it loads. The style is the request and the source is what came back, and only the second one knows. map-attribution.js reads the live source first and falls back to any style-declared value. The TileJSON value is HTML with anchors and a © entity, and a poster footer is text, so it is flattened rather than printed raw. Repeated credits collapse. When nothing can be read -- no map, a style that throws before it has loaded -- it returns empty and the caller keeps its own string, because a blank licence line is the one outcome that is never acceptable. The check uses the real TileJSON string as its fixture. A tidy invented style would have passed while the real map printed the wrong credit, and its first negative test is exactly that: reading the style alone returns nothing for OpenFreeMap. Cut directly on main rather than on the branch this began life in, so it does not wait behind a poster-layout change it has nothing to do with. A wrong tile credit is a licence problem; the layout question it was sitting behind is not. |
||
|
|
84e1e0cf42 |
The poster sliced its rows in degrees, and Mercator charges more for a degree up north
DRV1650-SR-POSTER-EXPORT, the "too much Antarctica" half. The big-poster renderer
stitches a grid of tiles, each drawn into an EQUAL-HEIGHT slot of the output image, and
it chose each row's latitude band by dividing the degree span evenly:
latStep = (ne.lat - sw.lat) / rows
Screen height is proportional to ln(tan(pi/4 + lat/2)), not to latitude, so equal DEGREE
bands are not equal SCREEN bands. MEASURED on a -60..75 view in 4 rows: the top band is
37.0% of the true rendered height and was being painted into 25% of the poster; the next
19.7% into 25%; the bottom 25.2% into 25%. Every row is squeezed or stretched, which is
the wrong aspect, and the bands where a degree buys the most pixels are the most
over-represented -- the Antarctica the Driver saw.
Sliced in Mercator y instead, every row is 25.0%, exactly, because that IS the screen
coordinate.
★ THIRD PLACE TODAY FOR THE SAME MISTAKE. A whiteboard chart nearly shipped with a linear
scale into degrees; a measurement harness nearly reported a false threshold by correcting
the wrong axis; and this has been shipping. Treating latitude as a screen coordinate is
the error, the conversion is one line, and it now lives in library/maps/mercator-slice.js
rather than being rediscovered a fourth time.
check-mercator-slice asserts the PROPERTY across five view/row combinations -- every band
an equal share of rendered height, contiguous, covering the span, north-first -- and its
CONTROL is the old code: degree-linear slicing must FAIL the same assertion, which it does
by 12.0 points. Without that control the test would pass on a tree where nothing changed.
A -90..90 view clamps at the Mercator limit instead of going infinite.
NOT FIXED HERE, because it is a product decision and not a defect: the export also asks
for an 8.5x11 PORTRAIT sheet (research-map.vue:1410), so a landscape map is letterboxed
onto a tall page. That is a deliberate default, it is the other half of the wrong vertical aspect, and changing the poster's default shape is a product decision, not a fix.
|
||
|
|
3d16a1d62c |
Shipped comments describe the behaviour instead of quoting a person
Seven comments added in this batch quoted the reporter word for word. Each now states the behaviour it guards. |
||
|
|
fb769b3e86 |
The language tree becomes anyone's, and the numbers nobody wrote down get written down
Driver brief §B. gospel-internet's GiTree had NO props and NO emits: it read a board composable and three module singletons, which is what made it gospel's rather than anybody's. Everything it reached for is now a prop, and the parts worth testing are no longer inside an SFC a gate cannot run. WHAT IS ACTUALLY IN THE COMPONENT: wiring, 160 lines. The drawing is the whiteboard tiered-points layer, the surface is whiteboard-map.js (moved out of gospel's plugin -- 67 lines whose every import already came from library, so it was never theirs), the hierarchy is language-tree-model.js and the camera is frame-target.js. Four modules, four headless gates. An SFC cannot be executed by a check, so it should hold as little as possible. THREE BEHAVIOURS PINNED BECAUSE THEY ARE EASY TO STATE WRONGLY: FRAME ON SELECT, NOT ON CHANGE. Clearing does not re-frame; the camera stays where the reader left it. "It frames when the selection changes" is the obvious phrasing and it is wrong in exactly the case that annoys people -- being yanked somewhere after deselecting. A SELECTION THAT ARRIVES BEFORE THE MAP IS REPLAYED. GiTree survived this by ordering: its host set the node through a composable that waited for data, so the value always landed after the map. As a PROP the ordering belongs to the host, and a prop whose effect depends on WHEN you set it is a trap -- a shared deep link would work or not depending on load timing, which nobody could report. ready replays whatever `selected` is. HOVER CANNOT MOVE THE CAMERA, structurally rather than by convention: hover sets a filter on its own layer and there is no camera call on that path to remove later. AND THE CONSTANTS. gospel's maintainer inherited GiTree and had to read intent out of structure -- there is no "this took measuring" note anywhere in it. Asked which line they would flag, they named frame(). So padding 60 (the outermost nodes carry labels that extend past the point), maxZoom 8 (a two-language family must not fill the screen with its own neighbourhood gone), duration 900, and the floor zoom that stops framing one node from zooming the reader OUT are now stated with their reasons and asserted. Two branch rules with them, both of which a tidy-up would erase: reduced motion is a DIFFERENT CALL (jumpTo, not easeTo with duration 0 -- the engines do not treat those as one), and a single point CENTRES rather than fitting a degenerate box, because a one-language family is ordinary and fitBounds on no extent zooms to nothing. THE ROLLUP IS OVER THE READER'S SCOPE. "A family shows the worst colour below it" is only true relative to what is currently on the board: a family that is red overall can be green inside a filter. Handing the rollup a whole subtree makes the colours quietly disagree with the board -- no error, no blank, just a wrong fact about a real people group. Asserted in both directions, and colorFor stays injectable because nothing in the drawing depends on the rollup being worst-of. THE CLICK TOGGLE MOVED TO THE HOST, and that is a change rather than a codification: GiTree cleared the selection when you re-clicked the selected node, and that toggle lived in the component's own handler. A host that wants it must now write it. In exchange the chart holds no selection state. gospel's UI is not wired and their boardEngine.js is untouched on main; they adopt on their own schedule. |
||
|
|
d3ee0bb6ec |
The whiteboard is a layer of tiered points; only the edges were ever a tree
Driver brief §B, the seam under the language tree, so gospel's set charts inherit zoom, label priority and tiling from the map engine instead of re-implementing them in SVG. READ FROM GiTree RATHER THAN ASSUMED: six layer groups, exactly ONE hierarchical. The parent->child links are sourced from linkFC; the points, the selection and hover rings, the dish behind a cluster and EVERY label layer come from one point collection and know nothing about trees. gospel re-read that independently and found the same split, which is the only reason I trust it -- "the seam is generic" is the claim that should never travel on one reader. So library/charts/whiteboard/tiered-points.js takes a point FeatureCollection whose features carry id, label, tier and priority, and gives each TIER its own symbol layer with its own minzoom. That is the whole of "band names zoomed out, ring names mid, group names in": the engine is told which labels may exist at which zoom and resolves the collisions itself. Paint expressions pass through unread -- the layer has no opinion about what a colour means, which is what keeps it reusable. Selection and hover are FILTERED layers, so hover costs one setFilter and structurally cannot move a camera: there is nothing in here that could. AND THE PROJECTION IS NOT A SCALE, which is the part that would have shipped wrong. gospel asked for degrees and planned to scale by one number; their charts are CONCENTRIC RINGS, and Mercator stretches vertically by 1/cos(lat) -- 1.0154x at 10 degrees, 1.1547x at 30 -- so a linear scale renders rings as OVALS, worst at the outer ring where the labels a reader compares are. library/maps/whiteboard-projection.js is the inverse conversion, EXTRACTED from build-language-tree.mjs rather than written, because the second copy was about to be made by hand. The degree-height it emits shrinks from 0.9996 of the width at the centre to 0.6794 at the edge; that shrink IS the correction the renderer then cancels. TWO TESTS, EACH GUARDING A DIFFERENT RISK. check:whiteboard-projection reproduces the old private implementation VERBATIM as a reference and compares over 1,734 grid points EXACTLY -- the language tree is a build-time artifact, so a rounding shift would move 4,000 coordinates and look like nothing. It then asserts a ring renders round at the inner, middle and outer radii, AND that a linear scale FAILS that same assertion, because an assertion the defect can pass is measuring nothing. check:tiered-points runs gospel's CENTERED SET through the layer: 2,079 points, 3 tiers, priority by population, and NO EDGES. The absent links layer is the evidence -- if the whiteboard draws this chart with no line layer anywhere, edges are separable in fact rather than in my reading. The map is a recording fake, so the assertions are about what the module DOES; a regex over the source would pass on a module that never called addLayer. Negative-tested: making the projection linear, changing its rounding, dropping the label priority, or giving every tier the same minzoom each turn one of them red. gospel's UI is NOT wired -- their component is theirs to write, later, against a module that now exists. Their boardEngine.js stays where it is until the chart lands. |
||
|
|
b8bf3f4c47 |
The latitude ceiling belongs to extentFraction, not to a chart's extent
gospel measured it across extents 100 / 228 / 260 / 1000 and I confirmed: |lat| is 52.070 at frac 0.17 for all four, identical to three decimals. K = extentFraction / extent and the outermost point sits at u = extent, so u*K = extentFraction whatever the extent is. It is worth the header because it kills the fix a reader would try first. Someone whose drawing hits the engines' ~85 degree clamp will reach for a smaller extent, and it buys nothing at all -- extent is a unit choice; the fraction is the whole ceiling. The corollary is the useful half: one assertion on extentFraction covers every consumer, so the shared module needs no per-chart latitude guard. |
||
|
|
edcc31d36e |
The other way to get the space wrong is to correct the axis that needs no correction
gospel retracted their extentFraction threshold and named their bug: they computed screen x as lng * cos(lat). That is the plate-carree correction, and Web Mercator does not apply it -- screen x is proportional to LONGITUDE ALONE, which is precisely why the projection is conformal and why circles stay circles. They measured the damage done by a transform they had added themselves. Nothing needed retracting from this header: it already documented the opposite, that roundness is exact at every value. What it did NOT cover was this second failure, and this one is subtler than the first. A reader who has just been told "it is a projection, not a scale" naturally reaches for a cos(lat) term, because that is what correcting a projection feels like it should involve. MEASURED here rather than taken: true Mercator gives 0.000% warp at every fraction, while inserting cos(lat) into x invents 12.4% at 0.17, 23.8% at 0.25, 39.8% at 0.35, 56.5% at 0.45. Those are NOT gospel's numbers -- theirs ran 2.94/6.12/11.33/17.51, so their harness differed in a second way I did not reproduce. Both wrong spaces produce a clean monotonic table that grows with the fraction, and the magnitudes depend on which wrong transform was picked, so the SHAPE of the numbers cannot tell you which mistake you made. Only comparing against x = lng, y proportional to ln(tan(pi/4 + lat/2)) can. So the header now says: if a measurement of this module ever reports a threshold, suspect the harness before the maths. Roundness cannot degrade, because this is the exact inverse of what the renderer does. The latitude ceiling stands and gospel confirmed it independently to a tenth of a degree: |lat| 52.1 at 0.17, 77.3 at 0.35, 83.2 at 0.45, 89.7 at 0.95, against the engines' ~85 clamp. That is the real usable range, and it is the only one. |
||
|
|
378cce63d2 |
extentFraction cannot deform a shape; what it can do is run out of latitude
gospel measured the bounded set's 0.92r/1.12r band against this projection and reported the gap closing between extentFraction 0.25 and 0.35, and asked for the threshold in this header. I could not reproduce it, and the disagreement is worth the header more than the threshold would have been. MEASURED IN MERCATOR SPACE -- the space the map actually draws in -- a ring comes back at 0.000% warp at 0.17, 0.35, 0.45, 0.60, 0.80 and 0.95. That is not a tolerance being met; it is exact, because this is the exact inverse of the renderer's own transform. A consumer with a hard geometric invariant does not have to defend it against this parameter at all. MEASURED IN DEGREES, treating lng/lat as if it were the screen, the same ring reports 17.5% warp at 0.17, 35.3% at 0.25 and 62.9% at 0.35. Those numbers are the CORRECTION doing its job, read in a coordinate system nothing renders in. Screen y is proportional to ln(tan(pi/4 + lat/2)), never to lat -- which is the same confusion this module exists to prevent, met from the other side. THE REAL CEILING IS LATITUDE. The outermost point reaches |lat| 52 deg at 0.17, 77 at 0.35, 83 at 0.45 and 89.7 at 0.95, and Web Mercator is undefined at the poles with the engines clamping near 85. So the parameter's usable range is bounded by how close to the pole the drawing lands, not by any deformation of shape. That is now in the header, because it is the limit a consumer actually has to respect. AND THE CONTROL'S PLACEMENT IS NOW ARGUED RATHER THAN ASSUMED, which is gospel's other finding and it was right: a linear scale is round to three parts in a thousand at the inner ring (0.9968) and only fails further out (0.795 at the outer). An assertion placed at the inner radius would PASS the exact mistake it exists to catch. Both halves are asserted now -- the outer control must fail, and the inner one is recorded as passing so nobody mistakes it for coverage, or "simplifies" three radii to one. |
||
|
|
15fc38555c |
The language tree becomes anyone's, and the numbers nobody wrote down get written down
Driver brief §B. gospel-internet's GiTree had NO props and NO emits: it read a board composable and three module singletons, which is what made it gospel's rather than anybody's. Everything it reached for is now a prop, and the parts worth testing are no longer inside an SFC a gate cannot run. WHAT IS ACTUALLY IN THE COMPONENT: wiring, 160 lines. The drawing is the whiteboard tiered-points layer, the surface is whiteboard-map.js (moved out of gospel's plugin -- 67 lines whose every import already came from library, so it was never theirs), the hierarchy is language-tree-model.js and the camera is frame-target.js. Four modules, four headless gates. An SFC cannot be executed by a check, so it should hold as little as possible. THREE BEHAVIOURS PINNED BECAUSE THEY ARE EASY TO STATE WRONGLY: FRAME ON SELECT, NOT ON CHANGE. Clearing does not re-frame; the camera stays where the reader left it. "It frames when the selection changes" is the obvious phrasing and it is wrong in exactly the case that annoys people -- being yanked somewhere after deselecting. A SELECTION THAT ARRIVES BEFORE THE MAP IS REPLAYED. GiTree survived this by ordering: its host set the node through a composable that waited for data, so the value always landed after the map. As a PROP the ordering belongs to the host, and a prop whose effect depends on WHEN you set it is a trap -- a shared deep link would work or not depending on load timing, which nobody could report. ready replays whatever `selected` is. HOVER CANNOT MOVE THE CAMERA, structurally rather than by convention: hover sets a filter on its own layer and there is no camera call on that path to remove later. AND THE CONSTANTS. gospel's maintainer inherited GiTree and had to read intent out of structure -- there is no "this took measuring" note anywhere in it. Asked which line they would flag, they named frame(). So padding 60 (the outermost nodes carry labels that extend past the point), maxZoom 8 (a two-language family must not fill the screen with its own neighbourhood gone), duration 900, and the floor zoom that stops framing one node from zooming the reader OUT are now stated with their reasons and asserted. Two branch rules with them, both of which a tidy-up would erase: reduced motion is a DIFFERENT CALL (jumpTo, not easeTo with duration 0 -- the engines do not treat those as one), and a single point CENTRES rather than fitting a degenerate box, because a one-language family is ordinary and fitBounds on no extent zooms to nothing. THE ROLLUP IS OVER THE READER'S SCOPE. "A family shows the worst colour below it" is only true relative to what is currently on the board: a family that is red overall can be green inside a filter. Handing the rollup a whole subtree makes the colours quietly disagree with the board -- no error, no blank, just a wrong fact about a real people group. Asserted in both directions, and colorFor stays injectable because nothing in the drawing depends on the rollup being worst-of. THE CLICK TOGGLE MOVED TO THE HOST, and that is a change rather than a codification: GiTree cleared the selection when you re-clicked the selected node, and that toggle lived in the component's own handler. A host that wants it must now write it. In exchange the chart holds no selection state. gospel's UI is not wired and their boardEngine.js is untouched on main; they adopt on their own schedule. |
||
|
|
03c310833b |
The whiteboard is a layer of tiered points; only the edges were ever a tree
Driver brief §B, the seam under the language tree, so gospel's set charts inherit zoom, label priority and tiling from the map engine instead of re-implementing them in SVG. READ FROM GiTree RATHER THAN ASSUMED: six layer groups, exactly ONE hierarchical. The parent->child links are sourced from linkFC; the points, the selection and hover rings, the dish behind a cluster and EVERY label layer come from one point collection and know nothing about trees. gospel re-read that independently and found the same split, which is the only reason I trust it -- "the seam is generic" is the claim that should never travel on one reader. So library/charts/whiteboard/tiered-points.js takes a point FeatureCollection whose features carry id, label, tier and priority, and gives each TIER its own symbol layer with its own minzoom. That is the whole of "band names zoomed out, ring names mid, group names in": the engine is told which labels may exist at which zoom and resolves the collisions itself. Paint expressions pass through unread -- the layer has no opinion about what a colour means, which is what keeps it reusable. Selection and hover are FILTERED layers, so hover costs one setFilter and structurally cannot move a camera: there is nothing in here that could. AND THE PROJECTION IS NOT A SCALE, which is the part that would have shipped wrong. gospel asked for degrees and planned to scale by one number; their charts are CONCENTRIC RINGS, and Mercator stretches vertically by 1/cos(lat) -- 1.0154x at 10 degrees, 1.1547x at 30 -- so a linear scale renders rings as OVALS, worst at the outer ring where the labels a reader compares are. library/maps/whiteboard-projection.js is the inverse conversion, EXTRACTED from build-language-tree.mjs rather than written, because the second copy was about to be made by hand. The degree-height it emits shrinks from 0.9996 of the width at the centre to 0.6794 at the edge; that shrink IS the correction the renderer then cancels. TWO TESTS, EACH GUARDING A DIFFERENT RISK. check:whiteboard-projection reproduces the old private implementation VERBATIM as a reference and compares over 1,734 grid points EXACTLY -- the language tree is a build-time artifact, so a rounding shift would move 4,000 coordinates and look like nothing. It then asserts a ring renders round at the inner, middle and outer radii, AND that a linear scale FAILS that same assertion, because an assertion the defect can pass is measuring nothing. check:tiered-points runs gospel's CENTERED SET through the layer: 2,079 points, 3 tiers, priority by population, and NO EDGES. The absent links layer is the evidence -- if the whiteboard draws this chart with no line layer anywhere, edges are separable in fact rather than in my reading. The map is a recording fake, so the assertions are about what the module DOES; a regex over the source would pass on a module that never called addLayer. Negative-tested: making the projection linear, changing its rounding, dropping the label priority, or giving every tier the same minzoom each turn one of them red. gospel's UI is NOT wired -- their component is theirs to write, later, against a module that now exists. Their boardEngine.js stays where it is until the chart lands. |
||
|
|
42b4c8f16b |
Two readers of one private field, and nothing was holding them together
internal/lib/facet-walk.mjs's __srcFeatures (injected into the page by
check:facet-changes-map) and library/maps/pin-selection.js's readSourceFeatures (the
packet and zoomToLegendRow) answer the same question about the same private GeoJSON
field. They are two copies because one must be a string evaluated in a browser and the
other an ES module, so they cannot be one function -- but nothing tested that they
still agreed, and they had already stopped.
THE CROSS-CHECK FOUND A DIVERGENCE ON ITS FIRST RUN, which is the argument for it. A
source may hold a single Feature rather than a FeatureCollection; both engines accept
that. The product's reader handled it and the gate's did not, so the gate answered
"cannot measure" about a payload it could read perfectly well. Nobody reading either
file would have noticed: each is correct on its own terms. __srcFeatures now handles
it and the two agree.
ONE DIFFERENCE IS DELIBERATE AND IS NOW PINNED AS SUCH RATHER THAN WAIVED: on a
url-backed source the gate returns null while the product returns [] and warns. Both
are right for their caller -- a gate whose job is to tell "the map drew nothing" from
"I could not look" needs a third answer, while a selection returning null would break
the Set arithmetic every caller does on it. Asserted in both directions, so neither
side can quietly adopt the other's and call it a fix.
AND NEITHER MAY BE SILENT ABOUT A SHAPE IT DOES NOT KNOW. maplibre has a THIRD _data
shape: updateData() stores { updateable } (maplibre-gl-dev.js:49001). No product code
calls updateData today, so this is a landmine rather than a bug -- and it is armed the
moment someone adopts partial updates for performance, at which point the product
reader would have gone back to returning [] in silence: "the map drew nothing", stated
as fact about a payload nobody recognised, one line below the url warning that exists
to prevent exactly that. Both readers now name the unrecognised shape by its keys, and
a fixture holds it, so adopting updateData() trips a test instead of blanking a map.
Negative-tested: dropping the single-Feature case, silencing either reader, or making
the gate adopt the product's url answer each turn it red; restored, 14/14 green.
check:pin-selection still passes, so the product reader is unchanged where it matters.
|
||
|
|
88fe7509d0 |
The packet was praying over the numbers the page loaded with, not the live ones
rega RQ3(b): the packet's "Has Prayer" selection showed 50 of 997 where the legend row said 699. The legend row is right and the packet was wrong. A pin's prayer count arrives TWICE. The GeoJSON property is whatever was true when the page loaded; from then on polling writes the current count with setFeatureState, and prayer-progress.js:94 colours from ['feature-state','peoplePraying'] once polling is on. So the lit pins a reader counts are feature-state, and pin-selection.js had no 'feature-state' case at all -- it resolved to undefined, to-number defaulted it to 0, and the comparison selected only the pins whose FROZEN property happened to be above zero. 50 of them. The other 649 people groups had been prayed for and the packet left them out, which is the one thing a prayer packet must not get wrong. resolveExpr and matchesExpression now take the state alongside the properties, and selectedPinIds reads it per pin off the MAP rather than off the feature -- the source still holds the GeoJSON it loaded, setFeatureState never touches it. The source is added with promoteId:'uniqueId', so the id feature-state is keyed by is the same props[idKey] the selection already uses. Degrading is deliberate in both directions: before the first poll there is no state and property expressions still select, so an early packet is thinner and never empty; getFeatureState throws on an unknown source, so it is guarded and a bad source loses the selection rather than aborting the print. ALSO, rega RQ3(a), a candidate and not yet a confirmed cause. prewarmRegionPaint wrapped any non-feature-reading paint value in a case to make it data-driven, including a zoom interpolate -- I argued for that explicitly. useMapLayers.js:319 already held the rule and I had not read it: zoom interpolation is kept by putting the case in the interpolate OUTPUTS, and "the reverse (zoom nested inside a 'case') is what breaks the layer, so don't do that." That reverse is the shape Mapbox classifies as composite before reaching for an interpolation factor the expression cannot supply, which is the class of the interpolationFactor page error. The wrap now skips zoom expressions too. A missed pre-warm costs a frame; an illegal paint costs the layer. None of the three properties it touches is a zoom interpolate at boot, so this is not proof -- it removes the only writer that can build that shape from a runtime value. check-pin-selection asserts the live count wins over the stale property, that the stale property alone selects nothing, that a map with no feature-state still selects on properties, and that a throwing getFeatureState degrades. Negative-tested: removing the case, not reading the state, and dropping the throw-guard each turn it red; restored, green. |
||
|
|
f44da45258 |
A test for the source reader, because a no-op cannot be seen any other way
dx-shepherd asked for this with a7b12b44 and the reason is exact: every caller of readSourceFeatures is
written to do NOTHING when it finds no features, deliberately, so that a legend row for an off-screen
region degrades quietly instead of throwing. That is why the MapLibre bug produced no error, no console
line and no visual difference except a camera that did not move — and why it survived until a packet
printed the wrong 50 people.
check:pin-selection drives the reader with fixtures for both engines and no map at all:
· the MAPBOX shape (_data IS the FeatureCollection) reads its features;
· the MAPLIBRE shape (_data is { geojson }) reads the SAME features — the regression that shipped;
· a REMOTE source ({ url }) returns [] AND WARNS;
· the serialize() fallback works, a missing source is [] rather than a throw, a bare Feature is one;
· selectedPinIds ANDs its expressions, and with none returns every pin rather than none.
★ NEGATIVE-TESTED, which is the only thing that makes it worth having: run against the reader as it
shipped, it fails with exactly four findings — the maplibre shape reads 0 instead of 3, selectedPinIds
narrows to nothing, "no expressions" returns nothing, and the remote case returns [] silently. Against
the fixed reader it passes. A test that cannot fail on the code it was written for proves nothing.
★ AND THE REMOTE CASE IS NOW A DECISION, not an accident. dx-shepherd asked what { url } should do. It
returns [] and warns once per source: the features live on a server and cannot be enumerated locally, so
"I cannot read this" is a different fact from "there is nothing here". Collapsing those two is precisely
how the MapLibre bug hid for as long as it did, and no caller can tell them apart from an empty array.
Verified: check:pin-selection, check:mjs-parse, check:doc-scripts, check:package-json-unique-keys all
PASS. gates:pre was NOT run clean here — my main clone sits three landings behind origin/main, so a
newer package.json lists check:targets while that file is absent from the working tree; that failure is
my verification setup, not this change, and I checked it on an untouched tree before reporting.
|
||
|
|
8ce5b9fdd1 |
Reading a GeoJSON source found nothing on MapLibre, which broke more than the packet
rega's console answered the question in one line: "0 pin(s) selected but no rows matched", for both a
language-family selection and a prayer selection, with the pins plainly on screen. Not an id mismatch and
not a missing filter — readSourceFeatures returned an empty list.
THE TWO ENGINES STORE _data DIFFERENTLY. maplibre-gl's GeoJSONSource wraps it:
this._data = typeof options.data === 'string' ? { url: options.data } : { geojson: options.data }
so `data.features` is undefined there. Mapbox stores the GeoJSON directly. This code was written against
Mapbox, where it works, and has been returning nothing on MapLibre ever since — which is the keyless
engine, the one the public builds use.
★ THE PACKET IS THE SMALL HALF. This reader is what zoomToLegendRow uses to answer "which pins does this
legend row select", and it backs research-map, country-posters and doxa-simple-map. On MapLibre every one
of them has been failing to fit the camera to a clicked legend row — silently, because the function is
written to no-op rather than throw when it finds no features. The original comment above it explains that
querySourceFeatures is viewport-clipped and would make off-screen rows "silently no-op"; the replacement
did exactly that on one engine, for a different reason.
The wrapper is unwrapped, and a URL source returns [] explicitly — having no local features to read is a
different fact from finding none, and collapsing the two is how this hid.
Verified: gates:pre 13/13. Needs eyes, and the second one matters more than the first:
· the packet on MapLibre prints the SELECTED groups, cover count no longer the whole list;
· clicking a legend row on MapLibre FITS THE CAMERA to that row — on research-map, country-posters and
doxa-simple-map. If that has been dead on the keyless engine, this is the commit that revives it.
|
||
|
|
2ea3c53e0f |
The packet prints what the reader selected, and the question "which pins is that" becomes shared
PACKETSEL, found by exp-shepherd: the prayer packet printed the first 50 of EVERY people group whatever was selected, so a reader who had narrowed the map to one language family got a packet about somebody else. I carried that behaviour through verbatim when I moved the packet to the shared printer — it was already wrong and I preserved it, which is worth saying plainly. The rows now come from the pins currently SELECTED: the layer's own filter decides which pins render, `_pinMatchExpr` decides which of those are lit (the affinity tab dims rather than filters), and both must match — a pin at opacity 0 is not what the reader is looking at. ★ ASKED OF THE MAP'S FEATURES, NOT OF THE ROWS, and this is the part that had to be got right rather than guessed. Pin properties are DERIVED from rows: useMapLayers fills languageFamily in when the API returns null for all 2,069 groups, and canonicalizes doxaRegion onto the palette's key forms precisely so setFilter matches. So evaluating a pin filter against a raw data row is wrong in a way that NEVER THROWS — it just selects a different, plausible-looking subset. The features the map holds are the only place the two vocabularies agree. Answering that question already existed, privately, inside useMapFly: zoomToLegendRow has to solve it to fit the camera to a legend row, including the reason it cannot use querySourceFeatures (viewport-clipped, so an off-screen region silently selects nothing). Those three functions move VERBATIM to library/maps/pin-selection.js and useMapFly imports them, so there is one evaluator rather than two — and country-posters, which prints the same kind of packet from the same kind of selection, has the same helper to reach for instead of a second copy. Two failure modes are distinguished rather than collapsed: pins selected but NO row matching them is an id mismatch, so it prints the full list and warns; a genuinely empty selection still reaches the shared printer's own "that list resolved to no records", which is honest. And when the 50-row safety cap bites, the cover now says "— 50 of 214" instead of quietly being a different packet than the one asked for. Verified: gates:pre 13/13, check:composite-interop PASS. Needs eyes: rega — select one language family, print, and check the cards are that family and the cover carries the count. |
||
|
|
5f053d992c |
The galaxy basemap was localizable all along: its label keys use an underscore, not a colon
exp-shepherd found research-dashboard rendering ENDONYMS in every locale — 中国 and Türkiye on a Russian page — and fetching a glyph range for every script on screen. The reading was that its basemap carries no localized names and the choice was to swap the basemap or give up on its labels. Measured against the live tiles, neither is necessary: the names are there, under keys this chain never tried. The galaxy style is VersaTiles Colorful on the SHORTBREAD schema, whose label keys are name_ar / name_de / name_en, not name:ar. This coalesce only tried the colon spelling, so all 43 of that style's label layers fell through to `name` — the local name — while the localizer ran and reported success. A running fix that cannot reach its data looks exactly like a missing feature. MEASURED, six z3/z4 tiles across Europe, Asia and Africa, identical key sets on every one: name_ar, name_de, name_el, name_en, name_es, name_fr, name_it, name_nl, name_pl, name_pt, name_uk. Of the eleven locales we ship that is SEVEN in their own language — ar, de, en, es, fr, it, pt — with no change to the basemap the Driver chose and no change to how the map looks. hi, ro, ru and zh are genuinely absent from those tiles, so they now take name_en: one Latin script rather than mixed CJK and Devanagari endonyms, which is also the single-script glyph saving. That is the same role name:latin already plays for the OpenFreeMap styles, so it sits in the same position in the chain rather than becoming a special case. NOTHING CHANGES FOR THE OPENFREEMAP STYLES, by construction: the colon spelling stays first, so their name:<lang> or name:latin still wins and the underscore entries are never reached. The order is also what keeps the already-rewritten sentinel working — it tests for a chain beginning ["coalesce",["get","name:, which this still is. Verified both, rather than assumed. WHAT I DID NOT DO: swap the basemap. galaxy resolves to VersaTiles Colorful because the Driver asked for blue oceans like the Mapbox map and that style's water measured closest (#bfd9f2; OpenFreeMap Liberty is #9ebdff, noticeably stronger). Trading a look he chose for four locales' labels is his call, not mine, and with this change those four get readable Latin labels anyway. Verified: gates:pre 12/12. NOT measured here: glyph KB per locale, which needs a browser — rega has the instrument and the before numbers. |
||
|
|
3bb5fd7b86 |
Keyless maps label places in one language, English included
English readers got the style's own bilingual labels, native script under every name, so an English page fetched a glyph range for every script at world view: Prayer Progress loaded 18 font/sprite requests (777 KB) in English against 5 (185 KB) in Arabic. English now gets the same single-language labels as every other locale. |
||
|
|
e94cb56b79 |
Map labels ask for a font the engine's glyph server has: one shared helper
library/maps/label-font.js picks a symbol layer's text-font by engine: Noto Sans
(Regular, or Bold for medium/bold) under MapLibre, which is what OpenFreeMap
serves, and exactly the caller's fonts under Mapbox. Mapbox maps render as
before.
useMapClustering hard-coded 'DIN Offc Pro Medium, Arial Unicode MS Bold', which
OpenFreeMap does not have. Under MapLibre every native cluster count requested
it and got a 404; MapLibre then drew the counts in its fallback weight instead
of the intended bold. It now uses the helper, as do upg-neighbor (replacing its
own copy), prayer-walk-planner, and key-cities' city-name labels, which named no
font and so asked for MapLibre's default (also a 404 on OpenFreeMap).
Measured by importing the real composable in a browser, 3,000 points in native
mode:
- main under MapLibre: 1 glyph 404 (DIN Offc Pro Medium,Arial Unicode MS Bold),
and the counts drawn in the fallback weight;
- this change under MapLibre: 0 glyph 404s, text-font Noto Sans Bold, 22
clusters with counts in bold;
- this change under Mapbox: text-font still exactly DIN Offc Pro Medium /
Arial Unicode MS Bold.
No production map reaches native cluster counts today (Simple Research and
Country Posters force the 'mst' line mode), so this was latent in shared code.
Correction to
|
||
|
|
319064a11b |
Map place names in the reader's language, on both engines
Every map translated its own chrome but left the basemap's country, city and street names in the style default (English on Mapbox, Latin + native on OpenFreeMap) in all eleven locales. A reader in Arabic saw an Arabic legend over an English map. library/maps/localize-labels.js uses each engine's supported mechanism: - Mapbox: map.setLanguage(lang). It also localizes the Standard style's IMPORTED basemap, which a layer rewrite cannot reach. - MapLibre: basemap symbol layers point at ['coalesce', name:<lang>, name:latin, name]. English restores each layer's original expression, re-captured on every style load so a theme switch never restores the previous style's labels. Only layers whose text-field reads an OSM name field are touched. A plugin's own label layers (zone titles, group names) are never rewritten. useMapInstance applies it on every style.load and when the reader's language changes (ProfileLoader's 'lang', else profile-config locale, else ?lang / <html lang>). The Prayer Walk Planner drops its private copy for the shared one. Measured on the Prayer Progress map, ar/zh/ru/hi: - Mapbox getLanguage() == locale in all four. - MapLibre: 13/14 symbol layers localized, and the plugin's own label layer untouched. Read in the Arabic screenshots: القاهرة, الإسكندرية, السويس, الأردن on both engines. check:gallery-embed (+ :maplibre) and check:prayer-walk-planner pass. The shared change reaches all 11 bundles that compile initializeMap, and all are rebuilt here. check:shared-reached marker basemap-labels-localized: 10/10 consumers. |
||
|
|
4fb8176b00 |
Shape Arabic, Hebrew and Persian map labels on MapLibre maps too
library/maps/rtl-text.js installed the RTL text plugin for MAPBOX only (window.mapboxgl, from api.mapbox.com). So every MapLibre map, i.e. every keyless embed, drew RTL place names as disconnected, reversed glyphs. Measured on the Prayer Walk Planner's printed Arabic sheet. Under it was a second gap. Bundle entries call the installer at LOAD time, before useMapInstance has lazily injected a MapLibre engine, so even an engine-aware installer found nothing to install into. - rtl-text.js is engine-aware. Mapbox is unchanged (same URL, same lazy call). MapLibre gets the same plugin package from jsDelivr, keyless and not api.mapbox.com, so keyless pages still make zero Mapbox requests. New ensureRtlTextPlugin(gl) waits until it is LOADED, for callers that capture a single frame (print). - useMapInstance installs it the moment an engine global exists, the one point every map passes through. - The planner drops its private copy for the shared helper. Measured: a print map of downtown Cairo (MapLibre, keyless) shows shaped Arabic street and place names (شارع التحرير, مسجد الطباخ …); plugin status "loaded", fetched from jsDelivr. check:gallery-embed passes js, iframe, neighbour and linked (14 tabs); :maplibre passes; check:prayer-walk-planner passes. The shared change reaches all 11 bundles that compile initializeMap, and all are rebuilt here. check:shared-reached gains an rtl-text-maplibre marker (10/10 consumers carry it; cities-internet, gospel-resources and my-upg-100-list boot maps another way and are not affected). Also: the new Experimental bundle pplr-data-maps loads the Mapbox geocoder under maplibre (pplrData.js:38). It is an owned, printed MAPBOX_KNOWN entry routed to its owner; delete it in their fix commit. |
||
|
|
c959ed6323 |
Load the RTL text plugin from one place, and in the composite too
Some checks are pending
build-gate / build (push) Waiting to run
The cap-4 composite bakes research-dashboard's .vue directly, so it never runs
that bundle's entry file and has to repeat everything the entry does. It
repeated Pinia, i18n and the colour strategies. It did not repeat this one.
Measured in the shipped bundles:
research-dashboard.js ..... 1 occurrence of mapbox-gl-rtl-text
doxa-research-map.js ...... 1
dashboard-plus-grid.js .... 0
So Arabic, Hebrew and Persian labels rendered reversed inside the cap-5 shell
and correctly standalone. Not cosmetic here: Arab World has 99 rows and
Persian-Median 49, and a reversed label is a wrong NAME for exactly the groups
this map is about. It failed silently -- no error, no warning, just mirrored
text that reads as a font problem to anyone who does not read the script.
Fixed as a shared library function rather than a fourth hand-copy, which is
what the composite-registration card asks for: one definition, two callers,
nothing to drift. Idempotent by construction, since Mapbox throws on a second
setRTLTextPlugin in a session and that throw is swallowed.
Four of the five entry side effects fail silently; only provide('profileModules')
throws, and that is the one safe to omit because a baked composite does not use
ProfileLoader. So "it renders" is no evidence the entry's work was done.
Found by the map-beauty agent, which I verified independently before acting
rather than taking the bundle counts on report.
|