feat: add a submarine cable tool

Add find_submarine_cables over the bundled TeleGeography data: cables
passing through or landing in an area, or matching a name, with landing
points and the dataset's attribution. The local MCP server's fetch reads
the bundled data files a browser would load by URL, and nothing outside
that directory.
This commit is contained in:
Sameh Khamis committed 2026-10-01 13:28:02 -07:00
1 parent 9be90fff44
commit c1957d884e
9 files changed
+226 -7

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), 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, regional briefs, tropical cyclones, fire
area, submarine cables, 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
@@ -130,6 +130,7 @@ public feeds the sources already use.
| `get_weather_map` | `weatherMaps` | The latest NOAA radar, satellite or lightning map image over an area |
| `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) |
| `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
@@ -881,5 +881,6 @@
"src/tools/queries/share.test.mjs",
"src/tools/localRoute.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"
]
+4 -2
View File
@@ -3111,7 +3111,8 @@
"src/layers/wind/inspection.js",
"src/layers/wind/model.js",
"src/layers/wind/fields.js",
"src/tools/queries/imagery.js"
"src/tools/queries/imagery.js",
"src/tools/queries/cables.js"
],
"external": ["satellite.js"]
},
@@ -3181,7 +3182,8 @@
"src/layers/wind/source.js",
"src/sources/windBody.js",
"src/layers/recentImagery/catalog.js",
"src/layers/recentImagery/model.js"
"src/layers/recentImagery/model.js",
"src/layers/submarineCables/bundledSource.js"
],
"external": ["@mapbox/vector-tile", "pbf"]
}
+33 -3
View File
@@ -3,11 +3,20 @@
* running God's Eye View server's `/api` routes.
*/
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { createToolServices } from '../../src/tools/services.js';
// Bundled data some sources read by module-relative URL.
const BUNDLED_DATA = new URL('../../src/data/local_data/', import.meta.url);
export const DEFAULT_API_BASE = 'http://localhost:4173';
/** Resolve the sources' relative `/api/...` requests against `apiBase`. */
/**
* Resolve the sources' relative `/api/...` requests against `apiBase`, and
* serve `file:` URLs inside the bundled data directory from disk, as a
* browser would load those module-relative assets.
*/
export function createApiFetch({
apiBase = DEFAULT_API_BASE,
fetchImpl = (...args) => globalThis.fetch(...args),
@@ -15,13 +24,34 @@ export function createApiFetch({
const base = new URL(apiBase);
if (!['http:', 'https:'].includes(base.protocol))
throw new TypeError(`apiBase must be an http(s) URL: ${apiBase}`);
return (input, init) =>
fetchImpl(
return (input, init) => {
const url = String(input instanceof Request ? input.url : input);
if (url.startsWith('file:')) return readBundled(url);
return fetchImpl(
typeof input === 'string' && input.startsWith('/')
? new URL(input, base)
: input,
init,
);
};
}
async function readBundled(url) {
const href = new URL(url).href;
if (!href.startsWith(BUNDLED_DATA.href) || href.includes('/../'))
return new Response(null, { status: 404 });
try {
const body = await readFile(fileURLToPath(href));
return new Response(body, {
headers: {
'Content-Type': href.endsWith('.json')
? 'application/json'
: 'application/octet-stream',
},
});
} catch {
return new Response(null, { status: 404 });
}
}
/** Construct every service Core's tools read, backed by the local server. */
+2
View File
@@ -10,6 +10,7 @@ import {
getAircraftTrack,
} from './queries/aviation.js';
import { getWeatherMap, getWind } from './queries/atmosphere.js';
import { findSubmarineCables } from './queries/cables.js';
import { getHudCaption, situationBrief } from './queries/brief.js';
import {
findMilitaryInstallations,
@@ -93,6 +94,7 @@ export const coreTools = Object.freeze([
getWeatherMap,
getWind,
getRecentImagery,
findSubmarineCables,
getRegionalBrief,
getCyclones,
getFirePerimeters,
+86
View File
@@ -0,0 +1,86 @@
/** Submarine cables and their landing points. */
import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaContains, resolveArea } from '../area.js';
import { LIMIT_SCHEMA, capRows, countNoun } from '../results.js';
const ATTRIBUTION = '© TeleGeography — submarinecablemap.com (CC BY-NC-SA 3.0)';
function cableLines(feature) {
const geometry = feature?.geometry;
if (geometry?.type === 'LineString') return [geometry.coordinates];
if (geometry?.type === 'MultiLineString') return geometry.coordinates;
return [];
}
export const findSubmarineCables = defineTool({
name: 'find_submarine_cables',
title: 'Submarine cables',
description:
'Undersea telecommunication cables and landing points from the ' +
'TeleGeography map: those passing through or landing in an area, ' +
'and/or those whose name matches.',
inputSchema: {
type: 'object',
properties: {
area: AREA_SCHEMA,
name: { type: 'string', minLength: 2, maxLength: 80 },
limit: LIMIT_SCHEMA,
},
additionalProperties: false,
},
requires: ['cables'],
async run(args, { services, signal }) {
if (!args.area && !args.name)
throw new ToolError('invalid_arguments', 'Give an area, a name or both');
const area = args.area
? await resolveArea(args.area, { services, signal })
: null;
const wanted = args.name?.toLowerCase();
const { cables, landingPoints } = await services.cables.fetch(signal);
const landingRows = (landingPoints?.features || [])
.map((feature) => ({
id: feature.properties?.id ?? null,
name: feature.properties?.name ?? null,
lon: feature.geometry?.coordinates?.[0],
lat: feature.geometry?.coordinates?.[1],
}))
.filter(
(point) =>
point.name &&
Number.isFinite(point.lat) &&
(!area || areaContains(area, point)),
);
const rows = (cables?.features || [])
.filter((feature) => {
const name = String(feature.properties?.name || '');
if (!name || (wanted && !name.toLowerCase().includes(wanted)))
return false;
return (
!area ||
cableLines(feature).some((line) =>
line.some(([lon, lat]) => areaContains(area, { lat, lon })),
)
);
})
.map((feature) => ({
id: feature.properties.id ?? null,
name: feature.properties.name,
}))
.sort((a, b) => a.name.localeCompare(b.name));
const where = area ? ` in ${area.label}` : '';
const what = wanted ? ` matching "${args.name}"` : '';
return {
summary:
`${countNoun(rows.length, 'submarine cable')}${what}${where}` +
(area
? `, with ${countNoun(landingRows.length, 'landing point')}.`
: '.'),
data: {
...capRows(rows, args.limit),
landing_points: area ? capRows(landingRows, args.limit) : null,
attribution: ATTRIBUTION,
},
};
},
});
+95
View File
@@ -0,0 +1,95 @@
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 { createBundledCableSource } from '../../layers/submarineCables/bundledSource.js';
const cables = {
async fetch() {
return {
cables: {
features: [
{
properties: { id: 'marea', name: 'MAREA' },
geometry: {
type: 'MultiLineString',
coordinates: [
[
[-76, 36.8],
[-40, 40],
[-2.9, 43.4],
],
],
},
},
{
properties: { id: 'unity', name: 'Unity' },
geometry: {
type: 'LineString',
coordinates: [
[-118, 34],
[139, 35],
],
},
},
],
},
landingPoints: {
features: [
{
properties: { id: 'vb', name: 'Virginia Beach, VA, United States' },
geometry: { type: 'Point', coordinates: [-75.98, 36.85] },
},
{
properties: { id: 'bilbao', name: 'Bilbao, Spain' },
geometry: { type: 'Point', coordinates: [-2.93, 43.26] },
},
],
},
};
},
};
test('cables are found by area and name, with landing points and attribution', async () => {
const catalog = composeCatalog({ tools: coreTools, services: { cables } });
const virginia = await catalog.call('find_submarine_cables', {
area: { lat: 36.85, lon: -75.98, radius_km: 50 },
});
assert.equal(
virginia.summary,
'1 submarine cable in 50 km around 36.850, -75.980, with 1 landing point.',
);
assert.deepEqual(virginia.data.rows, [{ id: 'marea', name: 'MAREA' }]);
assert.equal(
virginia.data.landing_points.rows[0].name,
'Virginia Beach, VA, United States',
);
assert.match(virginia.data.attribution, /TeleGeography.*CC BY-NC-SA 3\.0/);
const named = await catalog.call('find_submarine_cables', { name: 'uni' });
assert.equal(named.summary, '1 submarine cable matching "uni".');
assert.equal(named.data.landing_points, null);
await assert.rejects(
catalog.call('find_submarine_cables', {}),
/area, a name or both/,
);
});
test('the local fetch serves bundled data files and nothing outside them', async () => {
const fetchImpl = createApiFetch({
apiBase: 'http://127.0.0.1:1',
fetchImpl: async () => assert.fail('no network'),
});
const { cables: collection, landingPoints } = await createBundledCableSource({
fetchImpl,
}).fetch();
assert.ok(collection.features.length > 500);
assert.ok(landingPoints.features.length > 1000);
const outside = await fetchImpl(
new URL('../../../package.json', import.meta.url).href,
);
assert.equal(outside.status, 404);
const missing = await fetchImpl(
new URL('../../data/local_data/missing.json', import.meta.url).href,
);
assert.equal(missing.status, 404);
});
+2
View File
@@ -16,6 +16,7 @@ import { createInstallationSource } from '../layers/installations/source.js';
import { createLaunchSource } from '../layers/launches/source.js';
import { createWfigsPerimeterSource } from '../layers/perimeters/source.js';
import { createRadioSource } from '../layers/radio/source.js';
import { createBundledCableSource } from '../layers/submarineCables/bundledSource.js';
import { searchHls } from '../layers/recentImagery/catalog.js';
import { rankLatest, wvsSnapshotUrl } from '../layers/recentImagery/model.js';
import { createSatelliteSource } from '../layers/satellites/source.js';
@@ -66,6 +67,7 @@ export function createToolServices({ fetchImpl, appUrl }) {
weatherMaps: createWeatherSource({ fetchImpl }),
wind: createWindSource({ fetchImpl }),
imagery: createImageryService({ fetchImpl }),
cables: createBundledCableSource({ fetchImpl }),
regional: requests.regional,
terrain: requests.terrain,
summary: requests.summary,