diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 8a9bec7ae..2c102da85 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -135,4 +135,4 @@ public feeds the sources already use. | `get_map_features` | `features` | Administrative areas, named places or monuments at a location (needs Overpass) | | `situation_brief` | `weather` | Weather, earthquakes, fires, aircraft, ships and cyclones for an area, by section | | `get_hud_caption` | `weather`, `summary` | The app's heads-up display caption for an area | -| `open_in_gods_eye_view` | `app` | A share link looking straight down on an area | +| `open_in_gods_eye_view` | `app` | A share link looking straight down on an area, with chosen layers on | diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 60c3edfb9..ecde8cac4 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -3102,6 +3102,8 @@ "src/tools/queries/brief.js", "src/tools/queries/maritime.js", "src/tools/queries/share.js", + "src/data/layerState.js", + "src/data/layerStateTokenReservations.json", "src/sources/gbfsStations.js", "src/sources/tle.js", "src/data/satellitePass.js" diff --git a/src/tools/queries/share.js b/src/tools/queries/share.js index 1c1aaf703..cbb3e8bab 100644 --- a/src/tools/queries/share.js +++ b/src/tools/queries/share.js @@ -1,5 +1,10 @@ /** A link that opens God's Eye View on an area. */ +import { + REGISTERED_LAYER_IDS, + createDefaultLayerState, + encodeLayerStateParams, +} from '../../data/layerState.js'; import { defineTool, ToolError } from '../catalog.js'; import { AREA_SCHEMA, areaCenter, areaRadiusKm, resolveArea } from '../area.js'; @@ -12,10 +17,19 @@ export const openInGodsEyeView = defineTool({ name: 'open_in_gods_eye_view', title: "Open in God's Eye View", description: - "A link that opens God's Eye View looking straight down on an area.", + "A link that opens God's Eye View looking straight down on an area, " + + 'optionally with chosen data layers turned on.', inputSchema: { type: 'object', - properties: { area: AREA_SCHEMA }, + properties: { + area: AREA_SCHEMA, + layers: { + type: 'array', + maxItems: REGISTERED_LAYER_IDS.length, + items: { type: 'string', enum: [...REGISTERED_LAYER_IDS] }, + description: 'Layers to turn on, such as "flights" or "earthquakes".', + }, + }, required: ['area'], additionalProperties: false, }, @@ -38,18 +52,26 @@ export const openInGodsEyeView = defineTool({ ), ), ); - // The share-link camera fields; the app restores its defaults for the rest. - base.hash = new URLSearchParams({ + // The share-link camera and layer fields; the app restores its defaults + // for everything else. + const params = new URLSearchParams({ v: '2', lat: center.lat.toFixed(4), lon: center.lon.toFixed(4), alt: String(altitude), heading: '0', pitch: '-90', - }).toString(); + }); + const layers = [...new Set(args.layers || [])]; + if (layers.length) + encodeLayerStateParams(params, { + ...createDefaultLayerState(), + enabledLayerIds: layers, + }); + base.hash = params.toString(); return { summary: `Open ${area.label} in God's Eye View: ${base.href}`, - data: { url: base.href, center, altitude_m: altitude }, + data: { url: base.href, center, altitude_m: altitude, layers }, }; }, }); diff --git a/src/tools/queries/share.test.mjs b/src/tools/queries/share.test.mjs index 570bc1d0f..038e1eb88 100644 --- a/src/tools/queries/share.test.mjs +++ b/src/tools/queries/share.test.mjs @@ -53,3 +53,28 @@ test('altitude is bounded for tiny and planet-sized areas', async () => { (error) => error.code === 'unavailable', ); }); + +test('links turn on the requested layers with the app share-link codec', async () => { + const { decodeLayerStateParams } = await import('../../data/layerState.js'); + const catalog = composeCatalog({ + tools: coreTools, + services: { app: { baseUrl: 'http://localhost:4173/' } }, + }); + const result = await catalog.call('open_in_gods_eye_view', { + area: { lat: 30.27, lon: -97.74, radius_km: 10 }, + layers: ['earthquakes', 'flights', 'flights'], + }); + assert.deepEqual(result.data.layers, ['earthquakes', 'flights']); + const params = new URLSearchParams(new URL(result.data.url).hash.slice(1)); + assert.deepEqual(decodeLayerStateParams(params).enabledLayerIds.sort(), [ + 'earthquakes', + 'flights', + ]); + await assert.rejects( + catalog.call('open_in_gods_eye_view', { + area: { lat: 0, lon: 0, radius_km: 1 }, + layers: ['not-a-layer'], + }), + /must be one of/, + ); +});