Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions content/quiet-corners-maps.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion content/quiet-corners.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ and never asks a camera operator for anything by itself; pressing a camera's
name is what opens its card on the map. A camera 25 km away is not the same
weather across a ridge, and the list says so.

The same map carries the [Field Maps](/quiet-corners/maps/) builder's Layers panel — base map choice, a names-and-search layer, a Places in this view list, display controls, and a set of starting presets, one of which, **Compare counties**, turns on this page's own county-ranking wash (off by default) through the same switch the Layers panel's **Land, sky & light context** group already uses for every other context surface: there is no separate toggle for it. Undo and Reset sit beside the presets for backing a shortcut out again. See the Field Maps page for the full list and what each one moves.
The same map carries the [Field Maps](/quiet-corners/maps/) builder's Layers panel — grouped by task (**Map style**, **Places & routes**, **Weather & conditions** and so on, with an **Active on map** list above them) — base map choice, a names-and-search layer, a Places in this view list, display controls, and a set of starting presets, one of which, **Compare counties**, turns on this page's own county-ranking wash (off by default) through the same switch the Layers panel uses for every other colour surface (the county ranking sits under **Explore counties**, and like night lights, radar or slope it is one choice among them, or **No context surface**): there is no separate toggle for it. Undo and Reset sit beside the presets for backing a shortcut out again. See the Field Maps page for the full list and what each one moves.

On a wider screen the four panels — Explore, Conditions, Layers and Print — sit in one column to the left of the map, one open at a time, so none of them ever covers the map; the column is the same width whichever panel is open, so the map never shifts under you, and Explore's county and town tables show each place as a small card with every figure labelled instead of a wide table. On a phone, opening any panel — Explore, Conditions, Layers or Print — opens it in a sheet under the map instead of over it, so the map stays visible and never blank. Drag the sheet's handle up or down, or use its Expand/Shrink button, to see more of the panel or more of the map; Close or Escape returns you to whichever control you opened it from.

Expand Down
24 changes: 21 additions & 3 deletions scripts/qcm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,11 +411,12 @@ scripts. `QCM_SMOKE_NO_BROWSER=1` makes the CLI behave as if there were no Chrom

## The context group: surfaces and overlays

