(async () => {
// === Curie "Locate Site" scraper → CSV: year,Facility,Assemblies,Tons (stops at 2082) ===
// - Facility from dropdown selection text
// - Assemblies from:
Number of Assemblies: N (missing -> 0)
// - Tons from: Metric Tons of Heavy Metal (MTHM): N (missing -> 0)
// - Debounced popup read (waits for popup content change to avoid offset)
// - Dedupes per (year|Facility)
// - Steps year-by-year to 2082 and saves Curie_Spent_Fuel_Timeline.csv
const STOP_YEAR = 2082;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const inViewport = el => {
if (!el || !(el instanceof Element)) return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
// --- Year readout ---
const getCurrentYear = () => {
const el = document.querySelector('#slider-current-year.slider-display');
if (!el) return null;
const t = (el.textContent || '').trim();
const m = t.match(/\b(18|19|20)\d{2}\b/);
return m ? parseInt(m[0], 10) : null;
};
// --- Timeline arrows ---
const clickableSel = [
'button', '[role="button"]', 'a',
'.btn', '.button', '.MuiIconButton-root', '.v-btn',
'.mdc-icon-button', '.ant-btn', '.el-button',
'.mat-icon-button', '.mat-button', '.bp3-button',
'[onclick]', '[ng-click]', '[data-action]', '[data-testid]', '[data-cy]', '[data-qa]'
].join(',');
const findRightBtn = () => {
const icon = Array.from(document.querySelectorAll('i.fa.fa-arrow-right, i.fa-arrow-right, i.fas.fa-arrow-right'))
.find(inViewport);
return icon ? (icon.closest(clickableSel) || icon) : null;
};
const findLeftBtn = () => {
const icon = Array.from(document.querySelectorAll('i.fa.fa-arrow-left, i.fa-arrow-left, i.fas.fa-arrow-left'))
.find(inViewport);
return icon ? (icon.closest(clickableSel) || icon) : null;
};
const waitForYearChangeFrom = async (prevYear, timeoutMs = 4000, pollMs = 120) => {
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const y = getCurrentYear();
if (Number.isInteger(y) && y !== prevYear) return y;
await sleep(pollMs);
}
return getCurrentYear();
};
const stepRightOneYear = async (rightBtn, leftBtn, prevYear) => {
if (!rightBtn) return null;
rightBtn.click();
let y = await waitForYearChangeFrom(prevYear);
if (!Number.isInteger(y) || y === prevYear) {
await sleep(250);
rightBtn.click();
y = await waitForYearChangeFrom(prevYear);
}
// Overshoot correction (> +1)
if (Number.isInteger(y) && y > prevYear + 1 && leftBtn) {
let guard = 0;
while (y > prevYear + 1 && guard < 10) {
leftBtn.click();
const prev = y;
y = await waitForYearChangeFrom(prev, 2500, 120);
if (!Number.isInteger(y)) y = prev - 1;
guard++;
}
if (y > prevYear + 1) y = prevYear + 1;
}
if (!Number.isInteger(y) || y === prevYear) return null;
return y;
};
// --- Popup helpers & parsing (DOM-based li extraction) ---
const popupEl = () => document.querySelector('.leaflet-popup-content');
const popupSignature = () => {
const el = popupEl();
return el ? (el.innerHTML || '').trim() : '';
};
const waitForPopup = async (timeoutMs = 3500, pollMs = 90) => {
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const el = popupEl();
if (el && el.offsetParent !== null) return el;
await sleep(pollMs);
}
return null;
};
const waitForPopupContentChange = async (prevSig, timeoutMs = 4000, pollMs = 90) => {
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const sig = popupSignature();
if (sig && sig !== prevSig) return sig;
await sleep(pollMs);
}
return popupSignature(); // may be unchanged
};
const closePopupIfOpen = () => {
const btn = document.querySelector('.leaflet-popup-close-button');
if (btn) btn.click();
};
// Extract Assemblies and Tons from elements in the popup
const extractFromPopup = (popup) => {
let Assemblies = 0;
let Tons = 0;
if (!popup) return { Assemblies, Tons };
const lis = Array.from(popup.querySelectorAll('li'));
for (const li of lis) {
const t = (li.textContent || '').trim();
if (/^number\s+of\s+assemblies\s*:/i.test(t)) {
const after = t.split(':').slice(1).join(':');
const m = (after || '').replace(/,/g, '').match(/\d+/);
Assemblies = m ? parseInt(m[0], 10) : 0;
}
if (/^metric\s+tons\s+of\s+heavy\s+metal\s*\(mthm\)\s*:/i.test(t)) {
const after = t.split(':').slice(1).join(':');
const m = (after || '').replace(/,/g, '').match(/-?\d+(?:\.\d+)?/);
Tons = m ? parseFloat(m[0]) : 0;
}
}
return {
Assemblies: Number.isFinite(Assemblies) ? Assemblies : 0,
Tons: Number.isFinite(Tons) ? Tons : 0
};
};
// --- Locate Site control discovery (native