Added files to scrape data, and process
This commit is contained in:
parent
65d3ffbe70
commit
ee2cf3e8da
75
Data/Curie_Spent_Fuel_Site_Totals.csv
Normal file
75
Data/Curie_Spent_Fuel_Site_Totals.csv
Normal file
@ -0,0 +1,75 @@
|
||||
Facility,Total_Assemblies,Total_Tons,Op_Date_Min,Op_Date_Max,Close_Date_Min,Close_Date_Max
|
||||
Arkansas Nuclear One,5612,2500.5,1974-12-19,1980-03-26,2034-05-20,2038-07-17
|
||||
Beaver Valley,4945,2283.4,1976-10-01,1987-11-17,2036-01-29,2047-05-27
|
||||
Big Rock Point,526,69.4,1964-05-01,1964-05-01,1997-08-29,1997-08-29
|
||||
Braidwood,7181,3008.0,1988-07-29,1988-10-17,2046-10-17,2047-12-18
|
||||
Browns Ferry,22048,3957.0,1974-08-01,1977-03-01,2033-12-20,2036-07-02
|
||||
Brunswick,13364,2401.3,1975-11-03,1977-03-18,2034-12-27,2036-09-08
|
||||
Byron,7294,3058.9,1985-09-16,1987-08-02,2044-10-31,2046-11-06
|
||||
Callaway,3670,1544.2,1984-12-19,1984-12-19,2044-10-18,2044-10-18
|
||||
Calvert Cliffs,5830,2316.6,1975-05-08,1977-04-01,2034-07-31,2036-08-13
|
||||
Catawba,6266,2828.3,1985-06-29,1986-08-19,2043-12-05,2043-12-05
|
||||
Clinton,7256,1314.2,1987-11-24,1987-11-24,2047-04-17,2047-04-17
|
||||
Columbia,8084,1433.6,1984-12-13,1984-12-13,2043-12-20,2043-12-20
|
||||
Comanche Peak,7523,3171.9,1990-08-13,1993-08-03,2050-02-08,2053-02-02
|
||||
Cook,6346,2771.6,1975-08-28,1978-07-01,2034-10-25,2037-12-23
|
||||
Cooper,5832,1058.4,1974-07-01,1974-07-01,2034-01-18,2034-01-18
|
||||
Crystal River,1389,650.0,1977-03-13,1977-03-13,2013-02-20,2013-02-20
|
||||
Davis-Besse,2220,1065.9,1978-07-31,1978-07-31,2037-04-22,2037-04-22
|
||||
Diablo Canyon,5264,2249.4,1985-05-07,1986-03-13,2029-10-31,2030-10-31
|
||||
Dresden,15511,2664.2,1960-07-04,1971-11-16,2029-12-22,2031-01-12
|
||||
Duane Arnold,3648,659.5,1975-02-01,1975-02-01,2020-08-10,2020-08-10
|
||||
Farley,5503,2380.3,1977-12-01,1981-07-30,2037-06-25,2041-03-31
|
||||
Fermi,7710,1374.9,1988-01-23,1988-01-23,2045-03-20,2045-03-20
|
||||
Fitzpatrick,6180,1123.0,1975-07-28,1975-07-28,2034-10-17,2034-10-17
|
||||
Fort Calhoun,1264,466.0,1973-09-26,1973-09-26,2016-10-24,2016-10-24
|
||||
Ginna,1894,713.2,1970-07-01,1970-07-01,2029-09-18,2029-09-18
|
||||
Grand Gulf,10788,1927.8,1985-07-01,1985-07-01,2044-11-01,2044-11-01
|
||||
Haddam Neck,1102,448.4,1974-12-27,1974-12-27,1996-12-09,1996-12-09
|
||||
Harris,2699,1227.2,1987-05-02,1987-05-02,2046-10-24,2046-10-24
|
||||
Hatch,14400,2589.9,1975-12-31,1979-09-05,2034-08-06,2038-06-13
|
||||
Hope Creek,9413,1691.8,1986-12-20,1986-12-20,2046-04-11,2046-04-11
|
||||
Humboldt Bay,390,28.9,1963-08-01,1963-08-01,1976-07-02,1976-07-02
|
||||
Indian Point,3997,1774.6,1962-10-01,1976-08-30,2020-04-20,2021-04-30
|
||||
Kewaunee,1335,518.7,1974-06-16,1974-06-16,2013-05-07,2013-05-07
|
||||
La Salle,17587,3143.9,1984-01-01,1984-10-19,2042-04-17,2043-12-16
|
||||
LaCrosse,334,38.1,1967-07-03,1967-07-03,1987-04-30,1987-04-30
|
||||
Limerick,17925,3209.7,1986-02-01,1990-01-08,2044-10-26,2049-06-22
|
||||
Maine Yankee,1434,542.3,1973-06-29,1973-06-29,1996-12-06,1996-12-06
|
||||
McGuire,6171,2790.2,1981-12-01,1984-03-01,2041-06-12,2043-03-03
|
||||
Millstone,8817,3071.2,1970-12-28,1986-04-23,2035-07-31,2045-11-25
|
||||
Monticello,5026,888.9,1971-06-30,1971-06-30,2030-09-08,2030-09-08
|
||||
Nine Mile Point,14224,2522.9,1969-12-01,1988-03-11,2029-08-22,2046-10-31
|
||||
North Anna,5324,2472.1,1978-06-06,1980-12-14,2038-04-01,2040-08-21
|
||||
Oconee,7304,3429.9,1973-07-15,1974-12-16,2033-02-06,2034-07-19
|
||||
Oyster Creek,4504,796.6,1969-12-01,1969-12-01,2018-09-17,2018-09-17
|
||||
Palisades,2097,869.7,1971-12-31,1971-12-31,2022-05-31,2022-05-31
|
||||
Palo Verde,12633,5481.9,1986-01-28,1988-01-08,2045-06-01,2047-11-25
|
||||
Peach Bottom,23078,4155.1,1974-07-05,1974-12-23,2053-08-08,2054-07-02
|
||||
Perry,9026,1620.9,1987-11-18,1987-11-18,2046-11-07,2046-11-07
|
||||
Pilgrim,4113,731.0,1972-12-01,1972-12-01,2019-05-31,2019-05-31
|
||||
Point Beach,3646,1408.5,1970-12-21,1972-10-01,2030-10-05,2033-03-08
|
||||
Prairie Island,3754,1386.8,1973-12-16,1974-12-21,2033-08-09,2034-10-29
|
||||
Quad Cities,14918,2624.5,1973-02-18,1973-03-10,2032-12-14,2032-12-14
|
||||
Rancho Seco,493,228.4,1974-08-16,1974-08-16,1989-06-07,1989-06-07
|
||||
River Bend,7701,1374.7,1986-06-16,1986-06-16,2045-08-29,2045-08-29
|
||||
Robinson,2228,966.8,1971-03-07,1971-03-07,2030-07-31,2030-07-31
|
||||
Saint Lucie,6731,2647.0,1976-12-21,1983-08-08,2036-03-01,2043-04-06
|
||||
Salem,5722,2619.3,1977-06-30,1981-10-13,2036-08-13,2040-04-18
|
||||
San Onofre,4125,1707.8,1967-03-27,1984-04-01,1992-11-30,2013-06-12
|
||||
Seabrook,3323,1518.2,1990-08-19,1990-08-19,2050-03-15,2050-03-15
|
||||
Sequoyah,6212,2833.2,1981-07-01,1982-06-01,2040-09-17,2041-09-15
|
||||
South Texas,6141,3279.1,1988-08-25,1989-06-19,2047-08-20,2048-12-15
|
||||
Summer,2735,1157.1,1984-01-01,1984-01-01,2042-08-06,2042-08-06
|
||||
Surry,6583,3017.5,1972-12-22,1973-05-01,2052-05-25,2053-01-29
|
||||
Susquehanna,19552,3460.6,1983-06-08,1985-02-12,2042-07-17,2044-03-23
|
||||
Three Mile Island,1663,786.5,1974-09-02,1974-09-02,2019-09-20,2019-09-20
|
||||
Trojan,790,359.3,1975-11-21,1975-11-21,1992-11-09,1992-11-09
|
||||
Turkey Point,6468,2930.6,1972-12-14,1973-09-07,2052-07-19,2053-04-10
|
||||
Vermont Yankee,3879,705.9,1972-11-30,1972-11-30,2014-12-29,2014-12-29
|
||||
Vogtle,12553,5899.1,1987-06-01,1989-05-20,2047-01-16,2084-03-30
|
||||
Waterford,3957,1683.3,1985-09-24,1985-09-24,2044-12-18,2044-12-18
|
||||
Watts Bar,6489,2995.8,1996-05-27,2016-10-19,2055-11-09,2075-10-22
|
||||
Wolf Creek,3351,1534.1,1985-09-03,1985-09-03,2045-03-11,2045-03-11
|
||||
Yankee Rowe,533,127.1,1963-12-24,1963-12-24,1991-10-01,1991-10-01
|
||||
Zion,2226,1019.4,1973-12-31,1974-09-17,1996-09-19,1997-02-21
|
||||
|
7671
Data/Curie_Spent_Fuel_Timeline.csv
Normal file
7671
Data/Curie_Spent_Fuel_Timeline.csv
Normal file
File diff suppressed because it is too large
Load Diff
34
Data_Proc.r
Normal file
34
Data_Proc.r
Normal file
@ -0,0 +1,34 @@
|
||||
library(tidyverse)
|
||||
library(parallel)
|
||||
|
||||
TS <- read_csv("Data/Curie_Spent_Fuel_Timeline.csv")
|
||||
TOTAL <- read_csv("Data/Curie_Spent_Fuel_Site_Totals.csv") %>% mutate(OP_YEAR=year(Op_Date_Min),CLOSE_YEAR=year(Close_Date_Max))%>% select(Facility,Total_Assemblies,Total_Tons,OP_YEAR,CLOSE_YEAR)
|
||||
###########Series of functions to calculate the gross consumer surplus from a CIFS for each facility, in each year from 1960 to 2082.
|
||||
|
||||
#Function to find the net present revenue of a facility,given a discount rate, and the current year, and year the facility will close. This is the sum of discounted costs that WOULD have taken place if the facility was not built
|
||||
VALUE_ADD <- function(r,CURRENT_YEAR,CLOSE_YEAR){
|
||||
Years_Until_Close <- max(CLOSE_YEAR-CURRENT_YEAR+1,0)
|
||||
VALUES <- (1+r)^-(1:10^6)
|
||||
if(Years_Until_Close==0){return(sum(VALUES))} else{return(sum(VALUES[-1:-Years_Until_Close]))}
|
||||
}
|
||||
#A function to extend the revenues calculations to the closure date of all of the facilities.
|
||||
VALUE_ADD_SINGLE_YEAR <- function(r,CURRENT_YEAR,CLOSE_YEARS){return(sapply(CLOSE_YEARS,function(x){VALUE_ADD(r,CURRENT_YEAR,x)}))}
|
||||
#A function to extend the calculation of the net present revenues of each facility to all years of interest. That is what is the NPV of building the facility in each year, for each facility.
|
||||
NPV_CALC <- function(r,DATA=TOTAL,YEARS=1960:2083){
|
||||
Facility <- pull(DATA,Facility)
|
||||
RES <- cbind(Facility,do.call(cbind,lapply(YEARS,function(x){VALUE_ADD_SINGLE_YEAR(r,x,DATA$CLOSE_YEAR)})))
|
||||
colnames(RES) <- c("Facility",YEARS)
|
||||
RES <- as_tibble(RES) %>% pivot_longer(cols=-Facility,names_to="Year",values_to="Revenue") %>% arrange(Year)
|
||||
return(RES)
|
||||
}
|
||||
#A function which returns a list, of net present revenue calculation tables (facility by year) for a range of possible discount rates. This allows for the results to be quickly looked up, when we want to adjust the time value of money. These results will need to be combined with costs to calculate NPV
|
||||
MULTI_DISCOUNT_RATE_NPV <- function(INCREMENT=0.005,DATA=TOTAL,YEARS=1960:2083){
|
||||
NCORES <- detectCores()-1
|
||||
RES <- mclapply(seq(0,1,by=INCREMENT),NPV_CALC,mc.cores = NCORES)
|
||||
names(RES) <- 100*seq(0,1,by=INCREMENT) #Name with the discount rate of the given table
|
||||
return(RES)
|
||||
}
|
||||
|
||||
TOTAL_VALUE_METRICS <- MULTI_DISCOUNT_RATE_NPV(INCREMENT=0.0025)
|
||||
dir.create("./Results",showWarnings=FALSE)
|
||||
saveRDS(TOTAL_VALUE_METRICS,"./Results/Storage_Values_by_Facility_and_Variable_Discounts.Rds")
|
||||
414
Scrape_Curie_Map_Totals.js
Normal file
414
Scrape_Curie_Map_Totals.js
Normal file
@ -0,0 +1,414 @@
|
||||
///////////////////////////// // Get totals in the last year of the map (2082) as a way to check total capacity. This can be combined with the yearly data.
|
||||
|
||||
(async () => {
|
||||
/**********************
|
||||
* Utility functions *
|
||||
**********************/
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const visible = (el) => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
|
||||
const norm = (s) => (s || "").replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim();
|
||||
const labelKey = (s) => norm(s).replace(/:$/, "").toLowerCase();
|
||||
|
||||
// CSV helpers
|
||||
const csvEscape = (val) => {
|
||||
const s = (val === null || val === undefined) ? "" : String(val);
|
||||
if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
||||
return s;
|
||||
};
|
||||
const saveCSV = (rows, filename = "Curie_Spent_Fuel_Site_Totals.csv") => {
|
||||
const csv = rows.map(r => r.map(csvEscape).join(",")).join("\n");
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/********************************************
|
||||
* ISO-only date extraction & min/max *
|
||||
********************************************/
|
||||
const isValidISO = (yyyy, mm, dd) => {
|
||||
const y = Number(yyyy), m = Number(mm), d = Number(dd);
|
||||
if (!(y >= 1800 && y <= 2200)) return false;
|
||||
if (!(m >= 1 && m <= 12)) return false;
|
||||
if (!(d >= 1 && d <= 31)) return false; // light bounds
|
||||
return true;
|
||||
};
|
||||
|
||||
// Extract ISO date strings YYYY-MM-DD only, validated
|
||||
const extractISODates = (str) => {
|
||||
const s = norm(str);
|
||||
const re = /\b(\d{4})-(\d{2})-(\d{2})\b/g;
|
||||
const out = [];
|
||||
let m;
|
||||
while ((m = re.exec(s)) !== null) {
|
||||
if (isValidISO(m[1], m[2], m[3])) out.push(`${m[1]}-${m[2]}-${m[3]}`);
|
||||
}
|
||||
return Array.from(new Set(out)); // dedupe
|
||||
};
|
||||
|
||||
// Compute min/max via lexicographic compare (correct for ISO)
|
||||
const isoMinMax = (valOrArr) => {
|
||||
const arr = Array.isArray(valOrArr) ? valOrArr : [valOrArr];
|
||||
const all = [];
|
||||
for (const v of arr) {
|
||||
if (!v) continue;
|
||||
all.push(...extractISODates(v));
|
||||
}
|
||||
if (!all.length) return { min: "", max: "" };
|
||||
all.sort();
|
||||
return { min: all[0], max: all[all.length - 1] };
|
||||
};
|
||||
|
||||
/****************************************
|
||||
* Locate the "Locate Site" UI control *
|
||||
****************************************/
|
||||
const findLocateSiteControl = () => {
|
||||
// 1) Prefer labeled <select>
|
||||
const selects = Array.from(document.querySelectorAll("select"));
|
||||
for (const sel of selects) {
|
||||
const id = sel.id;
|
||||
let hasLabel = false;
|
||||
if (id) {
|
||||
const lab = document.querySelector(`label[for="${CSS.escape(id)}"]`);
|
||||
if (lab && /locate site/i.test(norm(lab.innerText))) hasLabel = true;
|
||||
}
|
||||
if (!hasLabel && sel.getAttribute("aria-label") && /locate site/i.test(sel.getAttribute("aria-label"))) {
|
||||
hasLabel = true;
|
||||
}
|
||||
if (!hasLabel) {
|
||||
const parentText = norm((sel.parentElement && sel.parentElement.innerText) || "");
|
||||
if (/locate site/i.test(parentText)) hasLabel = true;
|
||||
}
|
||||
if (hasLabel) return { type: "select", el: sel };
|
||||
}
|
||||
|
||||
// 2) ARIA combobox/button with text "Locate Site"
|
||||
const candidates = Array.from(document.querySelectorAll('[role="combobox"],[aria-haspopup="listbox"],button,div'))
|
||||
.filter(el => /locate site/i.test(norm(el.innerText)) || /locate site/i.test(norm(el.getAttribute("aria-label") || "")));
|
||||
if (candidates.length) {
|
||||
const combobox = candidates.find(visible) || candidates[0];
|
||||
return { type: "combobox", el: combobox };
|
||||
}
|
||||
|
||||
// 3) Fallback
|
||||
const any = Array.from(document.querySelectorAll("*")).find(el => /locate site/i.test(norm(el.innerText)));
|
||||
if (any) return { type: "unknown", el: any };
|
||||
return null;
|
||||
};
|
||||
|
||||
/*******************************************************
|
||||
* Read all facility names from the control *
|
||||
*******************************************************/
|
||||
const collectAllFacilityNames = async (control) => {
|
||||
const EXCLUDE = (t) => {
|
||||
const n = norm(t).toLowerCase();
|
||||
return !n || /^select|^choose|^locate site/.test(n) || n.includes("pick to zoom");
|
||||
};
|
||||
|
||||
if (control.type === "select") {
|
||||
const optEls = Array.from(control.el.querySelectorAll("option"));
|
||||
return optEls.map(o => norm(o.textContent)).filter(t => !EXCLUDE(t));
|
||||
}
|
||||
|
||||
// For combobox/custom dropdowns
|
||||
const openDropdown = () => control.el.click();
|
||||
const getListbox = () =>
|
||||
document.querySelector('[role="listbox"], [role="menu"], ul[role], div[role="listbox"], .dropdown-menu, .menu, ul');
|
||||
const closeDropdown = () => document.body.click();
|
||||
|
||||
openDropdown();
|
||||
await sleep(300);
|
||||
|
||||
const container = getListbox();
|
||||
if (!container) {
|
||||
console.warn("Could not locate the listbox menu for the combobox.");
|
||||
closeDropdown();
|
||||
return [];
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const readVisibleItems = () => {
|
||||
const items = Array.from(container.querySelectorAll('[role="option"], li, [data-value], .option, .menu-item'));
|
||||
for (const it of items) {
|
||||
const txt = norm(it.textContent);
|
||||
if (!EXCLUDE(txt)) seen.add(txt);
|
||||
}
|
||||
};
|
||||
|
||||
let lastSize = -1;
|
||||
let stagnant = 0;
|
||||
while (stagnant < 3) {
|
||||
readVisibleItems();
|
||||
if (container.scrollHeight > container.clientHeight) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
await sleep(150);
|
||||
readVisibleItems();
|
||||
container.scrollTop = 0;
|
||||
await sleep(120);
|
||||
}
|
||||
if (seen.size === lastSize) stagnant++;
|
||||
else { lastSize = seen.size; stagnant = 0; }
|
||||
}
|
||||
closeDropdown();
|
||||
return Array.from(seen);
|
||||
};
|
||||
|
||||
/***************************************************************
|
||||
* Select a facility by name in the control *
|
||||
***************************************************************/
|
||||
const selectFacility = async (control, name) => {
|
||||
if (control.type === "select") {
|
||||
const sel = control.el;
|
||||
const option = Array.from(sel.options).find(o => norm(o.textContent) === norm(name));
|
||||
if (!option) throw new Error(`Option not found: ${name}`);
|
||||
sel.value = option.value;
|
||||
sel.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
control.el.click();
|
||||
await sleep(180);
|
||||
const items = Array.from(document.querySelectorAll('[role="option"], li, [data-value], .option, .menu-item'));
|
||||
let target = items.find(it => norm(it.textContent) === norm(name));
|
||||
if (!target) target = items.find(it => norm(it.textContent).toLowerCase().includes(norm(name).toLowerCase()));
|
||||
if (!target) throw new Error(`Could not find menu item for: ${name}`);
|
||||
target.scrollIntoView({ block: "nearest" });
|
||||
target.click();
|
||||
await sleep(220);
|
||||
};
|
||||
|
||||
/*******************************************************
|
||||
* Wait for popup/details and extract the needed data *
|
||||
* (scope to most recently visible container) *
|
||||
*******************************************************/
|
||||
const waitForDetailLis = async (timeoutMs = 15000) => {
|
||||
const selectorContainers = '.leaflet-popup-content, .leaflet-popup, .popup, .modal, .panel, [role="dialog"], [role="region"]';
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < timeoutMs) {
|
||||
const containers = Array.from(document.querySelectorAll(selectorContainers)).filter(visible);
|
||||
// Prefer the last (most recently added) container that contains our labels
|
||||
for (let i = containers.length - 1; i >= 0; i--) {
|
||||
const lis = Array.from(containers[i].querySelectorAll("li"));
|
||||
const hasKey = lis.some(li =>
|
||||
/^(?:Number of Assemblies:|Operating Date:|Last Projected Discharge:|Projected License Expiration Year:|Metric Tons of Heavy Metal \(MTHM\):)/i
|
||||
.test(norm(li.textContent))
|
||||
);
|
||||
if (hasKey && lis.length) return lis;
|
||||
}
|
||||
|
||||
// Fallback: any visible <li> that matches
|
||||
const allLis = Array.from(document.querySelectorAll("li")).filter(visible);
|
||||
const hasKey = allLis.some(li =>
|
||||
/Number of Assemblies:|Operating Date:|Last Projected Discharge:|Projected License Expiration Year:|Metric Tons of Heavy Metal \(MTHM\):/i
|
||||
.test(norm(li.textContent))
|
||||
);
|
||||
if (hasKey) return allLis;
|
||||
|
||||
await sleep(150);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Parse list items to a Map of normalized label -> [values...], strict labels only
|
||||
const parseDetailListToMap = (lis) => {
|
||||
const map = new Map();
|
||||
const pushVal = (key, value) => {
|
||||
const k = labelKey(key);
|
||||
const v = norm(value);
|
||||
if (!map.has(k)) map.set(k, []);
|
||||
if (v) map.get(k).push(v);
|
||||
};
|
||||
|
||||
for (const li of lis) {
|
||||
const text = norm(li.textContent);
|
||||
if (!text) continue;
|
||||
|
||||
let label = "";
|
||||
let value = "";
|
||||
|
||||
// Prefer <strong>/<b> at start
|
||||
const boldish = li.querySelector("strong, b");
|
||||
if (boldish) {
|
||||
const boldText = norm(boldish.textContent);
|
||||
const restText = norm(text.replace(boldText, ""));
|
||||
if (/:$/.test(boldText)) {
|
||||
label = boldText;
|
||||
value = restText.replace(/^:\s*/, "");
|
||||
} else {
|
||||
const idx = text.indexOf(":");
|
||||
if (idx !== -1) { label = text.slice(0, idx + 1); value = text.slice(idx + 1); }
|
||||
}
|
||||
} else {
|
||||
const idx = text.indexOf(":");
|
||||
if (idx !== -1) { label = text.slice(0, idx + 1); value = text.slice(idx + 1); }
|
||||
}
|
||||
|
||||
label = labelKey(label);
|
||||
value = norm(value);
|
||||
|
||||
if ([
|
||||
"number of assemblies",
|
||||
"operating date",
|
||||
"last projected discharge",
|
||||
"projected license expiration year",
|
||||
"metric tons of heavy metal (mthm)"
|
||||
].includes(label)) {
|
||||
pushVal(label, value);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
const getDetailsForCurrentSelection = async () => {
|
||||
const lis = await waitForDetailLis(15000);
|
||||
if (!lis.length) {
|
||||
return {
|
||||
numberOfAssemblies: "",
|
||||
mthm: "",
|
||||
opMin: "",
|
||||
opMax: "",
|
||||
lpdMin: "",
|
||||
lpdMax: ""
|
||||
};
|
||||
}
|
||||
|
||||
const map = parseDetailListToMap(lis);
|
||||
|
||||
// Total_Assemblies
|
||||
let numberOfAssemblies = "";
|
||||
const noaVals = map.get("number of assemblies") || [];
|
||||
if (noaVals.length) {
|
||||
const m = noaVals.join(" ").match(/(\d[\d,]*)/);
|
||||
numberOfAssemblies = m ? m[1].replace(/,/g, "") : norm(noaVals.join(" "));
|
||||
}
|
||||
|
||||
// Total_Tons (MTHM)
|
||||
let mthm = "";
|
||||
const mthmVals = map.get("metric tons of heavy metal (mthm)") || [];
|
||||
if (mthmVals.length) {
|
||||
const mm = mthmVals.join(" ").match(/(\d[\d,]*(?:\.\d+)?)/);
|
||||
if (mm) mthm = mm[1].replace(/,/g, "");
|
||||
else mthm = norm(mthmVals.join(" "));
|
||||
}
|
||||
|
||||
// Operating Date (ISO only)
|
||||
let opMin = "", opMax = "";
|
||||
const opVals = map.get("operating date") || [];
|
||||
if (opVals.length) {
|
||||
const { min, max } = isoMinMax(opVals);
|
||||
opMin = min; opMax = max;
|
||||
}
|
||||
|
||||
// Close date from LPD (ISO-only). Fallback to PLEY (ISO-first, then year -> YYYY-12-31)
|
||||
let lpdMin = "", lpdMax = "";
|
||||
const lpdVals = map.get("last projected discharge") || [];
|
||||
let usedLPD = false;
|
||||
if (lpdVals.length) {
|
||||
const { min, max } = isoMinMax(lpdVals);
|
||||
if (min && max) {
|
||||
lpdMin = min; lpdMax = max;
|
||||
usedLPD = true;
|
||||
}
|
||||
}
|
||||
if (!usedLPD) {
|
||||
const pleyVals = map.get("projected license expiration year") || [];
|
||||
if (pleyVals.length) {
|
||||
// ISO inside PLEY first
|
||||
const isoFromPley = isoMinMax(pleyVals);
|
||||
if (isoFromPley.min && isoFromPley.max) {
|
||||
lpdMin = isoFromPley.min;
|
||||
lpdMax = isoFromPley.max;
|
||||
} else {
|
||||
// Year-only fallback -> end-of-year
|
||||
const years = [];
|
||||
for (const s of pleyVals) {
|
||||
const matches = norm(s).match(/\b(\d{4})\b/g) || [];
|
||||
years.push(...matches.map(Number).filter(y => y >= 1800 && y <= 2200));
|
||||
}
|
||||
if (years.length) {
|
||||
years.sort((a, b) => a - b);
|
||||
lpdMin = `${years[0]}-12-31`;
|
||||
lpdMax = `${years[years.length - 1]}-12-31`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
numberOfAssemblies,
|
||||
mthm,
|
||||
opMin, opMax,
|
||||
lpdMin, lpdMax
|
||||
};
|
||||
};
|
||||
|
||||
/*******************************
|
||||
* Main routine starts here *
|
||||
*******************************/
|
||||
console.log("Locating the 'Locate Site' control...");
|
||||
const control = findLocateSiteControl();
|
||||
if (!control) {
|
||||
console.error("Could not find the 'Locate Site' control on this page. Scroll the page to ensure it is visible, then try again.");
|
||||
return;
|
||||
}
|
||||
console.log("Control type:", control.type);
|
||||
|
||||
console.log("Collecting facility names (excluding 'Pick to zoom')...");
|
||||
const facilityNames = await collectAllFacilityNames(control);
|
||||
if (!facilityNames.length) {
|
||||
console.error("No facility names discovered in the 'Locate Site' menu.");
|
||||
return;
|
||||
}
|
||||
console.log(`Discovered ${facilityNames.length} facilities.`);
|
||||
|
||||
const results = [];
|
||||
// Simplified headers for R/Python use
|
||||
results.push([
|
||||
"Facility",
|
||||
"Total_Assemblies",
|
||||
"Total_Tons",
|
||||
"Op_Date_Min",
|
||||
"Op_Date_Max",
|
||||
"Close_Date_Min",
|
||||
"Close_Date_Max"
|
||||
]);
|
||||
|
||||
let i = 0;
|
||||
for (const name of facilityNames) {
|
||||
i++;
|
||||
console.log(`[${i}/${facilityNames.length}] Selecting: ${name}`);
|
||||
try {
|
||||
await selectFacility(control, name);
|
||||
await sleep(350); // allow UI to render
|
||||
|
||||
const details = await getDetailsForCurrentSelection();
|
||||
|
||||
results.push([
|
||||
name,
|
||||
details.numberOfAssemblies || "",
|
||||
details.mthm || "",
|
||||
details.opMin || "",
|
||||
details.opMax || "",
|
||||
details.lpdMin || "",
|
||||
details.lpdMax || ""
|
||||
]);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to collect for "${name}":`, err?.message || err);
|
||||
results.push([name, "", "", "", "", "", ""]);
|
||||
}
|
||||
await sleep(220);
|
||||
}
|
||||
|
||||
console.log("Saving CSV...");
|
||||
saveCSV(results, "Curie_Spent_Fuel_Site_Totals.csv");
|
||||
console.log("Done. Check your downloads for Curie_Spent_Fuel_Site_Totals.csv");
|
||||
})();
|
||||
|
||||
423
Scrape_Curie_Map_Yearly_Values.js
Normal file
423
Scrape_Curie_Map_Yearly_Values.js
Normal file
@ -0,0 +1,423 @@
|
||||
|
||||
(async () => {
|
||||
// === Curie "Locate Site" scraper → CSV: year,Facility,Assemblies,Tons (stops at 2082) ===
|
||||
// - Facility from dropdown selection text
|
||||
// - Assemblies from: <li>Number of Assemblies: N</li> (missing -> 0)
|
||||
// - Tons from: <li>Metric Tons of Heavy Metal (MTHM): N</li> (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 <li> 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 <select> or ARIA combobox) ---
|
||||
const isLocateLabel = (txt) => /\blocate\s+site\b/i.test(txt || '');
|
||||
|
||||
const findLocateSelect = () => {
|
||||
// Label + select
|
||||
for (const lab of Array.from(document.querySelectorAll('label'))) {
|
||||
if (isLocateLabel(lab.textContent)) {
|
||||
const id = lab.getAttribute('for');
|
||||
if (id) {
|
||||
const sel = document.getElementById(id);
|
||||
if (sel && sel.tagName.toLowerCase() === 'select' && inViewport(sel)) return sel;
|
||||
}
|
||||
const sel2 = lab.closest('*')?.querySelector('select');
|
||||
if (sel2 && inViewport(sel2)) return sel2;
|
||||
}
|
||||
}
|
||||
// aria-label/title
|
||||
const sel3 = Array.from(document.querySelectorAll('select[aria-label], select[title]'))
|
||||
.find(s => isLocateLabel(s.getAttribute('aria-label') || s.getAttribute('title') || ''));
|
||||
if (sel3 && inViewport(sel3)) return sel3;
|
||||
// any visible select near text
|
||||
for (const sel of Array.from(document.querySelectorAll('select'))) {
|
||||
if (!inViewport(sel)) continue;
|
||||
const parentText = (sel.closest('*')?.innerText || '').trim();
|
||||
if (isLocateLabel(parentText)) return sel;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findLocateCombobox = () => {
|
||||
const byAria = Array.from(document.querySelectorAll('[role="combobox"], input[aria-haspopup="listbox"], [aria-controls]'))
|
||||
.filter(inViewport)
|
||||
.find(el => isLocateLabel(el.getAttribute('aria-label') || el.getAttribute('title') || el.textContent || ''));
|
||||
if (byAria) return byAria;
|
||||
const containers = Array.from(document.querySelectorAll('*')).filter(inViewport)
|
||||
.filter(el => isLocateLabel(el.textContent || ''));
|
||||
for (const c of containers) {
|
||||
const trigger = c.querySelector('[role="combobox"], input, .select, .dropdown, button');
|
||||
if (trigger && inViewport(trigger)) return trigger;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Build fresh list of option texts each year (unique, in order)
|
||||
const getSelectOptionTexts = (sel) => {
|
||||
const out = [];
|
||||
for (let i = 0; i < sel.options.length; i++) {
|
||||
const o = sel.options[i];
|
||||
const t = (o.text || '').trim();
|
||||
if (o.disabled) continue;
|
||||
if (!t) continue;
|
||||
if (/^select\b|^choose\b|locate site/i.test(t)) continue; // placeholders
|
||||
out.push(t);
|
||||
}
|
||||
return out.filter((t, i) => out.indexOf(t) === i);
|
||||
};
|
||||
|
||||
const openComboAndGetOptionTexts = async (combo) => {
|
||||
combo.scrollIntoView({ block: 'center' });
|
||||
combo.click();
|
||||
const start = performance.now();
|
||||
let panel = null;
|
||||
while (performance.now() - start < 1500) {
|
||||
panel = document.querySelector('[role="listbox"], .select-menu, .dropdown-menu, .menu, .listbox');
|
||||
if (panel && panel.offsetParent !== null) break;
|
||||
await sleep(60);
|
||||
}
|
||||
if (!panel) return { texts: [] };
|
||||
const items = Array.from(panel.querySelectorAll('[role="option"], li, .option, .menu-item'))
|
||||
.filter(el => inViewport(el) && (el.textContent || '').trim());
|
||||
const texts = items.map(el => el.textContent.trim())
|
||||
.filter(t => !/^select\b|^choose\b|locate site/i.test(t));
|
||||
// unique
|
||||
const unique = texts.filter((t, i) => texts.indexOf(t) === i);
|
||||
// Close panel to keep a clean state
|
||||
document.body.click();
|
||||
await sleep(80);
|
||||
return { texts: unique };
|
||||
};
|
||||
|
||||
// Select option by exact text
|
||||
const selectByTextInSelect = async (sel, text) => {
|
||||
let idx = -1;
|
||||
for (let i = 0; i < sel.options.length; i++) {
|
||||
if ((sel.options[i].text || '').trim() === text) { idx = i; break; }
|
||||
}
|
||||
if (idx < 0) return false;
|
||||
sel.selectedIndex = idx;
|
||||
sel.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
};
|
||||
|
||||
const openComboAndClickText = async (combo, text) => {
|
||||
combo.scrollIntoView({ block: 'center' });
|
||||
combo.click();
|
||||
const start = performance.now();
|
||||
let panel = null;
|
||||
while (performance.now() - start < 1500) {
|
||||
panel = document.querySelector('[role="listbox"], .select-menu, .dropdown-menu, .menu, .listbox');
|
||||
if (panel && panel.offsetParent !== null) break;
|
||||
await sleep(60);
|
||||
}
|
||||
if (!panel) return false;
|
||||
const item = Array.from(panel.querySelectorAll('[role="option"], li, .option, .menu-item'))
|
||||
.find(el => (el.textContent || '').trim() === text);
|
||||
if (!item) {
|
||||
document.body.click();
|
||||
await sleep(50);
|
||||
return false;
|
||||
}
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
item.click();
|
||||
await sleep(120);
|
||||
return true;
|
||||
};
|
||||
|
||||
// --- CSV helpers ---
|
||||
const csvEscape = v => {
|
||||
if (v == null) return '';
|
||||
const s = String(v);
|
||||
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
};
|
||||
const downloadCSV = (rows) => {
|
||||
const header = ['year', 'Facility', 'Assemblies', 'Tons'];
|
||||
const body = rows.map(r => [r.year, r.Facility, r.Assemblies, r.Tons].map(csvEscape).join(',')).join('\n');
|
||||
const csv = header.join(',') + '\n' + body;
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'Curie_Spent_Fuel_Timeline.csv'; // fixed name
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
};
|
||||
|
||||
// --- MAIN (declare once; no redeclarations) ---
|
||||
const rightBtn = findRightBtn();
|
||||
const leftBtn = findLeftBtn();
|
||||
|
||||
window._curieScrapeStop = false;
|
||||
|
||||
let year = getCurrentYear();
|
||||
if (!Number.isInteger(year)) {
|
||||
console.warn('Could not read the current year; assuming 1968.');
|
||||
year = 1968;
|
||||
}
|
||||
|
||||
// Resolve the Locate Site control
|
||||
const nativeSelect = findLocateSelect();
|
||||
const comboTrigger = nativeSelect ? null : findLocateCombobox();
|
||||
if (!nativeSelect && !comboTrigger) {
|
||||
console.error('Could not find the "Locate Site" dropdown/combobox. Make sure it is visible.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
console.log(`Starting capture via "Locate Site" from ${year} to ${STOP_YEAR} (inclusive).`);
|
||||
|
||||
// Capture all facilities for a year (debounced popup + precise li parsing)
|
||||
const captureYear = async (y) => {
|
||||
let optionTexts = [];
|
||||
if (nativeSelect) {
|
||||
optionTexts = getSelectOptionTexts(nativeSelect);
|
||||
} else {
|
||||
const { texts } = await openComboAndGetOptionTexts(comboTrigger);
|
||||
optionTexts = texts;
|
||||
}
|
||||
|
||||
console.log(`Year ${y}: Locate Site options = ${optionTexts.length}`);
|
||||
|
||||
const perYearSeen = new Set(); // (year|Facility)
|
||||
let prevSig = popupSignature();
|
||||
|
||||
// Clean start
|
||||
closePopupIfOpen();
|
||||
prevSig = popupSignature();
|
||||
|
||||
for (let i = 0; i < optionTexts.length; i++) {
|
||||
if (window._curieScrapeStop) break;
|
||||
|
||||
const Facility = optionTexts[i];
|
||||
const rowKey = `${y}|${Facility}`;
|
||||
if (perYearSeen.has(rowKey)) continue;
|
||||
|
||||
// Close popup so content change is detectable
|
||||
closePopupIfOpen();
|
||||
prevSig = popupSignature();
|
||||
|
||||
// Select by text
|
||||
let ok = false;
|
||||
if (nativeSelect) {
|
||||
ok = await selectByTextInSelect(nativeSelect, Facility);
|
||||
} else {
|
||||
ok = await openComboAndClickText(comboTrigger, Facility);
|
||||
}
|
||||
if (!ok) {
|
||||
console.warn(` [${y}] Could not select "${Facility}".`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for popup & its content to CHANGE vs previous signature
|
||||
const pop = await waitForPopup(4500, 100);
|
||||
if (!pop) {
|
||||
console.warn(` [${y}] No popup after selecting "${Facility}". Skipping.`);
|
||||
continue;
|
||||
}
|
||||
const newSig = await waitForPopupContentChange(prevSig, 4500, 100);
|
||||
if (!newSig || newSig === prevSig) {
|
||||
// Retry once
|
||||
if (nativeSelect) {
|
||||
await selectByTextInSelect(nativeSelect, Facility);
|
||||
} else {
|
||||
await openComboAndClickText(comboTrigger, Facility);
|
||||
}
|
||||
const pop2 = await waitForPopup(4500, 100);
|
||||
if (!pop2) {
|
||||
console.warn(` [${y}] Popup did not open for "${Facility}" after retry.`);
|
||||
continue;
|
||||
}
|
||||
const newSig2 = await waitForPopupContentChange(prevSig, 4500, 100);
|
||||
if (!newSig2 || newSig2 === prevSig) {
|
||||
console.warn(` [${y}] Popup content unchanged for "${Facility}". Skipping to avoid offset.`);
|
||||
continue;
|
||||
}
|
||||
prevSig = newSig2;
|
||||
} else {
|
||||
prevSig = newSig;
|
||||
}
|
||||
|
||||
// Parse Assemblies and Tons from the popup's <li> elements
|
||||
const parsed = extractFromPopup(popupEl());
|
||||
const Assemblies = parsed.Assemblies || 0;
|
||||
const Tons = parsed.Tons || 0;
|
||||
|
||||
// Record row (dedupe per year|Facility)
|
||||
if (!perYearSeen.has(rowKey)) {
|
||||
perYearSeen.add(rowKey);
|
||||
rows.push({ year: y, Facility, Assemblies, Tons });
|
||||
}
|
||||
|
||||
// Close before next selection
|
||||
closePopupIfOpen();
|
||||
|
||||
if ((i + 1) % 10 === 0) {
|
||||
console.log(` ${i + 1}/${optionTexts.length} facilities captured...`);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-year quick summary
|
||||
const yrRows = rows.filter(r => r.year === y);
|
||||
const sumTons = yrRows.reduce((a, r) => a + (Number.isFinite(r.Tons) ? r.Tons : 0), 0);
|
||||
const sumAssemblies = yrRows.reduce((a, r) => a + (Number.isFinite(r.Assemblies) ? r.Assemblies : 0), 0);
|
||||
console.log(`Year ${y} summary → rows=${yrRows.length}, Sum Assemblies=${sumAssemblies}, Sum Tons=${sumTons.toFixed(3)}`);
|
||||
};
|
||||
|
||||
// Capture starting year
|
||||
await captureYear(year);
|
||||
|
||||
// Advance to 2082 (reuses the SAME rightBtn/leftBtn; no redeclaration)
|
||||
if (rightBtn) {
|
||||
while (year < STOP_YEAR) {
|
||||
if (window._curieScrapeStop) break;
|
||||
|
||||
const prev = year;
|
||||
const next = await stepRightOneYear(rightBtn, leftBtn, prev);
|
||||
|
||||
if (!Number.isInteger(next) || next === prev) {
|
||||
console.warn('Year did not advance; saving partial results and exiting.');
|
||||
break;
|
||||
}
|
||||
|
||||
year = next;
|
||||
await sleep(350); // let map/widgets update
|
||||
await captureYear(year);
|
||||
}
|
||||
}
|
||||
|
||||
// Download CSV with fixed filename
|
||||
downloadCSV(rows);
|
||||
|
||||
console.log(`Done. CSV downloaded with ${rows.length} rows. Last year captured: ${Math.min(year, STOP_YEAR)}`);
|
||||
})().catch(e => console.error('[Curie via-locate-site scraper error]', e));
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user