**Land, sky & light context** holds two kinds of row, and which kind a row is follows how it
renders, not what it is about. A row that fills the view as a raster (`rasterSurface` in
The context rows have no heading of their own since QQA-18: each sits under its task group
(`TASK_GROUPS` in `layer-registry.js`, see below), and they are two kinds of row, which kind
following how a row renders, not what it is about. A row that fills the view as a raster (`rasterSurface` in
`layer-registry.js`: night lights, radar, slope, land cover, tree canopy, the county wash,
the hillshade) hides what is under it, so those stay one radio group (`qcm-context`) with an
explicit **No context surface**. A row that draws vector ink on top (NIFC fire perimeters,
explicit **No context surface** (`qcm-context-none`, homed under Map style as the `noContextSurface` pseudo-id; the radios share one `name`, so the choice stays exclusive across headings). A row that draws vector ink on top (NIFC fire perimeters,
air quality, smoke, flood zones, aurora, wildlife) is an ordinary checkbox
(`qcm-layer-<id>`) and coexists with whichever surface is chosen. Ids never change, so an old
share link or saved state that names fire or air quality as its surface (`s=`) still turns
Expand All @@ -433,6 +434,23 @@ a height ceiling (7.5rem) that scrolls. With a surface and every overlay on it s
| `surface-choice.test.mjs` | The radio group in a browser: None at rest, one surface at a time. |
| `context-overlays-browser.test.mjs` | Fire and air quality are drawn together, with or without a surface; old links restore by id; the caption is one line per entry within its height budget. |

## Layer panel task groups

The Layers panel headings are the plan 4.7 task groups, one primary home per layer id:
`TASK_GROUPS` and `TASK_GROUP_OF` in `layer-registry.js` (a presentation map, separate from the
registry `group`, which stays the rendering role: base, federal, state, context, reading, cameras,
extras). A new provider adds a row to an existing task group, never a new top-level group, and
`layer-task-groups.test.mjs` fails when a registry id has no home. `main.js` renders rows by id,
so no layer id, input id, exclusivity or render path moves. `activeOnMapRows` and
`groupsToCollapse` (`layer-panel.js`) decide the **Active on map** list (checked, not a base
choice, not a Print options row) and which groups a cleared search closes again. The count line agrees with that list: `COUNTED_SCREEN_GROUPS` leaves `extras` out, so a print extra never reads as "1 layer on". The list is rebuilt only when its content changes, so a focused Remove or Retry button keeps focus through a fetch re-render. Task presets
(Explore outdoors, Check conditions, Night sky) are not built yet.

| Proof | What it holds |
| --- | --- |
| `layer-task-groups.test.mjs` | The ten headings in order; every registry id, base and the None row has exactly one home; Print options equals the extra-page rows. |
| `layer-groups-browser.test.mjs` | Journey 11: groups collapsed, search by alias, purpose and provider with the expansion restored, activate across topics, find and remove an active layer after filtering, a print extra, an old `#map=v1` link restored by id. |

## Physical print proof

Layout is proven here without paper. Whether a printer honours it is a separate check:
Expand Down
3 changes: 3 additions & 0 deletions scripts/qcm/blm-rec-sites.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ const literals = (src) => [...src.matchAll(/'((?:[^'\\\n]|\\.)*)'|"((?:[^"\\\n]|
test('no string a reader sees claims a darkness rating, or a sky at all', () => {
const fromRecords = CAPTURE.features.flatMap((f) => siteLines(normalizeBlmSite(f)));
for (const s of [...readerStrings(), ...fromRecords, ...literals(MODULE), ...literals(REGISTRY)]) {
// QQA-18: the registry now names the layer panel's Night sky task group (the plan's own heading);
// that exact heading is a place to find night lights and aurora, not a claim about a BLM site.
if (s === 'Night sky') continue;
for (const rx of FORBIDDEN) assert.ok(!rx.test(s), `${rx} matches a reader-facing string: ${JSON.stringify(s)}`);
}
const l = LAYER_BY_ID.blmRecSites;
Expand Down
4 changes: 2 additions & 2 deletions scripts/qcm/context-overlays-browser.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ test('fire perimeters and air quality are independent checkboxes in the context
const page = await freshPage();
const r = await page.evaluate(`(() => {
const el = (id) => document.getElementById(id);
const group = [...document.querySelectorAll('.qcm-layer-group')].find((g) => g.getAttribute('aria-label') === 'Land, sky & light context');
const group = [...document.querySelectorAll('.qcm-layer-group')].find((g) => g.getAttribute('aria-label') === 'Weather & conditions');
return { fireType: el('qcm-layer-nifcFirePerimeters').type, aqiType: el('qcm-layer-epaAirNowAqi').type,
fireInGroup: group.contains(el('qcm-layer-nifcFirePerimeters')), aqiInGroup: group.contains(el('qcm-layer-epaAirNowAqi')),
noFireRadio: !el('qcm-context-nifcFirePerimeters'), noAqiRadio: !el('qcm-context-epaAirNowAqi'),
radios: [...document.querySelectorAll('input[name="qcm-context"]')].map((x) => x.value) };
})()`);
assert.equal(r.fireType, 'checkbox');
assert.equal(r.aqiType, 'checkbox');
assert.ok(r.fireInGroup && r.aqiInGroup, 'the overlays stay in the context group, not a new top-level group');
assert.ok(r.fireInGroup && r.aqiInGroup, 'the weather overlays sit under Weather & conditions, beside the radar they draw on');
assert.ok(r.noFireRadio && r.noAqiRadio, 'a vector overlay must not also be a radio');
assert.ok(r.radios.includes('none') && r.radios.includes('nightLights'), 'the raster surfaces are still the radio choice');
assert.ok(!r.radios.includes('nifcFirePerimeters') && !r.radios.includes('epaAirNowAqi'));
Expand Down
1 change: 1 addition & 0 deletions scripts/qcm/control-register.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
{"id":"label-size","selector":"#qc-label-size","pages":["dashboard"],"family":"Map label size / crowding / contrast / emphasis","purpose":"Label text size.","scope":"Place labels.","effect":"Redraws labels at Normal, Large or Extra large.","keyboard":"Tab to it, then arrow keys (or Alt+Down) choose an option.","persistence":"saved + shared link","tests":["scripts/qcm/label-prefs-browser.test.mjs","scripts/qcm/label-size-browser.test.mjs"]},
{"id":"layer-search","selector":"#qcm-layer-search","pages":["dashboard"],"family":"Basemap / context / overlays / presets","purpose":"Filter the layer list by name.","scope":"Which layer rows are shown; no layer is switched.","effect":"Hides rows that do not match the typed text.","keyboard":"Tab to it, then type.","persistence":"none","tests":["scripts/qcm/camera-discovery-browser.test.mjs","scripts/qcm/layer-panel-browser.test.mjs","scripts/qcm/layer-panel-compact-browser.test.mjs"]},
{"id":"layers-active-only","selector":"#qcm-layers-active-only","pages":["dashboard"],"family":"Basemap / context / overlays / presets","purpose":"Show only the layers that are switched on.","scope":"Which layer rows are shown; no layer is switched.","effect":"Hides rows that are off.","keyboard":"Tab to it, then Space toggles it.","persistence":"none","tests":["scripts/qcm/layer-panel-browser.test.mjs","scripts/qcm/layer-panel-compact-browser.test.mjs"]},
{"id":"active-remove","selector":"button.qcm-active-remove","pages":["dashboard"],"family":"Basemap / context / overlays / presets","purpose":"Remove: switch a layer off from the Active on map list.","scope":"That layer's own switch; the map and the print recipe follow it.","effect":"Presses the layer's own control, so a context surface goes through No context surface; focus moves to the list title or the layer search.","keyboard":"Tab to it, then Enter or Space.","persistence":"shared link","transient":true,"needle":"qcm-active-remove","tests":["scripts/qcm/layer-groups-browser.test.mjs"]},
{"id":"layer-retry","selector":".qcm-layer-retry","pages":["dashboard"],"family":"Camera / freshness / link-out / retry","purpose":"Retry a layer whose data failed to load (one button per retriable layer, shown only after a failure).","scope":"That layer's fetch.","effect":"Requests the layer again and updates its status line.","keyboard":"Tab to it, then Enter or Space.","persistence":"none","needle":"qcm-layer-retry","tests":[],"untested":"No test file under scripts/qcm names this control; the gate and the unit that owns it should add one."},
{"id":"camera-goto","selector":".qcm-camera-goto","pages":["dashboard"],"family":"Camera / freshness / link-out / retry","purpose":"Go to a camera coverage region (West, California, Great Smoky Mountains).","scope":"Map view; opens the Cameras group and focuses that region's switch.","effect":"Fits the map to the region.","keyboard":"Tab to it, then Enter or Space.","persistence":"shared link","tests":["scripts/qcm/camera-discovery-browser.test.mjs"]},
{"id":"preset","selector":"button[id^=\"qcm-preset-\"]","pages":["dashboard"],"family":"Basemap / context / overlays / presets","purpose":"Preset buttons (Land access, Public land, Trails & terrain, Night sky, Compare counties, Map only, Loud or quiet) and their Undo and Reset.","scope":"The base map, the context surface and the layer switches they move.","effect":"Applies a named set of switches, says what moved in the status line; Undo restores only what the last press moved.","keyboard":"Tab to it, then Enter or Space.","persistence":"shared link","needle":"qcm-preset-","tests":["scripts/qcm/presets-browser.test.mjs","scripts/qcm/road-labels-browser.test.mjs"]},
Expand Down
6 changes: 5 additions & 1 deletion scripts/qcm/layer-count-browser.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ test('an area drawn and sources checked (recipe set, rows rebuilt): checking Roa
});

test('a context radio is counted, and "No context surface" takes the count back to none', { skip }, async () => {
const page = await freshPage({ group: 'Land, sky & light context' });
// QQA-18: the surface radios sit under their task heading (night lights: Night sky), while
// "No context surface" sits under Map style -- one `qcm-context` choice across two groups.
const page = await freshPage({ group: 'Night sky' });
const id = CONTEXT_SURFACE_IDS[0]; // a radio; overlays are qcm-layer-<id> checkboxes
const styleSel = '.qcm-layer-group[aria-label="Map style"]';
await trustedClick(page, `${styleSel} summary`, { until: `document.querySelector('${styleSel}').open` });
await trustedClick(page, `#qcm-context-${id}`);
assert.equal(await page.evaluate(`document.getElementById('qcm-context-${id}').checked`), true, 'the trusted click did not select the context radio');
assert.equal(await countText(page), '1 layer on', `context surface "${id}" is selected but the count does not say so`);
Expand Down
16 changes: 12 additions & 4 deletions scripts/qcm/layer-count.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ test('a checked camera layer is counted, never "No layers on"', () => {
assert.equal(line({ roadCameras: true, parkCameras: true }), '2 layers on');
});

test('context, reading and extras layers count the same way', () => {
assert.equal(line({ aerial: true }), '1 layer on');
test('context and reading layers count the same way', () => {
assert.equal(line({ padusAccess: true }), '1 layer on');
const context = LAYERS.find((l) => l.group === 'context');
assert.equal(line({ [context.id]: true }), '1 layer on');
Expand Down Expand Up @@ -49,10 +48,19 @@ test('no required row sits in a counted group, so the count needs no `required`
assert.deepEqual(stray, [], 'a required row in a counted group would always read as "on"');
});

test('every registry group is either counted here, badged by main.js, or the basemap choice', () => {
test('QQA-18: a print extra is a page in the PDF, not a layer on the map, so it never moves the count', () => {
for (const id of ['aerial', 'terrain', 'tripPage']) {
assert.equal(LAYERS.find((l) => l.id === id).group, 'extras');
assert.equal(line({ [id]: true }), 'No layers on', `${id} is a print extra and must not read as on the map`);
}
assert.equal(line({ tripPage: true, roadCameras: true }), '1 layer on');
assert.ok(!COUNTED_SCREEN_GROUPS.includes('extras'));
});

test('every registry group is either counted here, badged by main.js, the basemap choice, or a print page', () => {
const badgedByMain = new Set(['federal', 'state']);
for (const g of GROUPS) {
if (g.id === 'base') continue;
if (g.id === 'base' || g.id === 'extras') continue;
assert.ok(badgedByMain.has(g.id) || COUNTED_SCREEN_GROUPS.includes(g.id),
`group "${g.id}" is in no count: its checked rows would read "No layers on"`);
}
Expand Down
Loading
Loading