Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions demo/js/draw-ol.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// InteractiveMap with OpenLayers provider and draw-ol plugin
import InteractiveMap from '../../src/index.js'
import { vtsMapStyles27700, ngdMapStyles27700 } from './mapStyles.js'
import { vtsMapStyles27700, ngdMapStyles27700, apgbAerialStyle } from './mapStyles.js'
import { transformGeocodeRequest, transformVtsRequest27700 } from './auth.js'
// Providers
import openLayersProvider from '/providers/beta/openlayers/src/index.js'
Expand Down Expand Up @@ -54,7 +54,7 @@ const interactiveMap = new InteractiveMap('map', {
// readMapText: true,
plugins: [
mapStylesPlugin({
mapStyles: vtsMapStyles27700 // ngdMapStyles27700
mapStyles: [...vtsMapStyles27700, apgbAerialStyle] // ngdMapStyles27700
}),
searchPlugin({
transformRequest: transformGeocodeRequest,
Expand Down
4 changes: 2 additions & 2 deletions demo/js/gep.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import InteractiveMap from '../../src/index.js'
import { vtsMapStyles27700 } from './mapStyles.js'
import { vtsMapStyles27700, apgbAerialStyle } from './mapStyles.js'
import { transformGeocodeRequest, transformVtsRequest27700 } from './auth.js'
import '/plugins/beta/datasets/src/datasets.scss' // in a separate repo: import '@defra/interactive-map/plugins/datasets/css'
// Providers
Expand Down Expand Up @@ -37,7 +37,7 @@ const interactiveMap = new InteractiveMap('map', {
containerHeight: '650px',
plugins: [
mapStylesPlugin({
mapStyles: vtsMapStyles27700
mapStyles: [...vtsMapStyles27700, apgbAerialStyle]
}),
scaleBarPlugin({
units: 'metric'
Expand Down
17 changes: 15 additions & 2 deletions demo/js/mapStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const OS_ATTRIBUTION = `Contains OS data ${String.fromCodePoint(COPYRIGHT_SYMBOL
const OUTDOOR_THUMBNAIL = '/assets/images/outdoor-map-thumb.jpg'
const DARK_THUMBNAIL = '/assets/images/dark-map-thumb.jpg'
const BW_THUMBNAIL = '/assets/images/black-and-white-map-thumb.jpg'
const AERIAL_THUMBNAIL = '/assets/images/aerial-map-thumb.jpg'
const BW_ID = 'black-and-white'
const BW_LABEL = 'Black/White'

Expand Down Expand Up @@ -79,7 +80,7 @@ const vtsMapStyles3857 = [{
label: 'Aerial',
mapColorScheme: 'dark',
url: process.env.AERIAL_URL,
thumbnail: '/assets/images/aerial-map-thumb.jpg',
thumbnail: AERIAL_THUMBNAIL,
logoAltText: OS_LOGO_ALT,
attribution: 'Test'
}]
Expand Down Expand Up @@ -116,6 +117,17 @@ const vtsMapStyles27700 = [{
attribution: 'Test'
}]

const apgbAerialStyle = {
id: 'apgb-aerial-125mm',
label: 'Aerial 12.5cm',
type: 'wms',
url: process.env.APGB_WMS_URL,
params: { LAYERS: 'APGB_Latest_UK_125mm', BGCOLOR: '0x1E3448', TRANSPARENT: false },
thumbnail: AERIAL_THUMBNAIL,
mapColorScheme: 'dark',
attribution: `${String.fromCodePoint(COPYRIGHT_SYMBOL)} Getmapping Plc and Bluesky International Limited ${(new Date()).getFullYear()}`
}

const ngdMapStyles27700 = [{
id: 'outdoor',
label: 'Outdoor',
Expand Down Expand Up @@ -184,5 +196,6 @@ export {
vtsMapStyles3857,
vtsMapStyles27700,
ngdMapStyles27700,
mapsRasterStyles27700
mapsRasterStyles27700,
apgbAerialStyle
}
30 changes: 27 additions & 3 deletions docs/api/map-style-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ Unique identifier for the style. Used to reference the style programmatically.
---

### `type`
**Type:** `'raster' | 'ogc-vt'`
**Type:** `'vector' | 'raster' | 'wms' | 'ogc-vt'`

> [!NOTE]
> This property is only relevant when using the **OpenLayers provider**. The ESRI and MapLibre providers always use the standard Mapbox GL vector tile format and ignore this property.

Allows the OpenLayers provider to support raster, standard vector tile, and OGC API - Tiles basemaps. When omitted, OpenLayers uses the standard Mapbox GL vector tile path — the same format ESRI and MapLibre always use.
Allows the OpenLayers provider to support raster, WMS, standard vector tile, and OGC API - Tiles basemaps. When omitted, OpenLayers uses the standard Mapbox GL vector tile path.

- `'vector'` — standard Mapbox GL vector tile path. `url` should point to a Mapbox GL style document.
- `'raster'` — XYZ raster tile source. `url` should be a tile URL template with `{x}`, `{y}`, `{z}` placeholders.
- `'wms'` — WMS raster tile source. `url` should be the WMS service endpoint and `params` should provide the WMS request parameters.
- `'ogc-vt'` — OGC API - Tiles vector tile source. `url` should point to an OGC style endpoint that returns a Mapbox GL style document.

---
Expand Down Expand Up @@ -70,7 +72,7 @@ URL that returns a Mapbox GL style document (Mapbox Style Specification).
```

> [!NOTE]
> The **OpenLayers provider** supports two additional URL forms via the `type` property.
> The **OpenLayers provider** supports three additional URL forms via the `type` property.
>
> ```js
> // type 'ogc-vt' — OS NGD OGC API - Tiles, Outdoor (EPSG:27700)
Expand All @@ -84,10 +86,32 @@ URL that returns a Mapbox GL style document (Mapbox Style Specification).
> type: 'raster',
> url: 'https://api.os.uk/maps/raster/v1/zxy/Outdoor_27700/{z}/{x}/{y}.png?key=YOUR_API_KEY'
> }
>
> // type 'wms' — APGB aerial imagery via Getmapping WMS (EPSG:27700)
> {
> type: 'wms',
> url: 'https://www.getmapping.com/GmWMS/YOUR_MEMBER_GUID/ApgbBng.wmsx',
> params: { LAYERS: 'APGB_Latest_UK_125mm' }
> }
> ```

---

### `params`
**Type:** `Object`

WMS request parameters. Passed directly to the OpenLayers `TileWMS` source when `type` is `'wms'`. Most WMS GetMap requests should include `LAYERS`.

```js
{
type: 'wms',
url: 'https://www.getmapping.com/GmWMS/YOUR_MEMBER_GUID/ApgbBng.wmsx',
params: { LAYERS: 'APGB_Latest_UK_125mm' }
}
```

---

### `label`
**Type:** `string`

Expand Down
31 changes: 29 additions & 2 deletions providers/beta/openlayers/src/utils/tileLayers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XYZ from 'ol/source/XYZ.js'
import TileWMS from 'ol/source/TileWMS.js'
import VectorTileSource from 'ol/source/VectorTile.js'
import TileLayer from 'ol/layer/Tile.js'
import VectorTileLayer from 'ol/layer/VectorTile.js'
Expand All @@ -14,6 +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']

export function fetchWithTransform (url, resourceType, transformRequest) {
const result = transformRequest ? (transformRequest(url, resourceType) || {}) : {}
Expand All @@ -30,12 +32,28 @@ const createTileLoadFunction = (transformRequest) => (tile, src) => {
.catch(() => tile.setState(TileState.ERROR))
}

export function createTileSource (url, transformRequest) {
const tileGrid = new TileGrid({
function createTileGrid () {
return new TileGrid({
resolutions: TILE_GRID_RESOLUTIONS,
origin: TILE_GRID_ORIGIN,
tileSize: TILE_SIZE
})
}

export function createWMSTileSource (url, params, transformRequest) {
const tileGrid = createTileGrid()

return new TileWMS({
url,
params,
projection: CRS,
tileGrid,
tileLoadFunction: transformRequest ? createTileLoadFunction(transformRequest) : undefined
})
}

export function createTileSource (url, transformRequest) {
const tileGrid = createTileGrid()

const tileUrlFunction = ([z, x, y]) => url
.replace('{z}', z)
Expand All @@ -51,6 +69,15 @@ export function createTileSource (url, transformRequest) {
}

export async function createMapStyleLayer (mapStyle, transformRequest) {
if (mapStyle.type && !SUPPORTED_MAP_STYLE_TYPES.includes(mapStyle.type)) {
throw new Error(`Unsupported map style type: '${mapStyle.type}'`)
}

if (mapStyle.type === 'wms') {
const source = createWMSTileSource(mapStyle.url, mapStyle.params || {}, transformRequest)
return { layer: new TileLayer({ source }), source }
}

if (mapStyle.type === 'raster') {
const source = createTileSource(mapStyle.url, transformRequest)
return { layer: new TileLayer({ source }), source }
Expand Down
54 changes: 53 additions & 1 deletion providers/beta/openlayers/src/utils/tileLayers.test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import XYZ from 'ol/source/XYZ.js'
import TileWMS from 'ol/source/TileWMS.js'
import TileGrid from 'ol/tilegrid/TileGrid.js'
import TileLayer from 'ol/layer/Tile.js'
import VectorTileSource from 'ol/source/VectorTile.js'
import VectorTileLayer from 'ol/layer/VectorTile.js'
import { stylefunction } from 'ol-mapbox-style'
import { createTileSource, createMapStyleLayer, createVectorTileLayer } from './tileLayers.js'
import { createTileSource, createWMSTileSource, createMapStyleLayer, createVectorTileLayer } from './tileLayers.js'
import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from '../defaults.js'

const mockTileGridInstance = {}
const mockSourceInstance = {}
const mockWMSSourceInstance = {}
const mockTileLayerInstance = {}
const mockVectorTileSourceInstance = {}
const mockOGCVectorTileSourceInstance = { supportedMediaTypes: [] }
const mockVectorTileLayerInstance = {}
const mockMVTInstance = { supportedMediaTypes: [] }

jest.mock('ol/source/XYZ.js', () => ({ __esModule: true, default: jest.fn(() => mockSourceInstance) }))
jest.mock('ol/source/TileWMS.js', () => ({ __esModule: true, default: jest.fn(() => mockWMSSourceInstance) }))
jest.mock('ol/layer/Tile.js', () => ({ __esModule: true, default: jest.fn(() => mockTileLayerInstance) }))
jest.mock('ol/tilegrid/TileGrid.js', () => ({ __esModule: true, default: jest.fn(() => mockTileGridInstance) }))
jest.mock('ol/TileState.js', () => ({ __esModule: true, default: { ERROR: 'error' } }))
Expand Down Expand Up @@ -126,6 +129,43 @@ describe('createTileSource', () => {
})
})

describe('createWMSTileSource', () => {
const wmsParams = { LAYERS: 'MyLayer', FORMAT: 'image/jpeg' }

beforeEach(() => jest.clearAllMocks())

it('creates TileGrid with correct OS tile grid config', () => {
createWMSTileSource('https://wms.example.com/wms', wmsParams, null)
expect(TileGrid).toHaveBeenCalledWith({
resolutions: TILE_GRID_RESOLUTIONS,
origin: TILE_GRID_ORIGIN,
tileSize: TILE_SIZE
})
})

it('creates TileWMS source with EPSG:27700 projection and params', () => {
createWMSTileSource('https://wms.example.com/wms', wmsParams, null)
expect(TileWMS).toHaveBeenCalledWith(expect.objectContaining({
url: 'https://wms.example.com/wms',
params: wmsParams,
projection: 'EPSG:27700',
tileGrid: mockTileGridInstance
}))
})

it('does not set tileLoadFunction when transformRequest is null', () => {
createWMSTileSource('https://wms.example.com/wms', wmsParams, null)
const { tileLoadFunction } = TileWMS.mock.calls[0][0]
expect(tileLoadFunction).toBeUndefined()
})

it('sets tileLoadFunction when transformRequest is provided', () => {
createWMSTileSource('https://wms.example.com/wms', wmsParams, jest.fn())
const { tileLoadFunction } = TileWMS.mock.calls[0][0]
expect(typeof tileLoadFunction).toBe('function')
})
})

describe('tileLoadFunction (via transformRequest)', () => {
const url = 'https://tiles.example.com/7/3/5'
const mockImg = { src: null }
Expand Down Expand Up @@ -191,6 +231,13 @@ describe('createMapStyleLayer', () => {
global.fetch = makeVectorFetchMock()
})

it('creates a WMS tile layer and source when mapStyle.type is wms', async () => {
const params = { LAYERS: 'MyLayer' }
const result = await createMapStyleLayer({ url: 'https://wms.example.com/wms', type: 'wms', params }, null)
expect(TileLayer).toHaveBeenCalledWith({ source: mockWMSSourceInstance })
expect(result).toEqual({ layer: mockTileLayerInstance, source: mockWMSSourceInstance })
})

it('creates a raster tile layer and source when mapStyle.type is raster', async () => {
const result = await createMapStyleLayer({ url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }, null)
expect(TileLayer).toHaveBeenCalledWith({ source: mockSourceInstance })
Expand All @@ -210,6 +257,11 @@ describe('createMapStyleLayer', () => {
expect(result).toEqual({ layer: mockVectorTileLayerInstance, source: mockVectorTileSourceInstance })
})

it('throws when mapStyle.type is unsupported', async () => {
await expect(createMapStyleLayer({ url: styleUrl, type: 'unsupported' }, null))
.rejects.toThrow("Unsupported map style type: 'unsupported'")
})

it('creates a vector tile layer and source when mapStyle.type is missing', async () => {
const result = await createMapStyleLayer({ url: styleUrl }, null)
expect(VectorTileLayer).toHaveBeenCalledWith({ source: mockVectorTileSourceInstance, declutter: true })
Expand Down
15 changes: 12 additions & 3 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,18 +403,27 @@
* Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
* Injected as the `--map-overlay-foreground-color` CSS custom property.
*
* @property {'raster' | 'ogc-vt'} [type]
* @property {'vector' | 'raster' | 'wms' | 'ogc-vt'} [type]
* Tile layer type. Controls how the map provider constructs the basemap source. Omit (or leave
* undefined) for the default Mapbox GL vector tile path. The OpenLayers provider supports all
* three types, enabling raster, standard vector tile, and OGC vector tile basemaps:
* four types, enabling raster, WMS, standard vector tile, and OGC vector tile basemaps:
* - `'raster'` — XYZ raster tile source; `url` should be a tile URL template.
* - `'vector'` — Mapbox GL vector tile path; `url` should point to a Mapbox GL style document.
* - `'wms'` — WMS raster tile source; `url` should be the WMS service endpoint and `params`
* should provide the WMS request parameters.
* **Currently only supported by the OpenLayers provider.**
* - `'ogc-vt'` — OGC API - Tiles vector tile source; `url` should point to an OGC style endpoint
* that returns a Mapbox GL style document. **Currently only supported by the OpenLayers provider.**
*
* @property {string} url
* URL for the style. For the default vector tile path and `'ogc-vt'`, this should be a URL that
* returns a Mapbox GL style document (Mapbox Style Specification). For `'raster'`, this should
* be an XYZ tile URL template with `{x}`, `{y}`, `{z}` placeholders.
* be an XYZ tile URL template with `{x}`, `{y}`, `{z}` placeholders. For `'wms'`, this should
* be the WMS service endpoint (the GetMap URL).
*
* @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' }`.
*/

/**
Expand Down
1 change: 1 addition & 0 deletions webpack.dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default {
NGD_BLACK_AND_WHITE_URL_27700: JSON.stringify(process.env.NGD_BLACK_AND_WHITE_URL_27700),
// Aerial photography
AERIAL_URL: JSON.stringify(process.env.AERIAL_URL),
APGB_WMS_URL: JSON.stringify(process.env.APGB_WMS_URL),
// OS Maps API (27700 raster)
MAPS_OUTDOOR_URL: JSON.stringify(process.env.MAPS_OUTDOOR_URL),
MAPS_ROAD_URL: JSON.stringify(process.env.MAPS_ROAD_URL),
Expand Down
Loading