feat: add a datacenter and dam tool

This commit is contained in:
Sameh Khamis committed 2026-10-01 13:38:38 -07:00
1 parent a8e5ff9390
commit 28cfd39d1a
9 files changed
+285 -4

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ pass over a point, and those overhead now), public cameras (including a
camera's current image), license plate reader cameras, radio stations, place search, routing, bike-share
stations, transit vehicles, road traffic flow, weather, weather map images
(radar, satellite, lightning), wind, the most recent satellite image of an
area, submarine cables, regional briefs, tropical cyclones, fire
area, submarine cables, datacenters and dams, regional briefs, tropical cyclones, fire
perimeters, terrain height, military installations and map features, plus a
combined situation brief, the app's heads-up display caption, and a link that
opens the app over an area. Tools reuse the layers' portable source factories, take a
+1
View File
@@ -132,6 +132,7 @@ public feeds the sources already use.
| `get_wind` | `wind` | GFS or IFS model wind 10 m above ground at a location |
| `get_recent_imagery` | `imagery` | The most recent clear Landsat/Sentinel-2 image of an area (VIIRS fallback) |
| `find_submarine_cables` | `cables` | TeleGeography cables and landing points by area or name (CC BY-NC-SA 3.0) |
| `find_infrastructure` | `infrastructure` | OpenStreetMap datacenters or dams in an area, nearest first (ODbL) |
| `get_regional_brief` | `regional` | What and where a location is, its weather and recent headlines |
| `get_cyclones` | `cyclones` | Active NHC/CPHC tropical cyclones, optionally in an area |
| `get_fire_perimeters` | `perimeters` | Mapped WFIGS wildfire perimeters in an area, largest first |
+2 -1
View File
@@ -883,5 +883,6 @@
"src/tools/queries/atmosphere.test.mjs",
"src/tools/queries/imagery.test.mjs",
"src/tools/queries/cables.test.mjs",
"src/tools/queries/surveillance.test.mjs"
"src/tools/queries/surveillance.test.mjs",
"src/tools/queries/infrastructure.test.mjs"
]
+4 -2
View File
@@ -3116,7 +3116,8 @@
"src/layers/wind/fields.js",
"src/tools/queries/imagery.js",
"src/tools/queries/cables.js",
"src/tools/queries/surveillance.js"
"src/tools/queries/surveillance.js",
"src/tools/queries/infrastructure.js"
],
"external": ["satellite.js"]
},
@@ -3191,7 +3192,8 @@
"src/layers/alpr/source.js",
"src/layers/alpr/policy.js",
"src/layers/alpr/records.js",
"src/layers/alpr/tileRecords.js"
"src/layers/alpr/tileRecords.js",
"src/sources/infrastructureData.js"
],
"external": ["@mapbox/vector-tile", "pbf"]
}
+60
View File
@@ -101,3 +101,63 @@ export function mapAnalystRecord(raw, layerId = '') {
output,
};
}
/** A representative point for a feature: its point, or its outline's average vertex. */
export function featurePoint(feature) {
const geometry = feature?.geometry;
const outline =
geometry?.type === 'Point'
? [geometry.coordinates]
: geometry?.type === 'LineString'
? geometry.coordinates
: geometry?.type === 'Polygon'
? geometry.coordinates?.[0]
: geometry?.type === 'MultiPolygon'
? geometry.coordinates?.[0]?.[0]
: null;
const points = (outline || []).filter(
(point) => Number.isFinite(point?.[0]) && Number.isFinite(point?.[1]),
);
if (!points.length) return null;
return {
lon: points.reduce((sum, point) => sum + point[0], 0) / points.length,
lat: points.reduce((sum, point) => sum + point[1], 0) / points.length,
};
}
/** Read the bundled layers' analyst records through a supplied transport. */
export function createInfrastructureSource({
fetchImpl = (...args) => globalThis.fetch(...args),
} = {}) {
const cache = new Map();
return {
async getRecords(layerId, { signal } = {}) {
const url = INFRASTRUCTURE_DATA_URLS[layerId];
if (!url) throw new TypeError(`Unknown infrastructure layer: ${layerId}`);
if (!cache.has(layerId)) {
signal?.throwIfAborted();
const response = await fetchImpl(url, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const records = parseGeojsonLines(await response.text()).flatMap(
(feature) => {
const point = featurePoint(feature);
return point
? [
mapAnalystRecord(
{
id: feature.properties?.osm_id ?? feature.id,
...point,
properties: feature.properties,
},
layerId,
),
]
: [];
},
);
cache.set(layerId, records);
}
return cache.get(layerId);
},
};
}
+2
View File
@@ -23,6 +23,7 @@ import {
} from './queries/environment.js';
import { getActiveFires, getEarthquakes } from './queries/hazards.js';
import { getRecentImagery } from './queries/imagery.js';
import { findInfrastructure } from './queries/infrastructure.js';
import {
findVessel,
getVesselTrack,
@@ -97,6 +98,7 @@ export const coreTools = Object.freeze([
getWind,
getRecentImagery,
findSubmarineCables,
findInfrastructure,
getRegionalBrief,
getCyclones,
getFirePerimeters,
+70
View File
@@ -0,0 +1,70 @@
/** Bundled OpenStreetMap infrastructure: datacenters and dams. */
import { defineTool } from '../catalog.js';
import {
AREA_SCHEMA,
areaCenter,
areaContains,
distanceKm,
resolveArea,
} from '../area.js';
import { LIMIT_SCHEMA, capRows, countNoun } from '../results.js';
const KINDS = {
datacenters: { layerId: 'local-datacenters', noun: 'datacenter' },
dams: { layerId: 'local-dams', noun: 'dam' },
};
const ATTRIBUTION = {
datacenters: '© OpenStreetMap contributors (ODbL 1.0)',
dams: '© OpenStreetMap contributors (ODbL 1.0), via Open Infrastructure Map',
};
const round = (value, digits) =>
Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
export const findInfrastructure = defineTool({
name: 'find_infrastructure',
title: 'Datacenters and dams',
description:
'Datacenters (with operator and capacity) or dams (with river and power ' +
'output) mapped in OpenStreetMap within an area, nearest the center first.',
inputSchema: {
type: 'object',
properties: {
kind: { type: 'string', enum: Object.keys(KINDS) },
area: AREA_SCHEMA,
limit: LIMIT_SCHEMA,
},
required: ['kind', 'area'],
additionalProperties: false,
},
requires: ['infrastructure'],
async run(args, { services, signal }) {
const area = await resolveArea(args.area, { services, signal });
const center = areaCenter(area);
const { layerId, noun } = KINDS[args.kind];
const records = await services.infrastructure.getRecords(layerId, {
signal,
});
const rows = records
.filter((record) => areaContains(area, record))
.map((record) => ({
name: record.name,
operator: record.operator,
...(args.kind === 'datacenters'
? { capacity: record.capacity }
: { river: record.river, output: record.output }),
lat: round(record.lat, 5),
lon: round(record.lon, 5),
distance_km: round(distanceKm(center, record), 1),
}))
.sort((a, b) => a.distance_km - b.distance_km);
return {
summary: `${countNoun(rows.length, `mapped ${noun}`)} in ${area.label}.`,
data: {
...capRows(rows, args.limit),
attribution: ATTRIBUTION[args.kind],
},
};
},
});
+143
View File
@@ -0,0 +1,143 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeCatalog, coreTools } from '../index.js';
import { createApiFetch } from '../../../server/mcp/services.js';
import {
createInfrastructureSource,
featurePoint,
} from '../../sources/infrastructureData.js';
test('features are placed at their point or average outline vertex', () => {
assert.deepEqual(
featurePoint({ geometry: { type: 'Point', coordinates: [10, 20] } }),
{ lon: 10, lat: 20 },
);
assert.deepEqual(
featurePoint({
geometry: {
type: 'Polygon',
coordinates: [
[
[0, 0],
[2, 0],
[2, 2],
[0, 2],
],
],
},
}),
{ lon: 1, lat: 1 },
);
assert.deepEqual(
featurePoint({
geometry: {
type: 'MultiPolygon',
coordinates: [
[
[
[4, 4],
[6, 6],
],
],
],
},
}),
{ lon: 5, lat: 5 },
);
assert.equal(featurePoint({ geometry: null }), null);
});
test('datacenters and dams are listed nearest first with their own fields', async () => {
const records = {
'local-datacenters': [
{
id: 'a',
name: 'Equinix DC2',
lat: 38.9,
lon: -77.45,
operator: 'Equinix',
capacity: '20 MW',
river: null,
output: null,
},
{
id: 'b',
name: 'Far DC',
lat: 48.8,
lon: 2.35,
operator: null,
capacity: null,
river: null,
output: null,
},
],
'local-dams': [
{
id: 'c',
name: 'Hoover Dam',
lat: 36.016,
lon: -114.737,
operator: 'USBR',
capacity: null,
river: 'Colorado River',
output: '2080 MW',
},
],
};
const infrastructure = { getRecords: async (layerId) => records[layerId] };
const catalog = composeCatalog({
tools: coreTools,
services: { infrastructure },
});
const dc = await catalog.call('find_infrastructure', {
kind: 'datacenters',
area: { lat: 38.95, lon: -77.45, radius_km: 20 },
});
assert.equal(
dc.summary,
'1 mapped datacenter in 20 km around 38.950, -77.450.',
);
assert.deepEqual(dc.data.rows[0], {
name: 'Equinix DC2',
operator: 'Equinix',
capacity: '20 MW',
lat: 38.9,
lon: -77.45,
distance_km: 5.6,
});
assert.match(dc.data.attribution, /OpenStreetMap contributors \(ODbL 1\.0\)/);
const dams = await catalog.call('find_infrastructure', {
kind: 'dams',
area: { lat: 36, lon: -114.7, radius_km: 20 },
});
assert.deepEqual(dams.data.rows[0], {
name: 'Hoover Dam',
operator: 'USBR',
river: 'Colorado River',
output: '2080 MW',
lat: 36.016,
lon: -114.737,
distance_km: 3.8,
});
});
test('the bundled datasets load through the local fetch', async () => {
const fetchImpl = createApiFetch({
apiBase: 'http://127.0.0.1:1',
fetchImpl: async () => assert.fail('no network'),
});
const source = createInfrastructureSource({ fetchImpl });
const dams = await source.getRecords('local-dams');
const datacenters = await source.getRecords('local-datacenters');
assert.ok(dams.length > 500 && datacenters.length > 3000);
assert.ok(
dams.every(
(record) => Number.isFinite(record.lat) && Number.isFinite(record.lon),
),
);
assert.equal(await source.getRecords('local-dams'), dams);
await assert.rejects(
source.getRecords('local-other'),
/Unknown infrastructure layer/,
);
});
+2
View File
@@ -26,6 +26,7 @@ import { createTransitSource } from '../layers/transit/source.js';
import { createWeatherSource } from '../layers/weather/source.js';
import { createWindSource } from '../layers/wind/source.js';
import { createApplicationRequestServices } from '../services/requests.js';
import { createInfrastructureSource } from '../sources/infrastructureData.js';
import {
createAdsbLolSource,
createAisStreamSource,
@@ -70,6 +71,7 @@ export function createToolServices({ fetchImpl, appUrl }) {
imagery: createImageryService({ fetchImpl }),
cables: createBundledCableSource({ fetchImpl }),
alpr: createAlprTileSource({ tileFetchImpl: fetchImpl }),
infrastructure: createInfrastructureSource({ fetchImpl }),
regional: requests.regional,
terrain: requests.terrain,
summary: requests.summary,