diff --git a/demo/js/gep.js b/demo/js/gep.js index 3fbcf628..a96306de 100644 --- a/demo/js/gep.js +++ b/demo/js/gep.js @@ -57,17 +57,17 @@ interactiveMap.on('app:ready', function () { label: 'Layers', panelId: 'layers', iconSvgContent: '', - mobile: { slot: 'top-left', order: 1, showLabel: false }, - tablet: { slot: 'top-left', order: 2 }, - desktop: { slot: 'top-left', order: 2 } + mobile: { slot: 'top-left', order: 3, showLabel: false }, + tablet: { slot: 'top-left', order: 3 }, + desktop: { slot: 'top-left', order: 3 } }) interactiveMap.addButton('key', { label: 'Key', panelId: 'key', iconSvgContent: '', - mobile: { slot: 'top-left', order: 2, showLabel: false }, - tablet: { slot: 'top-left', order: 3 }, - desktop: { slot: 'top-left', order: 3 } + mobile: { slot: 'top-left', order: 4, showLabel: false }, + tablet: { slot: 'top-left', order: 4 }, + desktop: { slot: 'top-left', order: 4 } }) interactiveMap.addPanel('layers', { label: 'Layers', diff --git a/demo/js/mapStyles.js b/demo/js/mapStyles.js index cd87f085..b0e19695 100755 --- a/demo/js/mapStyles.js +++ b/demo/js/mapStyles.js @@ -11,6 +11,12 @@ const AERIAL_THUMBNAIL = '/assets/images/aerial-map-thumb.jpg' const BW_ID = 'black-and-white' const BW_LABEL = 'Black/White' +// Real OS National Grid coverage extent [minX, minY, maxX, maxY], EPSG:27700 — captured from +// OS's own VTS capabilities response (matches the TILE_GRID_ORIGIN corner already baked into +// providers/beta/openlayers/src/defaults.js). Used to stop the raster styles below requesting +// tiles for areas OS has no coverage for. +const OS_NATIONAL_GRID_EXTENT_27700 = [-238375, 0, 700000, 1300000] + const openMapStyles = [{ id: 'outdoor', label: 'Outdoor', @@ -120,7 +126,7 @@ const vtsMapStyles27700 = [{ const apgbAerialStyle = { id: 'apgb-aerial-125mm', - label: 'Aerial 12.5cm', + label: 'Aerial', type: 'wms', url: process.env.APGB_WMS_URL, params: { LAYERS: 'APGB_Latest_UK_125mm', BGCOLOR: '0x1E3448', TRANSPARENT: false }, @@ -160,7 +166,9 @@ const ngdMapStyles27700 = [{ const mapsRasterStyles27700 = [{ id: 'outdoor', label: 'Outdoor', + type: 'raster', url: `${process.env.MAPS_OUTDOOR_URL}?key=${process.env.OS_CLIENT_ID}`, + extent: OS_NATIONAL_GRID_EXTENT_27700, thumbnail: '/assets/images/outdoor-raster-thumb.jpg', logo: OS_LOGO, logoAltText: OS_LOGO_ALT, @@ -171,7 +179,9 @@ const mapsRasterStyles27700 = [{ }, { id: 'road', label: 'Road', + type: 'raster', url: `${process.env.MAPS_ROAD_URL}?key=${process.env.OS_CLIENT_ID}`, + extent: OS_NATIONAL_GRID_EXTENT_27700, thumbnail: '/assets/images/road-raster-thumb.jpg', logo: OS_LOGO, logoAltText: OS_LOGO_ALT, @@ -182,7 +192,9 @@ const mapsRasterStyles27700 = [{ }, { id: 'light', label: 'Light', + type: 'raster', url: `${process.env.MAPS_LIGHT_URL}?key=${process.env.OS_CLIENT_ID}`, + extent: OS_NATIONAL_GRID_EXTENT_27700, thumbnail: '/assets/images/light-raster-thumb.jpg', logo: OS_LOGO, logoAltText: OS_LOGO_ALT, diff --git a/docs/api/map-style-config.md b/docs/api/map-style-config.md index 3219799e..d7fa4dbe 100644 --- a/docs/api/map-style-config.md +++ b/docs/api/map-style-config.md @@ -100,7 +100,10 @@ URL that returns a Mapbox GL style document (Mapbox Style Specification). ### `params` **Type:** `Object` -WMS request parameters. Passed directly to the OpenLayers `TileWMS` source when `type` is `'wms'`. Most WMS GetMap requests should include `LAYERS`. +> [!NOTE] +> This property is only relevant when using the **OpenLayers provider** with `type: 'wms'`. It is ignored by other providers and by other `type` values. + +WMS request parameters. Passed directly to the OpenLayers `TileWMS` source. Most WMS GetMap requests should include `LAYERS`. ```js { @@ -112,6 +115,24 @@ WMS request parameters. Passed directly to the OpenLayers `TileWMS` source when --- +### `extent` +**Type:** `[number, number, number, number]` + +> [!NOTE] +> This property is only relevant when using the **OpenLayers provider** with `type: 'raster'`. A bare XYZ tile URL template has no capabilities document to determine real coverage from, so the consumer configuring the style must supply it directly. It is ignored by other providers and by other `type` values. + +Bounding box `[minX, minY, maxX, maxY]` in EPSG:27700 — the units the OpenLayers provider's tile grid is built in. When set, no tiles outside this area are requested, avoiding failed tile requests where the basemap has no coverage. Omit to request tiles across the whole tile grid regardless of real coverage (the default). Only limits which tiles are *requested* — panning and zooming outside the extent is unaffected. + +```js +{ + type: 'raster', + url: 'https://api.os.uk/maps/raster/v1/zxy/Outdoor_27700/{z}/{x}/{y}.png?key=YOUR_API_KEY', + extent: [-238375, 0, 700000, 1300000] // OS National Grid coverage +} +``` + +--- + ### `label` **Type:** `string` diff --git a/providers/beta/openlayers/src/utils/tileLayers.js b/providers/beta/openlayers/src/utils/tileLayers.js index 32e7365e..b2edc1c6 100644 --- a/providers/beta/openlayers/src/utils/tileLayers.js +++ b/providers/beta/openlayers/src/utils/tileLayers.js @@ -15,7 +15,7 @@ import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from '../defaults. recordStyleLayer(true) const CRS = 'EPSG:27700' -const SUPPORTED_MAP_STYLE_TYPES = ['vector', 'raster', 'wms', 'ogc-vt'] +const SUPPORTED_MAP_STYLE_TYPES = new Set(['vector', 'raster', 'wms', 'ogc-vt']) export function fetchWithTransform (url, resourceType, transformRequest) { const result = transformRequest ? (transformRequest(url, resourceType) || {}) : {} @@ -32,11 +32,12 @@ const createTileLoadFunction = (transformRequest) => (tile, src) => { .catch(() => tile.setState(TileState.ERROR)) } -function createTileGrid () { +function createTileGrid (extent) { return new TileGrid({ resolutions: TILE_GRID_RESOLUTIONS, origin: TILE_GRID_ORIGIN, - tileSize: TILE_SIZE + tileSize: TILE_SIZE, + ...(extent && { extent }) }) } @@ -52,8 +53,12 @@ export function createWMSTileSource (url, params, transformRequest) { }) } -export function createTileSource (url, transformRequest) { - const tileGrid = createTileGrid() +// `extent` has no discovery protocol to fetch it from for a bare XYZ template — unlike the +// other map style types, only the consumer configuring this particular tile service can supply +// it, via `mapStyle.extent` ([minX, minY, maxX, maxY] in EPSG:27700). Omitted → no extent, same +// as before: OL requests tiles for the whole tile grid regardless of real coverage. +export function createTileSource (url, transformRequest, extent) { + const tileGrid = createTileGrid(extent) const tileUrlFunction = ([z, x, y]) => url .replace('{z}', z) @@ -69,7 +74,7 @@ export function createTileSource (url, transformRequest) { } export async function createMapStyleLayer (mapStyle, transformRequest) { - if (mapStyle.type && !SUPPORTED_MAP_STYLE_TYPES.includes(mapStyle.type)) { + if (mapStyle.type && !SUPPORTED_MAP_STYLE_TYPES.has(mapStyle.type)) { throw new Error(`Unsupported map style type: '${mapStyle.type}'`) } @@ -79,7 +84,7 @@ export async function createMapStyleLayer (mapStyle, transformRequest) { } if (mapStyle.type === 'raster') { - const source = createTileSource(mapStyle.url, transformRequest) + const source = createTileSource(mapStyle.url, transformRequest, mapStyle.extent) return { layer: new TileLayer({ source }), source } } diff --git a/providers/beta/openlayers/src/utils/tileLayers.test.js b/providers/beta/openlayers/src/utils/tileLayers.test.js index 2303fed3..485cdc12 100644 --- a/providers/beta/openlayers/src/utils/tileLayers.test.js +++ b/providers/beta/openlayers/src/utils/tileLayers.test.js @@ -127,6 +127,26 @@ describe('createTileSource', () => { const { tileLoadFunction } = XYZ.mock.calls[0][0] expect(typeof tileLoadFunction).toBe('function') }) + + it('does not set a TileGrid extent when none is given, so tiles across the whole grid can be requested', () => { + createTileSource('https://tiles.example.com/{z}/{x}/{y}', null) + expect(TileGrid).toHaveBeenCalledWith({ + resolutions: TILE_GRID_RESOLUTIONS, + origin: TILE_GRID_ORIGIN, + tileSize: TILE_SIZE + }) + }) + + it('passes a given extent through to the TileGrid, so out-of-coverage tiles are never requested', () => { + const extent = [0, 0, 700000, 1300000] + createTileSource('https://tiles.example.com/{z}/{x}/{y}', null, extent) + expect(TileGrid).toHaveBeenCalledWith({ + resolutions: TILE_GRID_RESOLUTIONS, + origin: TILE_GRID_ORIGIN, + tileSize: TILE_SIZE, + extent + }) + }) }) describe('createWMSTileSource', () => { @@ -244,6 +264,12 @@ describe('createMapStyleLayer', () => { expect(result).toEqual({ layer: mockTileLayerInstance, source: mockSourceInstance }) }) + it('passes mapStyle.extent through to the raster TileGrid', async () => { + const extent = [-233752.41, -4325.11, 609472.1, 1278448.84] + await createMapStyleLayer({ url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster', extent }, null) + expect(TileGrid).toHaveBeenCalledWith(expect.objectContaining({ extent })) + }) + it('creates an OGC vector tile layer and source when mapStyle.type is ogc-vt', async () => { global.fetch = makeOGCFetchMock() const result = await createMapStyleLayer({ url: 'https://example.com/ogc-styles', type: 'ogc-vt' }, null) diff --git a/src/types.js b/src/types.js index ca4b24a6..80ca85af 100644 --- a/src/types.js +++ b/src/types.js @@ -462,6 +462,15 @@ * @property {Object} [params] * WMS request parameters. Passed directly to the OpenLayers `TileWMS` source when `type` is `'wms'`. * Most WMS GetMap requests should include `LAYERS`. Example: `{ LAYERS: 'MyLayer', FORMAT: 'image/jpeg' }`. + * + * @property {[number, number, number, number]} [extent] + * Bounding box [minX, minY, maxX, maxY] in EPSG:27700, the units the OpenLayers provider's + * tile grid is built in. When set, no tiles outside this area are requested — a plain XYZ tile + * URL template has no capabilities document to determine real coverage from, so the consumer + * configuring the style must supply it directly. **Currently only supported by the OpenLayers + * provider's `'raster'` type.** Omit to request tiles across the whole tile grid regardless of + * real coverage. Panning/zooming outside the extent is unaffected; + * only tile requests are limited. */ /**