UNPKG

hmpps-open-layers-map

Version:

A native Web Component for displaying maps using OpenLayers.

125 lines (124 loc) 5.43 kB
import config from '../config'; import { OLMapInstance } from '../open-layers-map-instance'; import { OrdnanceSurveyImageTileLayer, isImageTileLayer } from '../layers/ordnance-survey-image'; import { OrdnanceSurveyVectorTileLayer } from '../layers/ordnance-survey-vector'; import { FeaturePointerInteraction, MapPointerInteraction } from '../interactions'; import FeatureOverlay from '../overlays/feature-overlay'; import { startTokenRefresh, fetchAccessToken } from '../token-refresh'; /** * Build an OS Vector style URL from provided options. * - Normalises trailing slashes * - Handles Cypress/localhost stubs * - Prevents duplicate keys * - Appends `?key=` when necessary * - Returns null if there is no usable URL */ export function resolveFinalStyleUrl(vectorUrlFromAttr, apiKeyFromAttr) { // If a URL was passed in, normalise and (optionally) append key if (vectorUrlFromAttr && vectorUrlFromAttr.trim() !== '') { const normalisedUrl = vectorUrlFromAttr.replace(/\/$/, ''); // Don’t append keys to localhost stubs (used in Cypress tests) if (/^https?:\/\/localhost(:\d+)?\//i.test(normalisedUrl)) { return normalisedUrl; } // Don’t duplicate keys if (/\bkey=/.test(normalisedUrl)) { return normalisedUrl; } // Append if we have an apiKey if (apiKeyFromAttr && apiKeyFromAttr.trim() !== '') { const url = new URL(normalisedUrl); url.searchParams.set('key', apiKeyFromAttr); return url.toString(); } // No key available — skip OS Vector (force raster fallback) return null; } // No URL provided — fall back to config base + apiKey (if present) if (apiKeyFromAttr && apiKeyFromAttr.trim() !== '') { const url = new URL(config.tiles.urls.vectorStyleUrl, window.location.origin); if (!url.searchParams.has('key')) url.searchParams.set('key', apiKeyFromAttr); return url.toString(); } return null; } export async function setupOpenLayersMap(mapContainer, options) { let rasterAccessToken = ''; let rasterTokenTtlSeconds = 0; try { if (options.tokenUrl.toLowerCase() !== 'none') { const tokenResponse = await fetchAccessToken(options.tokenUrl); rasterAccessToken = tokenResponse.token; rasterTokenTtlSeconds = tokenResponse.expiresIn; } } catch (err) { console.error('Failed to retrieve access token:', err); } const map = new OLMapInstance({ target: mapContainer, layers: [], controls: options.controls, }); // Decide which tile type to apply const appliedTileType = options.tileType || 'vector'; if (appliedTileType === 'vector') { const styleUrl = resolveFinalStyleUrl(options.vectorUrl, options.apiKey); if (!styleUrl) { console.warn('[moj-map] No vectorUrl/apiKey provided; using raster tiles.'); map.addLayer(new OrdnanceSurveyImageTileLayer(options.tileUrl, rasterAccessToken)); } else { const vectorLayer = new OrdnanceSurveyVectorTileLayer(); try { await vectorLayer.applyVectorStyle(styleUrl); map.addLayer(vectorLayer); } catch (err) { // For localhost Cypress stubs, keep the vector layer attached so tests relying on it can proceed. if (/^https?:\/\/localhost(:\d+)?\//i.test(styleUrl)) { console.warn('[moj-map] applyVectorStyle failed on localhost; keeping vector layer for tests.', err); map.addLayer(vectorLayer); } else { console.warn('[moj-map] Failed to initialise vector layer. Falling back to image tiles.', err); map.addLayer(new OrdnanceSurveyImageTileLayer(options.tileUrl, rasterAccessToken)); } } } } else { const rasterLayer = new OrdnanceSurveyImageTileLayer(options.tileUrl, rasterAccessToken); map.addLayer(rasterLayer); if (rasterAccessToken && rasterTokenTtlSeconds && isImageTileLayer(rasterLayer)) { startTokenRefresh({ tokenUrl: options.tokenUrl, initialExpiresIn: rasterTokenTtlSeconds, onTokenUpdate: newToken => rasterLayer.updateToken(newToken), }); } } if (options.usesInternalOverlays && options.overlayEl instanceof HTMLElement) { const featureOverlay = new FeatureOverlay(options.overlayEl); map.addOverlay(featureOverlay); map.addInteraction(new FeaturePointerInteraction(featureOverlay)); const mapWithOverlay = map; mapWithOverlay.featureOverlay = featureOverlay; } if (options.controls?.grabCursor !== false) { map.addInteraction(new MapPointerInteraction()); const viewportEl = map.getViewport(); viewportEl.style.cursor = 'grab'; viewportEl.addEventListener('pointerdown', () => { viewportEl.style.cursor = 'grabbing'; }); viewportEl.addEventListener('pointerup', () => { viewportEl.style.cursor = 'grab'; }); viewportEl.addEventListener('pointerleave', () => { viewportEl.style.cursor = 'grab'; }); } return map; }