- Add 7 new grid regions (BPA, DUKE, SOCO, TVA, FPC, WAPA, NWMT) to cover entire continental US - Expand datacenters from 108 to 292 facilities across 39 operators - Add EIA power plant pipeline: download script, 3,546 plants >= 50 MW with diamond map markers - Rewrite backfill script for 10-year data (2015-07-01) with quarterly/monthly chunking, 3-region parallelism, resumability - Add materialized views (daily/weekly) with server-side granularity selection for chart performance - Fix map UX: z-index tooltips, disable POI clicks, move legend via MapControl
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
/**
|
|
* Downloads power plant data from the EIA ArcGIS FeatureServer.
|
|
*
|
|
* Fetches all US power plants >= 50 MW with pagination,
|
|
* then saves the combined result as data/power-plants.geojson.
|
|
*
|
|
* Usage: bun run scripts/download-power-plants.ts
|
|
*/
|
|
|
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { z } from 'zod/v4';
|
|
|
|
const BASE_URL =
|
|
'https://services2.arcgis.com/FiaPA4ga0iQKduv3/ArcGIS/rest/services/Power_Plants_in_the_US/FeatureServer/0/query';
|
|
|
|
const OUT_FIELDS = [
|
|
'Plant_Name',
|
|
'Plant_Code',
|
|
'Utility_Na',
|
|
'State',
|
|
'County',
|
|
'Latitude',
|
|
'Longitude',
|
|
'PrimSource',
|
|
'Total_MW',
|
|
].join(',');
|
|
|
|
const PAGE_SIZE = 2000;
|
|
|
|
const ArcGISFeatureSchema = z.object({
|
|
type: z.literal('Feature'),
|
|
geometry: z.object({
|
|
type: z.literal('Point'),
|
|
coordinates: z.tuple([z.number(), z.number()]),
|
|
}),
|
|
properties: z.record(z.string(), z.unknown()),
|
|
});
|
|
|
|
const ArcGISResponseSchema = z.object({
|
|
type: z.literal('FeatureCollection'),
|
|
features: z.array(ArcGISFeatureSchema),
|
|
properties: z.object({ exceededTransferLimit: z.boolean().optional() }).optional(),
|
|
});
|
|
|
|
type ArcGISFeature = z.infer<typeof ArcGISFeatureSchema>;
|
|
type ArcGISGeoJSONResponse = z.infer<typeof ArcGISResponseSchema>;
|
|
|
|
async function fetchPage(offset: number): Promise<ArcGISGeoJSONResponse> {
|
|
const params = new URLSearchParams({
|
|
where: 'Total_MW >= 50',
|
|
outFields: OUT_FIELDS,
|
|
f: 'geojson',
|
|
resultRecordCount: String(PAGE_SIZE),
|
|
resultOffset: String(offset),
|
|
});
|
|
|
|
const url = `${BASE_URL}?${params.toString()}`;
|
|
console.log(`Fetching offset=${offset}...`);
|
|
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
}
|
|
|
|
const json: unknown = await res.json();
|
|
return ArcGISResponseSchema.parse(json);
|
|
}
|
|
|
|
async function main() {
|
|
const allFeatures: ArcGISFeature[] = [];
|
|
let offset = 0;
|
|
|
|
while (true) {
|
|
const page = await fetchPage(offset);
|
|
const count = page.features.length;
|
|
console.log(` Got ${count} features`);
|
|
allFeatures.push(...page.features);
|
|
|
|
// ArcGIS signals more data via exceededTransferLimit or by returning a full page
|
|
const hasMore = page.properties?.exceededTransferLimit === true || count >= PAGE_SIZE;
|
|
if (!hasMore || count === 0) break;
|
|
offset += PAGE_SIZE;
|
|
}
|
|
|
|
console.log(`\nTotal features: ${allFeatures.length}`);
|
|
|
|
const collection: ArcGISGeoJSONResponse = {
|
|
type: 'FeatureCollection',
|
|
features: allFeatures,
|
|
};
|
|
|
|
const outDir = resolve(import.meta.dirname, '..', 'data');
|
|
mkdirSync(outDir, { recursive: true });
|
|
|
|
const outPath = resolve(outDir, 'power-plants.geojson');
|
|
writeFileSync(outPath, JSON.stringify(collection, null, 2));
|
|
console.log(`Saved to ${outPath}`);
|
|
}
|
|
|
|
main().catch((err: unknown) => {
|
|
console.error('Download failed:', err);
|
|
process.exit(1);
|
|
});
|