mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat: add a datacenter and dam tool
This commit is contained in:
1 parent
a8e5ff9390
commit
28cfd39d1a
9 files changed
+285
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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/,
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user