hmpps-open-layers-map
Version:
A native Web Component for displaying maps using OpenLayers.
125 lines (124 loc) • 5.43 kB
JavaScript
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;
}