mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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
|
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
|
stations, transit vehicles, road traffic flow, weather, weather map images
|
||||||
(radar, satellite, lightning), wind, the most recent satellite image of an
|
(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
|
perimeters, terrain height, military installations and map features, plus a
|
||||||
combined situation brief, the app's heads-up display caption, and a link that
|
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
|
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_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) |
|
| `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_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_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_cyclones` | `cyclones` | Active NHC/CPHC tropical cyclones, optionally in an area |
|
||||||
| `get_fire_perimeters` | `perimeters` | Mapped WFIGS wildfire perimeters in an area, largest first |
|
| `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/atmosphere.test.mjs",
|
||||||
"src/tools/queries/imagery.test.mjs",
|
"src/tools/queries/imagery.test.mjs",
|
||||||
"src/tools/queries/cables.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/layers/wind/fields.js",
|
||||||
"src/tools/queries/imagery.js",
|
"src/tools/queries/imagery.js",
|
||||||
"src/tools/queries/cables.js",
|
"src/tools/queries/cables.js",
|
||||||
"src/tools/queries/surveillance.js"
|
"src/tools/queries/surveillance.js",
|
||||||
|
"src/tools/queries/infrastructure.js"
|
||||||
],
|
],
|
||||||
"external": ["satellite.js"]
|
"external": ["satellite.js"]
|
||||||
},
|
},
|
||||||
@@ -3191,7 +3192,8 @@
|
|||||||
"src/layers/alpr/source.js",
|
"src/layers/alpr/source.js",
|
||||||
"src/layers/alpr/policy.js",
|
"src/layers/alpr/policy.js",
|
||||||
"src/layers/alpr/records.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"]
|
"external": ["@mapbox/vector-tile", "pbf"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,3 +101,63 @@ export function mapAnalystRecord(raw, layerId = '') {
|
|||||||
output,
|
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';
|
} from './queries/environment.js';
|
||||||
import { getActiveFires, getEarthquakes } from './queries/hazards.js';
|
import { getActiveFires, getEarthquakes } from './queries/hazards.js';
|
||||||
import { getRecentImagery } from './queries/imagery.js';
|
import { getRecentImagery } from './queries/imagery.js';
|
||||||
|
import { findInfrastructure } from './queries/infrastructure.js';
|
||||||
import {
|
import {
|
||||||
findVessel,
|
findVessel,
|
||||||
getVesselTrack,
|
getVesselTrack,
|
||||||
@@ -97,6 +98,7 @@ export const coreTools = Object.freeze([
|
|||||||
getWind,
|
getWind,
|
||||||
getRecentImagery,
|
getRecentImagery,
|
||||||
findSubmarineCables,
|
findSubmarineCables,
|
||||||
|
findInfrastructure,
|
||||||
getRegionalBrief,
|
getRegionalBrief,
|
||||||
getCyclones,
|
getCyclones,
|
||||||
getFirePerimeters,
|
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 { createWeatherSource } from '../layers/weather/source.js';
|
||||||
import { createWindSource } from '../layers/wind/source.js';
|
import { createWindSource } from '../layers/wind/source.js';
|
||||||
import { createApplicationRequestServices } from '../services/requests.js';
|
import { createApplicationRequestServices } from '../services/requests.js';
|
||||||
|
import { createInfrastructureSource } from '../sources/infrastructureData.js';
|
||||||
import {
|
import {
|
||||||
createAdsbLolSource,
|
createAdsbLolSource,
|
||||||
createAisStreamSource,
|
createAisStreamSource,
|
||||||
@@ -70,6 +71,7 @@ export function createToolServices({ fetchImpl, appUrl }) {
|
|||||||
imagery: createImageryService({ fetchImpl }),
|
imagery: createImageryService({ fetchImpl }),
|
||||||
cables: createBundledCableSource({ fetchImpl }),
|
cables: createBundledCableSource({ fetchImpl }),
|
||||||
alpr: createAlprTileSource({ tileFetchImpl: fetchImpl }),
|
alpr: createAlprTileSource({ tileFetchImpl: fetchImpl }),
|
||||||
|
infrastructure: createInfrastructureSource({ fetchImpl }),
|
||||||
regional: requests.regional,
|
regional: requests.regional,
|
||||||
terrain: requests.terrain,
|
terrain: requests.terrain,
|
||||||
summary: requests.summary,
|
summary: requests.summary,
|
||||||
|
|||||||
Reference in new issue
Block a user