diff --git a/src/tools/queries/atmosphere.js b/src/tools/queries/atmosphere.js index 838e4849f..f8e32012c 100644 --- a/src/tools/queries/atmosphere.js +++ b/src/tools/queries/atmosphere.js @@ -33,10 +33,12 @@ const round = (value, digits) => /** * A 2:1 box snapped to 0.25° around the area, moved inside the product's - * bounds. Returns null when the box would not fit, meaning the whole - * extent should be shown instead. + * bounds. Returns null when no such box covers the whole area (it would not + * fit, or the area crosses the antimeridian), meaning the whole extent + * should be shown instead. */ export function weatherWindow(area, bounds) { + if (area.west > area.east) return null; const center = areaCenter(area); const halfHeight = Math.max( MIN_HALF_HEIGHT_DEGREES, @@ -57,12 +59,19 @@ export function weatherWindow(area, bounds) { snapDown(bounds.north - height), ); const mapBox = { west, south, east: west + width, north: south + height }; - return mapBox.west >= bounds.west && + const insideProduct = + mapBox.west >= bounds.west && mapBox.east <= bounds.east && mapBox.south >= bounds.south && - mapBox.north <= bounds.north - ? mapBox - : null; + mapBox.north <= bounds.north; + // Moving the box inside the product can uncover part of the area; only a + // box that still holds the whole area (clipped to the product) will do. + const coversArea = + mapBox.west <= Math.max(area.west, bounds.west) && + mapBox.east >= Math.min(area.east, bounds.east) && + mapBox.south <= Math.max(area.south, bounds.south) && + mapBox.north >= Math.min(area.north, bounds.north); + return insideProduct && coversArea ? mapBox : null; } export const getWeatherMap = defineTool({ diff --git a/src/tools/queries/atmosphere.test.mjs b/src/tools/queries/atmosphere.test.mjs index 8623904ab..cf19d83f3 100644 --- a/src/tools/queries/atmosphere.test.mjs +++ b/src/tools/queries/atmosphere.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { composeCatalog, coreTools } from '../index.js'; import { weatherWindow } from './atmosphere.js'; +import { resolveArea } from '../area.js'; const CONUS = { west: -130, south: 20, east: -60, north: 55 }; const PNG = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]); @@ -156,3 +157,18 @@ test('wind is sampled from the model grid at the location', async () => { (error) => error.code === 'unavailable', ); }); + +test('a weather window that cannot cover the area falls back to the whole map', async () => { + const world = { west: -180, south: -90, east: 180, north: 90 }; + // 100 km around 179.8° crosses the antimeridian. + const dateline = await resolveArea({ lat: 0, lon: 179.8, radius_km: 100 }); + assert.equal(weatherWindow(dateline, world), null); + // Near a product edge the window moves inside and still covers the area. + const conus = { west: -130, south: 20, east: -60, north: 55 }; + const edge = await resolveArea({ lat: 40, lon: -129.5, radius_km: 50 }); + const box = weatherWindow(edge, conus); + assert.ok(box); + assert.ok(box.west <= Math.max(edge.west, conus.west)); + assert.ok(box.east >= edge.east); + assert.ok(box.south <= edge.south && box.north >= edge.north); +});