diff --git a/web/wall.html b/web/wall.html
index 3202312..423bb0a 100644
--- a/web/wall.html
+++ b/web/wall.html
@@ -211,6 +211,7 @@
}
/* the popup list is painted by the OS — give it a real ground, not the
panel's translucent one, or it inherits the wall behind the field */
+ .f-row select:disabled { opacity: .45; }
.f-row select { text-overflow: ellipsis; } /* long author names on a narrow phone */
.f-row option { background: #15151a; color: var(--f-text); }
.f-row input::placeholder { color: var(--f-faint); }
@@ -589,8 +590,16 @@
-
-
+
+
+
+
+
+
+
+
+
+
@@ -1483,9 +1492,38 @@ function renderSubjectTree(el, meta) {
}
}
+// estado / cidade / tópico facets of one piece, as sets of plain keys:
+// _st "S" _ct "S|C" _tp "S|C|T" · "S||T" · "||T" (a topic is findable at
+// any level of the cascade; a theme with no place only at the top one)
+const CATALOGUE_TAG = /^cole[cç][aã]o\b|^(?:externa|interna|diurna|noturna|horizontal|vertical)$/i;
+function subjectFacets(meta) {
+ const st = new Set(), ct = new Set(), tp = new Set();
+ const { root, others } = subjectTree(meta);
+ const brasil = root.kids.get('Brasil');
+ const topics = (node, path, add) => node.kids.forEach((kid, name) => {
+ const p = path.concat(name);
+ add(p.join(' - '));
+ topics(kid, p, add);
+ });
+ if (brasil) brasil.kids.forEach((stNode, sName) => {
+ st.add(sName);
+ stNode.kids.forEach((kid, name) => {
+ const addTopic = t => { tp.add(`${sName}||${t}`); tp.add(`||${t}`); };
+ if (kid.place) {
+ ct.add(`${sName}|${name}`);
+ topics(kid, [], t => { tp.add(`${sName}|${name}|${t}`); addTopic(t); });
+ } else {
+ addTopic(name); topics(kid, [name], addTopic);
+ }
+ });
+ });
+ others.filter(t => deburr(t) !== 'brasil' && !CATALOGUE_TAG.test(t.trim())).forEach(t => tp.add(`||${t}`));
+ return { st, ct, tp };
+}
+
function setupFilters(mapData) {
const entries = Object.values(mapData.map);
- const instCounts = new Map(), typeCounts = new Map(), authorGroups = new Map(), subjectGroups = new Map();
+ const instCounts = new Map(), typeCounts = new Map(), authorGroups = new Map();
let minYear = Infinity, maxYear = -Infinity;
entries.forEach(e => {
@@ -1493,9 +1531,6 @@ function setupFilters(mapData) {
if (meta.community) instCounts.set(meta.community, (instCounts.get(meta.community) || 0) + 1);
const t = cleanType(meta.type);
if (t) typeCounts.set(t, (typeCounts.get(t) || 0) + 1);
- subjectsOf(meta).map(subjectKey).forEach(k => {
- subjectGroups.set(k, (subjectGroups.get(k) || 0) + 1);
- });
const a = cleanAuthor(meta.contributor);
if (a) {
const k = authorKey(a), g = authorGroups.get(k) || { n: 0, forms: new Map() };
@@ -1533,18 +1568,6 @@ function setupFilters(mapData) {
selAuthor.appendChild(opt);
});
- const subjectNames = new Map(); // key → a readable spelling
- entries.forEach(e => subjectsOf(e.meta).forEach(t => {
- const k = subjectKey(t);
- if (k && subjectGroups.get(k) >= SUBJECT_MIN && !subjectNames.has(k)) subjectNames.set(k, String(t).trim());
- }));
- const selSubject = document.getElementById('f-subject');
- [...subjectGroups.entries()].filter(([, n]) => n >= SUBJECT_MIN).sort((a, b) => b[1] - a[1]).forEach(([k, n]) => {
- const opt = document.createElement('option');
- opt.value = k; opt.textContent = `${subjectNames.get(k)} (${n.toLocaleString('pt-BR')})`;
- selSubject.appendChild(opt);
- });
-
const decadeMin = Math.floor(minYear / 10) * 10;
const decadeMax = Math.floor(maxYear / 10) * 10;
const nDecades = (decadeMax - decadeMin) / 10 + 1;
@@ -1576,7 +1599,7 @@ function setupFilters(mapData) {
const a = cleanAuthor(meta.contributor);
m.userData._author = a ? authorKey(a) : '';
m.userData._type = cleanType(meta.type);
- m.userData._subjects = new Set(subjectsOf(meta).map(subjectKey));
+ Object.assign(m.userData, (({ st, ct, tp }) => ({ _st: st, _ct: ct, _tp: tp }))(subjectFacets(meta)));
// one haystack per piece: every metadata field, not a chosen few
// the handle is the map's key, not a field of meta — it still has to be
// searchable (people paste a handle or its number straight into the box)
@@ -1601,7 +1624,43 @@ function setupFilters(mapData) {
bars.push(bar);
}
- const state = { search: '', institution: '', yearLo: decadeMin, yearHi: decadeMax, yearTouched: false, author: '', type: '', subject: '' };
+ const selEstado = document.getElementById('f-estado');
+ const selCidade = document.getElementById('f-cidade');
+ const selTopico = document.getElementById('f-topico');
+ const fillSelect = (sel, rows, first) => {
+ sel.replaceChildren(Object.assign(document.createElement('option'), { value: '', textContent: first }));
+ rows.forEach(([value, n]) => {
+ const opt = document.createElement('option');
+ opt.value = value; opt.textContent = `${value} (${n.toLocaleString('pt-BR')})`;
+ sel.appendChild(opt);
+ });
+ };
+ const tally = (prefix, keyOf, min) => {
+ const c = new Map();
+ tileMeshes.forEach(m => keyOf(m.userData).forEach(k => {
+ if (k.startsWith(prefix)) { const v = k.slice(prefix.length); c.set(v, (c.get(v) || 0) + 1); }
+ }));
+ return [...c].filter(([, n]) => n >= min).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'pt'));
+ };
+ // topics under the current place: the whole catalogue is thousands, so the top
+ // level lists only recurring ones; inside a state or city every topic is shown
+ function topicPrefix() { return state.estado ? (state.cidade ? `${state.estado}|${state.cidade}|` : `${state.estado}||`) : '||'; }
+ function rebuildPlace() {
+ fillSelect(selCidade, state.estado ? tally(`${state.estado}|`, d => d._ct, 1) : [], state.estado ? 'todas' : 'escolha um estado');
+ selCidade.disabled = !state.estado;
+ selCidade.value = state.cidade;
+ fillSelect(selTopico, tally(topicPrefix(), d => d._tp, state.estado ? 1 : SUBJECT_MIN), 'todos');
+ selTopico.value = state.topico;
+ }
+ // changing a level empties the ones below it, whose lists just changed
+ function resetPlace(level, rebuild) {
+ if (level === 'estado') { if (!rebuild) { state.estado = ''; selEstado.value = ''; } state.cidade = ''; }
+ if (level === 'cidade' && !rebuild) { state.cidade = ''; selCidade.value = ''; }
+ state.topico = '';
+ rebuildPlace();
+ }
+
+ const state = { search: '', institution: '', yearLo: decadeMin, yearHi: decadeMax, yearTouched: false, author: '', type: '', estado: '', cidade: '', topico: '' };
function resetYear() {
state.yearTouched = false;
@@ -1623,7 +1682,9 @@ function setupFilters(mapData) {
(!state.institution || meta.community === state.institution) &&
(!state.type || d._type === state.type) &&
(!state.author || d._author === state.author) &&
- (!state.subject || d._subjects.has(state.subject)) &&
+ (!state.estado || d._st.has(state.estado)) &&
+ (!state.cidade || d._ct.has(`${state.estado}|${state.cidade}`)) &&
+ (!state.topico || d._tp.has(topicPrefix() + state.topico)) &&
terms.every(t => d._hay.includes(t));
if (okRest && d._year != null) {
const i = Math.floor((d._year - decadeMin) / 10);
@@ -1684,7 +1745,9 @@ function setupFilters(mapData) {
if (state.institution) out.push({ key: 'institution', text: state.institution });
if (state.yearTouched) out.push({ key: 'year', text: yearLabel(state.yearLo, state.yearHi) });
if (state.author) out.push({ key: 'author', text: authorNames.get(state.author) });
- if (state.subject) out.push({ key: 'subject', text: subjectNames.get(state.subject) });
+ if (state.estado) out.push({ key: 'estado', text: state.estado });
+ if (state.cidade) out.push({ key: 'cidade', text: state.cidade });
+ if (state.topico) out.push({ key: 'topico', text: state.topico });
if (state.type) out.push({ key: 'type', text: state.type });
return out;
}
@@ -1693,7 +1756,9 @@ function setupFilters(mapData) {
if (key === 'search') { state.search = ''; searchInput.value = ''; }
if (key === 'institution') { state.institution = ''; selInst.value = ''; }
if (key === 'type') { state.type = ''; selType.value = ''; }
- if (key === 'subject') { state.subject = ''; selSubject.value = ''; }
+ if (key === 'estado') resetPlace('estado');
+ if (key === 'cidade') resetPlace('cidade');
+ if (key === 'topico') { state.topico = ''; selTopico.value = ''; }
if (key === 'author') { state.author = ''; selAuthor.value = ''; }
if (key === 'year') resetYear();
apply();
@@ -1735,7 +1800,9 @@ function setupFilters(mapData) {
});
searchX.addEventListener('click', () => { clearTimeout(searchTimer); clearOne('search'); searchInput.focus(); });
- selSubject.addEventListener('change', () => { state.subject = selSubject.value; apply(); });
+ selEstado.addEventListener('change', () => { state.estado = selEstado.value; resetPlace('estado', true); apply(); });
+ selCidade.addEventListener('change', () => { state.cidade = selCidade.value; resetPlace('cidade', true); apply(); });
+ selTopico.addEventListener('change', () => { state.topico = selTopico.value; apply(); });
selAuthor.addEventListener('change', () => { state.author = selAuthor.value; apply(); });
function onYear(e) {
@@ -1755,12 +1822,14 @@ function setupFilters(mapData) {
clearBtn.addEventListener('click', () => {
clearTimeout(searchTimer);
- state.search = ''; state.institution = ''; state.type = ''; state.author = ''; state.subject = '';
- searchInput.value = ''; selInst.value = ''; selType.value = ''; selAuthor.value = ''; selSubject.value = '';
+ state.search = ''; state.institution = ''; state.type = ''; state.author = ''; state.estado = state.cidade = state.topico = '';
+ searchInput.value = ''; selInst.value = ''; selType.value = ''; selAuthor.value = ''; selEstado.value = ''; rebuildPlace();
resetYear();
apply();
});
+ fillSelect(selEstado, tally('', d => d._st, 1), 'todos');
+ rebuildPlace();
apply();
toggleFilters(!isMobile); // on a phone the wall matters more than the panel
}