clone

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

feat: gather text categories into a section of their own

Dami committed b4a126e
Categories drawn as text are labels for the ground itself -- Area, Region,
Province -- and they were listed wherever the archive happened to put them,
which on RuneScape means Area sits between Altar and Bank. They are read and
edited as one set, and they are not markers, so they now have their own
section above the pin groups and under the zones: labels and boundaries both
say where you are, rather than what is worth going to.

The legend is drawn from sections rather than straight from the archive's
groups, and the switches, the counts, the fold state and Show only all follow
the section a category is actually in. A group whose categories were all text
no longer renders an empty heading, and the group counts stop including the
labels that were never drawn with them.
commit
b4a126e15d7ba398e38ed3f39ab00d84b6f2f929
parent
43d5ab4d97f2cca50fb0f350cb80c0e45452d36d
author
Dami <dami.etoile@gmail.com>
1 file changed, +73 -46
Mfrontend/app.js119++++++++++++++++++------------
M frontend/app.js +73 -46
@@ -388,8 +388,8 @@ function bindUIEvents() {
388388 389389 elements.legend.addEventListener("change", (event) => {390390 const input = event.target;391- if (input.dataset.group) {392- toggleGroup(Number(input.dataset.group));391+ if (input.dataset.sectionToggle) {392+ toggleSection(input.dataset.sectionToggle);393393 return;394394 }395395 if (!input.dataset.category) return;
@@ -397,7 +397,7 @@ function bindUIEvents() {
397397 if (input.checked) state.hiddenCategories.delete(categoryID);398398 else state.hiddenCategories.add(categoryID);399399 applyPinFilters();400- syncGroupSwitches();400+ syncSectionSwitches();401401 });402402 403403 // Which row the pointer is over is worked out from where the pointer is,
@@ -427,7 +427,7 @@ function bindUIEvents() {
427427 elements.layers.addEventListener("scroll", markHoveredRow, { passive: true });428428 429429 elements.layers.addEventListener("click", (event) => {430- const only = event.target.closest("[data-only-category], [data-only-group]");430+ const only = event.target.closest("[data-only-category], [data-only-section]");431431 if (only) {432432 // The button sits inside the row's label, whose default action would433433 // otherwise toggle the very checkbox this is meant to override.
@@ -435,7 +435,7 @@ function bindUIEvents() {
435435 event.stopPropagation();436436 showOnly(only.dataset.onlyCategory437437 ? { category: Number(only.dataset.onlyCategory) }438- : { group: Number(only.dataset.onlyGroup) });438+ : { section: only.dataset.onlySection });439439 return;440440 }441441 const button = event.target.closest("[data-section]");
@@ -595,7 +595,13 @@ async function loadMap(entry) {
595595 ]);596596 const categories = detail.groups.flatMap((group) => group.categories);597597 unpackLocations(packed, categories);598- return { ...entry, variants: detail.variants, groups: detail.groups, zones: detail.zones || [] };598+ return {599+ ...entry,600+ variants: detail.variants,601+ groups: detail.groups,602+ zones: detail.zones || [],603+ sections: legendSections(detail.groups),604+ };599605 }600606 601607 // The reader of packLocations. Each field is a view straight onto the buffer,
@@ -1246,29 +1252,50 @@ function populateSelect(select, items, labelKey, valueKey) {
12461252 });12471253 }12481254 1255+// Categories drawn as text are labels for the ground itself -- Area, Region,1256+// Province -- and are read and edited as one set. Gathered here rather than1257+// left in place, where "Area" sits between Altar and Bank and reads like1258+// another kind of marker.1259+function legendSections(groups) {1260+ const sections = [];1261+ const text = [];1262+ for (const group of groups) {1263+ const drawn = [];1264+ for (const category of group.categories) {1265+ (category.displayType === "text" ? text : drawn).push(category);1266+ }1267+ if (drawn.length) {1268+ sections.push({ key: `group-${group.id}`, title: group.title, categories: drawn });1269+ }1270+ }1271+ // Above the pin groups, under the zones: labels and boundaries both say where1272+ // you are, rather than what is worth going to.1273+ if (text.length) sections.unshift({ key: "text", title: "Text", categories: text });1274+ return sections;1275+}1276+12491277 function renderLegend() {12501278 const fragment = document.createDocumentFragment();1251- for (const group of state.map.groups) {1252- if (!group.categories.length) continue;1253- const section = document.createElement("section");1254- section.className = "layer-section";1255- section.dataset.groupSection = String(group.id);1256- const locations = group.categories.reduce((total, category) => total + category.locations.length, 0);1257- section.append(layerHeader(`group-${group.id}`, group.title, locations, group.id));1279+ for (const section of state.map.sections) {1280+ const element = document.createElement("section");1281+ element.className = "layer-section";1282+ element.dataset.layerSection = section.key;1283+ const locations = section.categories.reduce((total, category) => total + category.locations.length, 0);1284+ element.append(layerHeader(section.key, section.title, locations));12581285 const toggles = document.createElement("div");12591286 toggles.className = "category-toggles";1260- for (const category of group.categories) toggles.append(categoryToggle(category));1261- section.append(toggles);1262- fragment.append(section);1287+ for (const category of section.categories) toggles.append(categoryToggle(category));1288+ element.append(toggles);1289+ fragment.append(element);12631290 }12641291 elements.legend.replaceChildren(fragment);12651292 syncSectionCollapse();1266- syncGroupSwitches();1293+ syncSectionSwitches();12671294 }12681295 12691296 // Mirrors the markup of the static zones header so every layer section reads the12701297 // same: disclosure on the left, one switch on the right.1271-function layerHeader(key, title, count, groupID) {1298+function layerHeader(key, title, count) {12721299 const header = document.createElement("div");12731300 header.className = "layer-header";12741301
@@ -1288,13 +1315,13 @@ function layerHeader(key, title, count, groupID) {
12881315 disclosure.append(chevron, name, total);12891316 12901317 const only = onlyButton(`Show only ${title}`);1291- only.dataset.onlyGroup = String(groupID);1318+ only.dataset.onlySection = key;12921319 12931320 const toggle = document.createElement("label");12941321 toggle.className = "layer-switch";12951322 const checkbox = document.createElement("input");12961323 checkbox.type = "checkbox";1297- checkbox.dataset.group = String(groupID);1324+ checkbox.dataset.sectionToggle = key;12981325 checkbox.setAttribute("aria-label", `Show ${title}`);12991326 const knob = document.createElement("span");13001327 knob.setAttribute("aria-hidden", "true");
@@ -1712,7 +1739,7 @@ function setAllCategories(visible) {
17121739 syncLegendCheckboxes();17131740 applyPinFilters();17141741 renderSearchResults();1715- syncGroupSwitches();1742+ syncSectionSwitches();17161743 }17171744 17181745 function syncLegendCheckboxes() {
@@ -1721,17 +1748,17 @@ function syncLegendCheckboxes() {
17211748 }17221749 }17231750 1724-function toggleGroup(groupID) {1725- const group = state.map.groups.find((item) => item.id === groupID);1726- if (!group) return;1727- const hasVisible = group.categories.some((category) => !state.hiddenCategories.has(category.id));1728- for (const category of group.categories) {1751+function toggleSection(key) {1752+ const section = state.map.sections.find((item) => item.key === key);1753+ if (!section) return;1754+ const hasVisible = section.categories.some((category) => !state.hiddenCategories.has(category.id));1755+ for (const category of section.categories) {17291756 if (hasVisible) state.hiddenCategories.add(category.id);17301757 else state.hiddenCategories.delete(category.id);17311758 }17321759 syncLegendCheckboxes();17331760 applyPinFilters();1734- syncGroupSwitches();1761+ syncSectionSwitches();17351762 }17361763 17371764 // Isolating is the common request of a long legend: "just the Korok Seeds",
@@ -1746,9 +1773,9 @@ function showOnly(target) {
17461773 return;17471774 }17481775 state.hiddenCategories.clear();1749- for (const group of state.map.groups) {1750- const wanted = target.group === group.id;1751- for (const category of group.categories) {1776+ for (const section of state.map.sections) {1777+ const wanted = target.section === section.key;1778+ for (const category of section.categories) {17521779 if (!wanted && target.category !== category.id) {17531780 state.hiddenCategories.add(category.id);17541781 }
@@ -1757,14 +1784,14 @@ function showOnly(target) {
17571784 syncLegendCheckboxes();17581785 applyPinFilters();17591786 renderSearchResults();1760- syncGroupSwitches();1787+ syncSectionSwitches();17611788 }17621789 17631790 // True when what is on screen is already exactly what this target would isolate.17641791 function isOnly(target) {1765- for (const group of state.map.groups) {1766- for (const category of group.categories) {1767- const wanted = target.group === group.id || target.category === category.id;1792+ for (const section of state.map.sections) {1793+ for (const category of section.categories) {1794+ const wanted = target.section === section.key || target.category === category.id;17681795 if (wanted === state.hiddenCategories.has(category.id)) return false;17691796 }17701797 }
@@ -1781,35 +1808,35 @@ function updateSoloChip() {
17811808 }17821809 let onlyVisible = null;17831810 let visibleCount = 0;1784- let soleGroup = null;1785- let groupsShowing = 0;1786- for (const group of state.map.groups) {1811+ let soleSection = null;1812+ let sectionsShowing = 0;1813+ for (const section of state.map.sections) {17871814 let shown = 0;1788- for (const category of group.categories) {1815+ for (const category of section.categories) {17891816 if (state.hiddenCategories.has(category.id)) continue;17901817 shown++;17911818 visibleCount++;17921819 onlyVisible = category;17931820 }17941821 if (shown > 0) {1795- groupsShowing++;1796- soleGroup = shown === group.categories.length ? group : null;1822+ sectionsShowing++;1823+ soleSection = shown === section.categories.length ? section : null;17971824 }17981825 }17991826 let label = "";18001827 if (visibleCount === 1 && onlyVisible) label = onlyVisible.title;1801- else if (groupsShowing === 1 && soleGroup) label = soleGroup.title;1828+ else if (sectionsShowing === 1 && soleSection) label = soleSection.title;18021829 chip.hidden = !label;18031830 chip.textContent = label ? `only: ${label}` : "";18041831 chip.title = label ? `Showing only ${label} — click to show everything` : "";18051832 }18061833 1807-function syncGroupSwitches() {1834+function syncSectionSwitches() {18081835 if (!state.map) return;1809- for (const group of state.map.groups) {1810- const input = elements.legend.querySelector(`input[data-group="${group.id}"]`);1836+ for (const section of state.map.sections) {1837+ const input = elements.legend.querySelector(`input[data-section-toggle="${section.key}"]`);18111838 if (input) {1812- input.checked = group.categories.some((category) => !state.hiddenCategories.has(category.id));1839+ input.checked = section.categories.some((category) => !state.hiddenCategories.has(category.id));18131840 }18141841 }18151842 updateSoloChip();
@@ -1823,7 +1850,7 @@ function revealPin(pin) {
18231850 elements.searchResults.hidden = true;18241851 syncLegendCheckboxes();18251852 applyPinFilters();1826- syncGroupSwitches();1853+ syncSectionSwitches();18271854 showPin(pin, true);18281855 }18291856