Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f34f158ac | ||
|
|
42d56ba092 | ||
|
|
aedef870c1 | ||
|
|
9e7a885bdd | ||
|
|
278aab5094 | ||
|
|
19cb7aa185 | ||
|
|
edaf776dec |
+258
-35
@@ -211,6 +211,8 @@
|
|||||||
}
|
}
|
||||||
/* the popup list is painted by the OS — give it a real ground, not the
|
/* 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 */
|
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 option { background: #15151a; color: var(--f-text); }
|
||||||
.f-row input::placeholder { color: var(--f-faint); }
|
.f-row input::placeholder { color: var(--f-faint); }
|
||||||
.f-row input[type="search"]::-webkit-search-decoration,
|
.f-row input[type="search"]::-webkit-search-decoration,
|
||||||
@@ -377,6 +379,20 @@
|
|||||||
#card-title { font-size: 1.02rem; color: #f5f5f0; }
|
#card-title { font-size: 1.02rem; color: #f5f5f0; }
|
||||||
#card-date { font-size: .85rem; color: #b9b9b0; margin-top: 2px; }
|
#card-date { font-size: .85rem; color: #b9b9b0; margin-top: 2px; }
|
||||||
#card-contrib { font-size: .82rem; color: #cdd8e0; margin-top: 10px; }
|
#card-contrib { font-size: .82rem; color: #cdd8e0; margin-top: 10px; }
|
||||||
|
#card-tags { margin-top: 12px; font-family: system-ui, sans-serif; font-size: .7rem; line-height: 1.55; color: #b8c6d1; }
|
||||||
|
#card-tags:empty { display: none; }
|
||||||
|
#card-tags .tn { margin: 0; padding: 0 0 0 13px; border-left: 1px solid rgba(127,168,201,.28); }
|
||||||
|
#card-tags > .tn { padding-left: 0; border-left: 0; }
|
||||||
|
#card-tags .tl { padding: 1px 0; overflow-wrap: anywhere; }
|
||||||
|
#card-tags .tn.place > .tl { color: #dcebf6; font-weight: 600; }
|
||||||
|
#card-tags .tn.topic > .tl { color: #b8c6d1; }
|
||||||
|
#card-tags .tn > .tl::before { content: '└ '; color: rgba(127,168,201,.5); }
|
||||||
|
#card-tags > .tn > .tl::before { content: ''; }
|
||||||
|
#card-tags .to { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
|
||||||
|
#card-tags .to span {
|
||||||
|
font-size: .64rem; line-height: 1.7; padding: 0 8px; border-radius: 999px;
|
||||||
|
background: rgba(127,168,201,.12); border: 1px solid rgba(127,168,201,.22);
|
||||||
|
}
|
||||||
#card-footer {
|
#card-footer {
|
||||||
margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.12);
|
margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.12);
|
||||||
font-family: system-ui, sans-serif; font-size: .68rem; letter-spacing: .04em; color: #888;
|
font-family: system-ui, sans-serif; font-size: .68rem; letter-spacing: .04em; color: #888;
|
||||||
@@ -571,11 +587,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="f-row">
|
<div class="f-row">
|
||||||
<label for="f-author">Autor / fotógrafo</label>
|
<label for="f-author">Autor / fotógrafo</label>
|
||||||
<div class="f-field">
|
<select id="f-author"><option value="">todos</option></select>
|
||||||
<input type="text" id="f-author" list="f-author-list" autocomplete="off" placeholder="buscar ou escolher…">
|
|
||||||
<button type="button" class="f-x" id="f-author-x" aria-label="limpar autor" hidden>×</button>
|
|
||||||
</div>
|
</div>
|
||||||
<datalist id="f-author-list"></datalist>
|
<div class="f-row">
|
||||||
|
<label for="f-estado">Estado</label>
|
||||||
|
<select id="f-estado"><option value="">todos</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="f-row">
|
||||||
|
<label for="f-cidade">Cidade</label>
|
||||||
|
<select id="f-cidade" disabled><option value="">escolha um estado</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="f-row">
|
||||||
|
<label for="f-topico">Tópico</label>
|
||||||
|
<select id="f-topico"><option value="">todos</option></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="f-row">
|
<div class="f-row">
|
||||||
<label for="f-type">Tipo de suporte</label>
|
<label for="f-type">Tipo de suporte</label>
|
||||||
@@ -599,6 +623,7 @@
|
|||||||
<div id="card-title">—</div>
|
<div id="card-title">—</div>
|
||||||
<div id="card-date">—</div>
|
<div id="card-date">—</div>
|
||||||
<div id="card-contrib"></div>
|
<div id="card-contrib"></div>
|
||||||
|
<div id="card-tags"></div>
|
||||||
<div id="card-footer">
|
<div id="card-footer">
|
||||||
<div><a id="card-link" href="#" target="_blank" rel="noopener">ver item original ↗</a> · <span id="card-handle"></span></div>
|
<div><a id="card-link" href="#" target="_blank" rel="noopener">ver item original ↗</a> · <span id="card-handle"></span></div>
|
||||||
<div id="card-rights"></div>
|
<div id="card-rights"></div>
|
||||||
@@ -1265,6 +1290,9 @@ function showCard(data) {
|
|||||||
document.getElementById('card-title').textContent = meta.title || '(sem título)';
|
document.getElementById('card-title').textContent = meta.title || '(sem título)';
|
||||||
document.getElementById('card-date').textContent = meta.date || '';
|
document.getElementById('card-date').textContent = meta.date || '';
|
||||||
document.getElementById('card-contrib').textContent = meta.contributor ? meta.contributor : '';
|
document.getElementById('card-contrib').textContent = meta.contributor ? meta.contributor : '';
|
||||||
|
// this piece's dc.subject, one chip each (accent/case duplicates folded)
|
||||||
|
const tagsEl = document.getElementById('card-tags');
|
||||||
|
renderSubjectTree(tagsEl, meta);
|
||||||
document.getElementById('card-handle').textContent = data.handle || '';
|
document.getElementById('card-handle').textContent = data.handle || '';
|
||||||
document.getElementById('card-link').href = data.handle ? sourceUrl(data.handle) : '#';
|
document.getElementById('card-link').href = data.handle ? sourceUrl(data.handle) : '#';
|
||||||
document.getElementById('card-rights').textContent = meta.rights ? `Direitos: ${meta.rights}` : '';
|
document.getElementById('card-rights').textContent = meta.rights ? `Direitos: ${meta.rights}` : '';
|
||||||
@@ -1348,16 +1376,167 @@ function yearOf(meta) {
|
|||||||
return m ? parseInt(m[1], 10) : null;
|
return m ? parseInt(m[1], 10) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── filter-value cleanup: the catalogue's type / author strings are messy ───
|
||||||
|
// (OCR debris, case variants, life dates glued on, "Nome Sobrenome" vs "Sobrenome, Nome").
|
||||||
|
// Only the filter lists and matching use these; the card still shows the raw record.
|
||||||
|
const KNOWN_TYPES = ['Fotografia/ Papel', 'Negativo/ Vidro', 'Diapositivo/ Vidro', 'Reprodução Fotomecânica/ Papel', 'Negativo flexível'];
|
||||||
|
function cleanType(raw) {
|
||||||
|
const s = String(raw || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (!s) return '';
|
||||||
|
const fold = deburr(s);
|
||||||
|
const known = KNOWN_TYPES.find(k => deburr(k) === fold);
|
||||||
|
if (known) return known;
|
||||||
|
// OCR debris: rebuild from the tail that survived ("…afia/ Papel", "…gativo/ Vidro")
|
||||||
|
if (/(?:rafia|FOa)\/ Papel$/.test(s)) return 'Fotografia/ Papel';
|
||||||
|
if (/gativo\/ Vidro$/.test(s)) return 'Negativo/ Vidro';
|
||||||
|
if (/Fotomec/.test(s)) return 'Reprodução Fotomecânica/ Papel';
|
||||||
|
if (/[<>{}\d]/.test(s)) return ''; // unrecoverable garbage — leave it out of the list
|
||||||
|
return /[A-ZÀ-Ý]{3,}/.test(s) ? s.charAt(0) + s.slice(1).toLowerCase() : s; // "NEGATIVO flexível" → "Negativo flexível"
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_AUTHOR = /^(s\/autor|s\/nome|s\/a|\[?s\.?n\.?\]?|sem autor|\[sem autor\]|autoria n[aã]o identificada|autor n[aã]o identificado|n[aã]o identificado|an[oô]nimo)$/i;
|
||||||
|
function cleanAuthor(raw) {
|
||||||
|
let s = String(raw || '').replace(/\s+/g, ' ').trim();
|
||||||
|
s = s.replace(/^Fot[óo]grafo:\s*/i, '')
|
||||||
|
.replace(/\s*[-–]?\s*\((?:autoria|fot[óo]grafo|firma)\)/gi, '')
|
||||||
|
.replace(/\s+-\s+(?:Fot[óo]grafo|Est[úu]dio\/Ag[eê]ncia)$/i, '');
|
||||||
|
// life dates / floruit glued to the end: ", 1864-1957", "(1893 - 1961)", "[? -1908]", "18- -19-"
|
||||||
|
const tail = /[,\s]*[\[(]?\s*(?:(?:ca\.|fl\.|m\.|n\.)\s*)?(?:\?|\d{2,4}-?)\s*(?:[-–]\s*(?:ca\.\s*)?\d{0,4})?\s*[\])]?[.,\s]*$/i;
|
||||||
|
for (let i = 0; i < 3 && tail.test(s) && s.replace(tail, '').length > 2; i++) s = s.replace(tail, '');
|
||||||
|
s = s.replace(/[.,\s]+$/, '').replace(/^\[(.*)\]$/, '$1').trim();
|
||||||
|
if (!s || NO_AUTHOR.test(s) || /[<>]|\d{3}/.test(s)) return '';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
// same person, different word order or accents → same key
|
||||||
|
const authorKey = name => deburr(name).replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter(Boolean).sort().join(' ');
|
||||||
|
|
||||||
|
// dc.subject folds accents and case; the select shows only tags that recur, since
|
||||||
|
// ~8000 distinct values (57% used once) would be an unusable list
|
||||||
|
const subjectKey = t => deburr(String(t)).replace(/\s+/g, ' ').trim();
|
||||||
|
const SUBJECT_MIN = 20;
|
||||||
|
|
||||||
|
// the catalogue repeats most subjects in English ("Railroads - Buildings and
|
||||||
|
// structures - Porto Velho (Rondônia, Brazil)" beside "Ferrovias - …"); keep the
|
||||||
|
// Portuguese side. An English-only record keeps what it has rather than none.
|
||||||
|
const EN_WORD = new RegExp('\\b(?:brazil|state|buildings?|structures|and|of|the|railroads?|rivers?|bridges|streets|church(?:es)?|photographs?|public|dwellings|portraits?|stations?|construction|region|waterfalls|plazas|mountains|women|history|life|hospitals?|views?|colon(?:y|ies)|slaves|panoramic|aerial|harbors?|ports?|forts?|islands?|beaches|houses|schools?|palaces?|squares?|paraguay|amazon|falls|lakes?|people|children|boats|ships|trains?|roads?|cities|city|towns?|landscapes?|factories|industry)\\b', 'i');
|
||||||
|
function subjectsOf(meta) {
|
||||||
|
const seen = new Set(), all = [];
|
||||||
|
[].concat((meta || {}).subject || []).forEach(t => {
|
||||||
|
const k = subjectKey(t);
|
||||||
|
if (k && !seen.has(k)) { seen.add(k); all.push(String(t).trim()); }
|
||||||
|
});
|
||||||
|
const pt = all.filter(t => !EN_WORD.test(t));
|
||||||
|
return pt.length ? pt : all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brasil › estado › cidade › tópico, derived from the tag text: "Tópico - Cidade (UF)"
|
||||||
|
// carries the whole path. Tags without a recognisable place stay out of the tree.
|
||||||
|
const UF = { AC: 'Acre', AL: 'Alagoas', AP: 'Amapá', AM: 'Amazonas', BA: 'Bahia', CE: 'Ceará', DF: 'Distrito Federal',
|
||||||
|
ES: 'Espírito Santo', GO: 'Goiás', MA: 'Maranhão', MT: 'Mato Grosso', MS: 'Mato Grosso do Sul', MG: 'Minas Gerais',
|
||||||
|
PA: 'Pará', PB: 'Paraíba', PR: 'Paraná', PE: 'Pernambuco', PI: 'Piauí', RJ: 'Rio de Janeiro', RN: 'Rio Grande do Norte',
|
||||||
|
RS: 'Rio Grande do Sul', RO: 'Rondônia', RR: 'Roraima', SC: 'Santa Catarina', SP: 'São Paulo', SE: 'Sergipe', TO: 'Tocantins' };
|
||||||
|
const STATE_BY_KEY = new Map(Object.values(UF).map(n => [deburr(n), n]));
|
||||||
|
|
||||||
|
function subjectTree(meta) {
|
||||||
|
const root = { kids: new Map() };
|
||||||
|
const put = path => path.reduce((n, label) => {
|
||||||
|
if (!n.kids.has(label)) n.kids.set(label, { kids: new Map(), place: false });
|
||||||
|
return n.kids.get(label);
|
||||||
|
}, root);
|
||||||
|
const others = [];
|
||||||
|
let hasPlace = false;
|
||||||
|
subjectsOf(meta).forEach(tag => {
|
||||||
|
const key = deburr(tag).replace(/\s*\(estado\)$/, '');
|
||||||
|
if (key === 'brasil') { put(['Brasil']).place = true; hasPlace = true; return; }
|
||||||
|
if (STATE_BY_KEY.has(key)) {
|
||||||
|
const st = STATE_BY_KEY.get(key);
|
||||||
|
put(['Brasil']).place = true; put(['Brasil', st]).place = true; hasPlace = true; return;
|
||||||
|
}
|
||||||
|
const parts = tag.split(/\s+-\s+/);
|
||||||
|
// "Espírito Santo (Estado) - Colônias": a state carrying a topic
|
||||||
|
const sm = parts[0].match(/^(.+?)\s*\(Estado\)$/i);
|
||||||
|
if (sm && STATE_BY_KEY.has(deburr(sm[1]))) {
|
||||||
|
const path = ['Brasil', STATE_BY_KEY.get(deburr(sm[1]))];
|
||||||
|
path.forEach((_, i) => { put(path.slice(0, i + 1)).place = true; });
|
||||||
|
parts.slice(1).forEach((_, i) => put(path.concat(parts.slice(1, i + 2))));
|
||||||
|
hasPlace = true; return;
|
||||||
|
}
|
||||||
|
const m = parts[parts.length - 1].match(/^(.+?)\s*\(([A-Z]{2})\)$/);
|
||||||
|
if (!m || !UF[m[2]]) { others.push(tag); return; }
|
||||||
|
const path = ['Brasil', UF[m[2]], m[1]];
|
||||||
|
path.slice(0, 3).forEach((_, i) => { put(path.slice(0, i + 1)).place = true; });
|
||||||
|
parts.slice(0, -1).forEach((_, i) => { put(path.concat(parts.slice(0, i + 1))); });
|
||||||
|
hasPlace = true;
|
||||||
|
});
|
||||||
|
// a lone "Brasil" is no tree — it reads better as a chip with the rest
|
||||||
|
if (hasPlace && root.kids.get('Brasil').kids.size === 0) { root.kids.clear(); others.push('Brasil'); }
|
||||||
|
return { root, others };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubjectTree(el, meta) {
|
||||||
|
el.replaceChildren();
|
||||||
|
const { root, others } = subjectTree(meta);
|
||||||
|
const draw = (node, label, into) => {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.className = 'tn ' + (node.place ? 'place' : 'topic');
|
||||||
|
const l = document.createElement('div'); l.className = 'tl'; l.textContent = label;
|
||||||
|
d.appendChild(l);
|
||||||
|
node.kids.forEach((kid, name) => draw(kid, name, d));
|
||||||
|
into.appendChild(d);
|
||||||
|
};
|
||||||
|
root.kids.forEach((kid, name) => draw(kid, name, el));
|
||||||
|
if (others.length) {
|
||||||
|
const o = document.createElement('div'); o.className = 'to';
|
||||||
|
others.forEach(t => { const c = document.createElement('span'); c.textContent = t; o.appendChild(c); });
|
||||||
|
el.appendChild(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
function setupFilters(mapData) {
|
||||||
const entries = Object.values(mapData.map);
|
const entries = Object.values(mapData.map);
|
||||||
const instCounts = new Map(), typeCounts = new Map(), authorCounts = new Map();
|
const instCounts = new Map(), typeCounts = new Map(), authorGroups = new Map();
|
||||||
let minYear = Infinity, maxYear = -Infinity;
|
let minYear = Infinity, maxYear = -Infinity;
|
||||||
|
|
||||||
entries.forEach(e => {
|
entries.forEach(e => {
|
||||||
const meta = e.meta || {};
|
const meta = e.meta || {};
|
||||||
if (meta.community) instCounts.set(meta.community, (instCounts.get(meta.community) || 0) + 1);
|
if (meta.community) instCounts.set(meta.community, (instCounts.get(meta.community) || 0) + 1);
|
||||||
if (meta.type) typeCounts.set(meta.type, (typeCounts.get(meta.type) || 0) + 1);
|
const t = cleanType(meta.type);
|
||||||
if (meta.contributor) authorCounts.set(meta.contributor, (authorCounts.get(meta.contributor) || 0) + 1);
|
if (t) typeCounts.set(t, (typeCounts.get(t) || 0) + 1);
|
||||||
|
const a = cleanAuthor(meta.contributor);
|
||||||
|
if (a) {
|
||||||
|
const k = authorKey(a), g = authorGroups.get(k) || { n: 0, forms: new Map() };
|
||||||
|
g.n++; g.forms.set(a, (g.forms.get(a) || 0) + 1);
|
||||||
|
authorGroups.set(k, g);
|
||||||
|
}
|
||||||
const y = yearOf(meta);
|
const y = yearOf(meta);
|
||||||
if (y != null) { if (y < minYear) minYear = y; if (y > maxYear) maxYear = y; }
|
if (y != null) { if (y < minYear) minYear = y; if (y > maxYear) maxYear = y; }
|
||||||
});
|
});
|
||||||
@@ -1377,11 +1556,16 @@ function setupFilters(mapData) {
|
|||||||
selType.appendChild(opt);
|
selType.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
const authorListEl = document.getElementById('f-author-list');
|
// one entry per person: the most common spelling names the group, and
|
||||||
[...authorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 300).forEach(([name]) => {
|
// "Sobrenome, Nome" wins a tie over "Nome Sobrenome"
|
||||||
|
const authorNames = new Map();
|
||||||
|
const selAuthor = document.getElementById('f-author');
|
||||||
|
[...authorGroups.entries()].sort((a, b) => b[1].n - a[1].n).forEach(([key, g]) => {
|
||||||
|
const name = [...g.forms.entries()].sort((a, b) => b[1] - a[1] || b[0].includes(',') - a[0].includes(','))[0][0];
|
||||||
|
authorNames.set(key, name);
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = name;
|
opt.value = key; opt.textContent = `${name} (${g.n.toLocaleString('pt-BR')})`;
|
||||||
authorListEl.appendChild(opt);
|
selAuthor.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
const decadeMin = Math.floor(minYear / 10) * 10;
|
const decadeMin = Math.floor(minYear / 10) * 10;
|
||||||
@@ -1400,8 +1584,6 @@ function setupFilters(mapData) {
|
|||||||
const yearReadout = document.getElementById('f-year-readout');
|
const yearReadout = document.getElementById('f-year-readout');
|
||||||
const yearFill = document.getElementById('f-year-fill');
|
const yearFill = document.getElementById('f-year-fill');
|
||||||
const histEl = document.getElementById('f-hist');
|
const histEl = document.getElementById('f-hist');
|
||||||
const authorInput = document.getElementById('f-author');
|
|
||||||
const authorX = document.getElementById('f-author-x');
|
|
||||||
const searchInput = document.getElementById('f-search');
|
const searchInput = document.getElementById('f-search');
|
||||||
const searchX = document.getElementById('f-search-x');
|
const searchX = document.getElementById('f-search-x');
|
||||||
const clearBtn = document.getElementById('f-clear');
|
const clearBtn = document.getElementById('f-clear');
|
||||||
@@ -1414,7 +1596,10 @@ function setupFilters(mapData) {
|
|||||||
tileMeshes.forEach(m => {
|
tileMeshes.forEach(m => {
|
||||||
const meta = m.userData.meta || {};
|
const meta = m.userData.meta || {};
|
||||||
m.userData._year = yearOf(meta);
|
m.userData._year = yearOf(meta);
|
||||||
m.userData._author = deburr(meta.contributor || '');
|
const a = cleanAuthor(meta.contributor);
|
||||||
|
m.userData._author = a ? authorKey(a) : '';
|
||||||
|
m.userData._type = cleanType(meta.type);
|
||||||
|
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
|
// 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
|
// 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)
|
// searchable (people paste a handle or its number straight into the box)
|
||||||
@@ -1439,7 +1624,43 @@ function setupFilters(mapData) {
|
|||||||
bars.push(bar);
|
bars.push(bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = { search: '', institution: '', yearLo: decadeMin, yearHi: decadeMax, yearTouched: false, author: '', type: '' };
|
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() {
|
function resetYear() {
|
||||||
state.yearTouched = false;
|
state.yearTouched = false;
|
||||||
@@ -1448,7 +1669,6 @@ function setupFilters(mapData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function apply() {
|
function apply() {
|
||||||
const q = deburr(state.author.trim());
|
|
||||||
// free text is AND across terms, so "rio 1900" narrows instead of widening
|
// free text is AND across terms, so "rio 1900" narrows instead of widening
|
||||||
const terms = deburr(state.search.trim()).split(/\s+/).filter(Boolean);
|
const terms = deburr(state.search.trim()).split(/\s+/).filter(Boolean);
|
||||||
const decCounts = new Array(nDecades).fill(0);
|
const decCounts = new Array(nDecades).fill(0);
|
||||||
@@ -1460,8 +1680,11 @@ function setupFilters(mapData) {
|
|||||||
// bars show what each decade *would* give under the other filters
|
// bars show what each decade *would* give under the other filters
|
||||||
const okRest =
|
const okRest =
|
||||||
(!state.institution || meta.community === state.institution) &&
|
(!state.institution || meta.community === state.institution) &&
|
||||||
(!state.type || meta.type === state.type) &&
|
(!state.type || d._type === state.type) &&
|
||||||
(!q || d._author.includes(q)) &&
|
(!state.author || d._author === state.author) &&
|
||||||
|
(!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));
|
terms.every(t => d._hay.includes(t));
|
||||||
if (okRest && d._year != null) {
|
if (okRest && d._year != null) {
|
||||||
const i = Math.floor((d._year - decadeMin) / 10);
|
const i = Math.floor((d._year - decadeMin) / 10);
|
||||||
@@ -1494,7 +1717,6 @@ function setupFilters(mapData) {
|
|||||||
const active = activeFilters();
|
const active = activeFilters();
|
||||||
renderChips(active);
|
renderChips(active);
|
||||||
searchX.hidden = !state.search;
|
searchX.hidden = !state.search;
|
||||||
authorX.hidden = !state.author;
|
|
||||||
clearBtn.classList.toggle('disabled', active.length === 0);
|
clearBtn.classList.toggle('disabled', active.length === 0);
|
||||||
|
|
||||||
countBadge.classList.toggle('empty', active.length === 0);
|
countBadge.classList.toggle('empty', active.length === 0);
|
||||||
@@ -1522,7 +1744,10 @@ function setupFilters(mapData) {
|
|||||||
if (state.search.trim()) out.push({ key: 'search', text: `“${state.search.trim()}”` });
|
if (state.search.trim()) out.push({ key: 'search', text: `“${state.search.trim()}”` });
|
||||||
if (state.institution) out.push({ key: 'institution', text: state.institution });
|
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.yearTouched) out.push({ key: 'year', text: yearLabel(state.yearLo, state.yearHi) });
|
||||||
if (state.author.trim()) out.push({ key: 'author', text: state.author.trim() });
|
if (state.author) out.push({ key: 'author', text: authorNames.get(state.author) });
|
||||||
|
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 });
|
if (state.type) out.push({ key: 'type', text: state.type });
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -1531,7 +1756,10 @@ function setupFilters(mapData) {
|
|||||||
if (key === 'search') { state.search = ''; searchInput.value = ''; }
|
if (key === 'search') { state.search = ''; searchInput.value = ''; }
|
||||||
if (key === 'institution') { state.institution = ''; selInst.value = ''; }
|
if (key === 'institution') { state.institution = ''; selInst.value = ''; }
|
||||||
if (key === 'type') { state.type = ''; selType.value = ''; }
|
if (key === 'type') { state.type = ''; selType.value = ''; }
|
||||||
if (key === 'author') { state.author = ''; authorInput.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();
|
if (key === 'year') resetYear();
|
||||||
apply();
|
apply();
|
||||||
}
|
}
|
||||||
@@ -1572,17 +1800,10 @@ function setupFilters(mapData) {
|
|||||||
});
|
});
|
||||||
searchX.addEventListener('click', () => { clearTimeout(searchTimer); clearOne('search'); searchInput.focus(); });
|
searchX.addEventListener('click', () => { clearTimeout(searchTimer); clearOne('search'); searchInput.focus(); });
|
||||||
|
|
||||||
let authorTimer = null;
|
selEstado.addEventListener('change', () => { state.estado = selEstado.value; resetPlace('estado', true); apply(); });
|
||||||
authorInput.addEventListener('input', () => {
|
selCidade.addEventListener('change', () => { state.cidade = selCidade.value; resetPlace('cidade', true); apply(); });
|
||||||
authorX.hidden = !authorInput.value;
|
selTopico.addEventListener('change', () => { state.topico = selTopico.value; apply(); });
|
||||||
clearTimeout(authorTimer);
|
selAuthor.addEventListener('change', () => { state.author = selAuthor.value; apply(); });
|
||||||
authorTimer = setTimeout(() => { state.author = authorInput.value; apply(); }, 160);
|
|
||||||
});
|
|
||||||
authorInput.addEventListener('change', () => { // picking from the datalist
|
|
||||||
clearTimeout(authorTimer);
|
|
||||||
state.author = authorInput.value; apply();
|
|
||||||
});
|
|
||||||
authorX.addEventListener('click', () => { clearTimeout(authorTimer); clearOne('author'); authorInput.focus(); });
|
|
||||||
|
|
||||||
function onYear(e) {
|
function onYear(e) {
|
||||||
let lo = parseInt(loSlider.value, 10), hi = parseInt(hiSlider.value, 10);
|
let lo = parseInt(loSlider.value, 10), hi = parseInt(hiSlider.value, 10);
|
||||||
@@ -1600,13 +1821,15 @@ function setupFilters(mapData) {
|
|||||||
hiSlider.addEventListener('input', onYear);
|
hiSlider.addEventListener('input', onYear);
|
||||||
|
|
||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', () => {
|
||||||
clearTimeout(authorTimer); clearTimeout(searchTimer);
|
clearTimeout(searchTimer);
|
||||||
state.search = ''; state.institution = ''; state.type = ''; state.author = '';
|
state.search = ''; state.institution = ''; state.type = ''; state.author = ''; state.estado = state.cidade = state.topico = '';
|
||||||
searchInput.value = ''; selInst.value = ''; selType.value = ''; authorInput.value = '';
|
searchInput.value = ''; selInst.value = ''; selType.value = ''; selAuthor.value = ''; selEstado.value = ''; rebuildPlace();
|
||||||
resetYear();
|
resetYear();
|
||||||
apply();
|
apply();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fillSelect(selEstado, tally('', d => d._st, 1), 'todos');
|
||||||
|
rebuildPlace();
|
||||||
apply();
|
apply();
|
||||||
toggleFilters(!isMobile); // on a phone the wall matters more than the panel
|
toggleFilters(!isMobile); // on a phone the wall matters more than the panel
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user