/** * 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; type ArcGISGeoJSONResponse = z.infer; async function fetchPage(offset: number): Promise { 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); });