busi488energy/scripts/download-power-plants.ts
Joey Eamigh 8f99f6535e
phase 7: full US coverage — grid regions, datacenters, power plants, backfill, chart perf
- 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
2026-02-11 16:08:06 -05:00

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);
});