clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

fix: one count for what the map is drawing

Dami committed c6eb7d6
The window carried two counts of the same map and they did not agree. The
footer said "16 features enabled · 16 in view" -- two words for one number,
neither of them saying which set it meant -- while the panel on the other side
of the map said "60 features, filtered" with a single location standing on the
chart. Both were reading their own definition of visible: the footer asked the
legend's ledger, the dock asked the search, and the canvas asked every filter
there is. Highlighting one zoning district culls every location outside it, so
the three answers came apart by a factor of four, and a reader with no way to
tell which one was the map's rightly trusted none of them.

There is one answer now and every surface asks it: the point features that
survive every filter, and the ground whose collection is shown. The footer
reads "16 of 16 features in view" -- one set, counted twice, the second number
the same one the dock puts at the top of its list. The dock lists exactly what
is out there rather than everything the legend would allow, and its flag rises
for any filter that can move the number, not only for a collection put away.

The relist rides with the repaint, too. Highlighting ground and descending
into a cell went straight to the canvas and left the panel beside the map
offering features the map had just taken away; the one call that repaints now
recounts and relists as well, so a filter lands on all three surfaces or on
none.
commit
c6eb7d60297312761334d566936ce7a9fa8d933e
parent
7566e181f1c1fe99b0ce0aac61dbfae1d0c30633
author
Dami <dami.etoile@gmail.com>
kind
fix risk score 2.1 of 3, Jev's estimate
6 files changed, +111 -46
Mfrontend/src/features.js7+++-
Mfrontend/src/grid.js18++++-----
Mfrontend/src/legend.js5+--
Mfrontend/src/navigation.js36++++++++++++--------
Mfrontend/src/search.js35+++++++++----------
Afrontend/src/visibility.js56++++++++++++++++++++++++++++++
M frontend/src/features.js +6 -1
M frontend/src/grid.js +8 -10
@@ -19,7 +19,7 @@ import { gridTheme, palette } from "./constants.js";
1919 import { closeDetail } from "./detail.js";2020 import { elements } from "./dom.js";2121 import { viewMaxZoom } from "./navigation.js";22-import { refreshPrioritySource } from "./features.js";22+import { refreshPinRendering } from "./features.js";2323 import { state } from "./state.js";2424 2525 export function handleGridKey(event) {
@@ -60,9 +60,9 @@ export function toggleGrid(enabled = !state.gridEnabled) {
6060 // and then closed the grid altogether is starting over, not resuming.6161 if (enabled) setSubgridVisible(true);6262 renderGrid();63- refreshPrioritySource();64- state.layers.pins.changed();65- state.layers.priority.changed();63+ // A cell already chosen starts holding the map to itself again the moment64+ // the grid is opened, so the footer and the dock have to hear about it too.65+ refreshPinRendering();6666 }6767 6868 // Dividing the chosen cell and being held to it are two questions. The
@@ -100,9 +100,7 @@ export function selectGridCell(raw) {
100100 state.gridCell = id;101101 if (!state.gridEnabled) state.gridEnabled = true;102102 renderGrid();103- refreshPrioritySource();104- state.layers.pins.changed();105- state.layers.priority.changed();103+ refreshPinRendering();106104 if (!changed) return;107105 closeDetail();108106 state.engine.getView().fit(currentGridExtent(), {
@@ -182,9 +180,9 @@ export function setGridSystem(slug) {
182180 state.gridSystem = slug;183181 state.gridCell = held ? equivalentCell(from, activeSystem(), held, state.world) : "";184182 renderGrid();185- refreshPrioritySource();186- state.layers.pins.changed();187- state.layers.priority.changed();183+ // The carried cell covers slightly different ground, so which features it184+ // holds the map to has moved with it.185+ refreshPinRendering();188186 }189187 190188 // gridCellColor is the accent a cell wears everywhere it is drawn, chosen
M frontend/src/legend.js +2 -3
M frontend/src/navigation.js +22 -14
@@ -2,8 +2,8 @@ import { intersects } from "ol/extent.js";
22 import XYZ from "ol/source/XYZ.js";33 import TileGrid from "ol/tilegrid/TileGrid.js";44 5-import { isCollectionHidden } from "./collections.js";65 import { state } from "./state.js";6+import { visiblePins, visibleZones } from "./visibility.js";77 import { elements, populateSelect } from "./dom.js";88 import { overzoomLevels } from "./constants.js";99 import { loadWorld } from "./catalog.js";
@@ -19,7 +19,6 @@ import { renderLegend } from "./legend.js";
1919 import {2020 applyPinFilters,2121 buildFeatures,22- pinIsHidden,2322 refreshPrioritySource,2423 setHoveredPin,2524 } from "./features.js";
@@ -345,29 +344,38 @@ export function changeZoom(delta) {
345344 });346345 }347346 348-// The footer counts features of every kind: pins by their coordinate, zones349-// by whether their ground reaches into the view. "Enabled" is what the350-// legend's ledger lets draw; "in view" is the part of it under the window.347+// The footer counts features of every kind: points by their coordinate, shape348+// features by whether their ground reaches into the view. It used to read349+// "N features enabled · M in view", which asked the reader to hold two words350+// for two different sets and told them neither: "enabled" was the legend's351+// ledger, "in view" the window over it, and the pair read as a distinction352+// without a difference. It is one set counted twice now -- the features the353+// map is drawing, and how many of them the window is over -- said as one354+// sentence, and the number after "of" is the same number the dock puts at the355+// top of its list.351356 export function updateVisibleCount() {352357 const zoom = state.engine?.getView().getZoom();353358 elements.viewport.dataset.zoom = Number.isFinite(zoom) ? zoom.toFixed(3) : "";354359 if (!state.engine || !state.world) {355- elements.visibleCount.textContent = "0 features enabled";360+ elements.visibleCount.textContent = "No features shown";356361 return;357362 }358363 const extent = state.engine.getView().calculateExtent(state.engine.getSize());359- let enabled = state.eligibleLocations;364+ const pins = visiblePins();365+ const zones = visibleZones();366+ const shown = pins.length + zones.length;367+ if (!shown) {368+ elements.visibleCount.textContent = "No features shown";369+ return;370+ }360371 let inView = 0;361- for (const pin of state.features) {362- if (pinIsHidden(pin)) continue;372+ for (const pin of pins) {363373 const [x, y] = pin.coordinate;364374 if (x >= extent[0] && x <= extent[2] && y >= extent[1] && y <= extent[3]) inView++;365375 }366- for (const record of state.zoneRecords.values()) {367- if (isCollectionHidden(record.zone.collectionId)) continue;368- enabled++;376+ for (const record of zones) {369377 if (intersects(record.extent, extent)) inView++;370378 }371- elements.visibleCount.textContent =372- `${formatNumber(enabled)} features enabled · ${formatNumber(inView)} in view`;379+ elements.visibleCount.textContent = `${formatNumber(inView)} of ` +380+ `${formatNumber(shown)} ${shown === 1 ? "feature" : "features"} in view`;373381 }
M frontend/src/search.js +17 -18
@@ -1,31 +1,23 @@
1-import { isCollectionHidden } from "./collections.js";21 import { elements } from "./dom.js";32 import { saveSession } from "./session.js";43 import { state } from "./state.js";54 import { applyCategoryVisual, applyCategoryGlyph, initials } from "./theme.js";65 import { formatNumber } from "./util.js";6+import { filtersApply, listableFeatures } from "./visibility.js";77 88 // The dock lists at most this many rows. Every annotation still renders on99 // the canvas; the list is a shortlist, and it says when it is one.1010 const dockListLimit = 100;1111 12-// The dock's results pane: every feature that survives the current search and13-// collection filters, alphabetically, capped -- named shape features listed14-// beside the locations, since ground is as findable as anything standing on15-// it. With a search underway the list is exactly the matches; with none it is16-// a browsable index of what the legend has left visible.12+// The dock's results pane: exactly the features the map is drawing, in13+// alphabetical order and capped -- named shape features listed beside the14+// locations, since ground is as findable as anything standing on it. It used15+// to list everything the legend allowed, which is a longer answer than the map16+// gives whenever ground or a cell is holding the view: the panel offered sixty17+// places while one stood on the chart. What is listed is what is out there.1718 export function renderSearchResults() {1819 if (!elements.searchResults) return;19- const eligible = [20- ...state.features21- .filter((pin) => !pin.filteredHidden)22- .map((pin) => ({ title: pin.location.title, pin })),23- ...[...state.zoneRecords.values()]24- .filter((record) => record.zone.title &&25- !isCollectionHidden(record.zone.collectionId) &&26- (!state.search || record.zone.title.toLocaleLowerCase().includes(state.search)))27- .map((record) => ({ title: record.zone.title, record })),28- ].sort((a, b) => a.title.localeCompare(b.title));20+ const eligible = listableFeatures();2921 const shown = eligible.slice(0, dockListLimit);3022 const fragment = document.createDocumentFragment();3123 if (!shown.length) {
@@ -33,7 +25,9 @@ export function renderSearchResults() {
3325 empty.className = "search-empty";3426 empty.textContent = state.search3527 ? "No visible features match."36- : "Nothing is enabled. Show a collection to list its features.";28+ : state.highlightedZones.size29+ ? "Nothing stands inside the highlighted ground."30+ : "Nothing is shown. Bring a collection back to list its features.";3731 fragment.append(empty);3832 }3933 for (const entry of shown) {
@@ -110,13 +104,18 @@ export function updateDockRail() {
110104 elements.dockRailName.hidden = !shown;111105 }112106 107+// The header counts the list under it and says why it is as short as it is:108+// the search in the reader's own words, or the flag for the filters they set109+// on the other side of the map. Every filter that can move the number raises110+// the flag -- a collection put away, ground highlighted, a cell holding the111+// view -- so a count that drops always has its reason on screen beside it.113112 function updateDockReadout(count) {114113 elements.dockCount.textContent =115114 `${formatNumber(count)} ${count === 1 ? "feature" : "features"}`;116115 if (state.search) {117116 elements.dockFlag.textContent = `“${state.search}”`;118117 elements.dockFlag.hidden = false;119- } else if (state.hiddenCollections.size > 0) {118+ } else if (filtersApply()) {120119 elements.dockFlag.textContent = "filtered";121120 elements.dockFlag.hidden = false;122121 } else {
A frontend/src/visibility.js +56 -0
@@ -0,0 +1,56 @@
1+// One answer to what the reader is looking at.2+//3+// Three surfaces used to work this out for themselves. The canvas drew what4+// survived every filter; the footer counted what the legend allowed; the dock5+// listed what the search left. So highlighting one district could cull sixty6+// features off the map while the panel beside it went on offering all sixty,7+// and the two numbers on screen disagreed by a factor of four with nothing to8+// say which was the map's. Every surface asks here now, and a filter that9+// lands on one of them lands on all of them.10+11+import { isCollectionHidden } from "./collections.js";12+import { pinIsHidden } from "./features.js";13+import { state } from "./state.js";14+15+// Every point feature the chart is drawing: its collection shown, the search16+// satisfied, and standing inside whatever ground or cell is holding the view.17+export function visiblePins() {18+ return state.features.filter((pin) => !pinIsHidden(pin));19+}20+21+// Every shape feature the chart is drawing: its collection shown, and on the22+// lens's own shard, which is all the registry holds. Highlighting narrows23+// which points stand rather than which ground is drawn, so it asks nothing24+// here.25+export function visibleZones() {26+ return [...state.zoneRecords.values()]27+ .filter((record) => !isCollectionHidden(record.zone.collectionId));28+}29+30+// What the map is drawing, counted: features of every kind, one number.31+export function drawnFeatureCount() {32+ return visiblePins().length + visibleZones().length;33+}34+35+// The part of it a list can name, alphabetically. Shape features answer the36+// search here rather than on the canvas -- searching for a place has never37+// taken the ground out from under it -- and one the archive left untitled is38+// on the map without being in any index of it.39+export function listableFeatures() {40+ return [41+ ...visiblePins().map((pin) => ({ title: pin.location.title, pin })),42+ ...visibleZones()43+ .filter((record) => record.zone.title &&44+ (!state.search || record.zone.title.toLocaleLowerCase().includes(state.search)))45+ .map((record) => ({ title: record.zone.title, record })),46+ ].sort((a, b) => a.title.localeCompare(b.title));47+}48+49+// Whether anything but the reader's own search is narrowing the list: a50+// collection put away, ground highlighted, a cell holding the view. The dock51+// flags itself with this, so the count dropping always has a reason on screen52+// beside it.53+export function filtersApply() {54+ return state.hiddenCollections.size > 0 || state.highlightedZones.size > 0 ||55+ Boolean(state.gridEnabled && state.gridCell);56+}