hmpps-open-layers-map
Version:
A native Web Component for displaying maps using OpenLayers.
183 lines (182 loc) • 7.45 kB
JavaScript
import maplibreCss from 'maplibre-gl/dist/maplibre-gl.css?raw';
import { OLMapInstance } from './map/open-layers-map-instance';
import { MapLibreMapInstance } from './map/maplibre-map-instance';
import { setupOpenLayersMap } from './map/setup/setup-openlayers-map';
import { setupMapLibreMap } from './map/setup/setup-maplibre-map';
import { createMapDOM, createScopedStyle, getMapNonce } from './helpers/dom';
import config from './map/config';
import { createOpenLayersAdapter, createMapLibreAdapter } from './map/map-adapter';
import styles from '../styles/moj-map.raw.css?raw';
export class MojMap extends HTMLElement {
mapNonce = null;
adapter;
layers = new Map();
shadow;
featureOverlay;
geoJson = null;
mapInstance;
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
}
async connectedCallback() {
this.mapNonce = getMapNonce(this);
this.render();
this.geoJson = this.parseGeoJsonFromSlot();
await this.initialiseMap();
this.dispatchEvent(new CustomEvent('map:ready', {
detail: {
map: this.map,
geoJson: this.geoJson,
},
bubbles: true,
composed: true,
}));
}
get geojson() {
return this.geoJson;
}
get map() {
return this.mapInstance;
}
get olMapInstance() {
return this.mapInstance instanceof OLMapInstance ? this.mapInstance : null;
}
get maplibreMapInstance() {
return this.mapInstance instanceof MapLibreMapInstance ? this.mapInstance : null;
}
addLayer(layer, layerStateOptions) {
if (!this.adapter)
throw new Error('Map not ready');
if (this.layers.has(layer.id))
this.removeLayer(layer.id);
layer.attach(this.adapter, layerStateOptions);
this.layers.set(layer.id, layer);
return typeof layer.getNativeLayer === 'function' ? layer.getNativeLayer() : undefined;
}
removeLayer(id) {
if (!this.adapter)
return;
const layer = this.layers.get(id);
if (!layer)
return;
layer.detach(this.adapter);
this.layers.delete(id);
}
getLayer(id) {
return this.layers.get(id);
}
closeOverlay() {
this.featureOverlay?.close();
}
parseAttributes() {
const renderer = this.getAttribute('renderer') === 'maplibre' ? 'maplibre' : 'openlayers';
const tileType = this.getAttribute('tile-type');
const userTokenUrl = this.getAttribute('access-token-url');
const tileUrlAttr = this.getAttribute('tile-url');
const vectorUrlAttr = this.getAttribute('vector-url');
const apiKey = this.getAttribute('api-key') || undefined;
const tileUrl = tileUrlAttr && tileUrlAttr.trim() ? tileUrlAttr : config.tiles.urls.tileUrl;
const vectorUrl = vectorUrlAttr && vectorUrlAttr.trim() ? vectorUrlAttr : config.tiles.urls.vectorStyleUrl;
const tokenUrl = tileType === 'raster' ? userTokenUrl || config.tiles.defaultTokenUrl : userTokenUrl || 'none';
if ((tileType ?? 'vector') === 'vector') {
const hasKeyInUrl = /\bkey=/.test(vectorUrl);
if (!apiKey && !hasKeyInUrl) {
console.warn('[moj-map] No apiKey and vectorUrl has no key – will fall back to raster tiles.');
}
}
return {
renderer,
tileType,
tokenUrl,
usesInternalOverlays: this.hasAttribute('uses-internal-overlays'),
overlayTemplateId: this.getAttribute('overlay-template-id') || undefined,
tileUrl,
vectorUrl,
apiKey,
};
}
parseGeoJsonFromSlot() {
const script = this.querySelector('script[type="application/json"][slot="geojson-data"]');
if (script && script.textContent) {
try {
return JSON.parse(script.textContent);
}
catch (e) {
console.warn('Invalid GeoJSON passed to <moj-map>', e);
return null;
}
}
return null;
}
async initialiseMap() {
const options = this.parseAttributes();
const mapContainer = this.shadow.querySelector('#map');
if (options.renderer === 'maplibre') {
this.mapInstance = await setupMapLibreMap(mapContainer, options.vectorUrl, this.getControlOptions().enable3DBuildings ?? false, options.apiKey);
this.adapter = createMapLibreAdapter(this, this.mapInstance);
}
else {
const overlayEl = this.shadow.querySelector('.app-map__overlay') ?? null;
this.mapInstance = await setupOpenLayersMap(mapContainer, {
target: mapContainer,
tileType: options.tileType,
tokenUrl: options.tokenUrl,
tileUrl: options.tileUrl,
vectorUrl: options.vectorUrl,
apiKey: options.apiKey,
usesInternalOverlays: options.usesInternalOverlays,
overlayEl,
controls: this.getControlOptions(),
});
this.adapter = createOpenLayersAdapter(this, this.mapInstance);
const withOverlay = this.mapInstance;
this.featureOverlay = withOverlay.featureOverlay;
}
}
getControlOptions() {
const parseBool = (name) => this.hasAttribute(name) && this.getAttribute(name) !== 'false';
const rotateAttr = this.getAttribute('rotate-control');
let rotateOpt;
if (rotateAttr === 'false') {
rotateOpt = false;
}
else if (rotateAttr === 'auto-hide') {
rotateOpt = { autoHide: true };
}
else {
rotateOpt = { autoHide: false };
}
const explicitScale = this.getAttribute('scale-control');
const legacyScaleLine = this.hasAttribute('scale-line') && this.getAttribute('scale-line') !== 'false';
const scaleControl = explicitScale ?? (legacyScaleLine ? 'line' : undefined);
const locationDisplay = this.getAttribute('location-display') ?? undefined;
const locationDisplaySource = this.getAttribute('location-source') ?? undefined;
const zoomSlider = parseBool('zoom-slider');
const grabCursor = parseBool('grab-cursor');
this.classList.toggle('has-rotate-control', rotateOpt !== false);
this.classList.toggle('has-zoom-slider', zoomSlider);
this.classList.toggle('has-scale-control', !!scaleControl);
this.classList.toggle('has-location-dms', locationDisplay === 'dms');
return {
grabCursor,
rotate: rotateOpt,
zoomSlider,
scaleControl,
locationDisplay,
locationDisplaySource,
enable3DBuildings: parseBool('enable-3d-buildings'),
};
}
render() {
if (this.mapNonce === null) {
console.warn('Warning: No CSP nonce provided. Styles may not be applied correctly.');
return;
}
this.shadow.innerHTML = '';
this.shadow.appendChild(createScopedStyle(styles, this.mapNonce));
this.shadow.appendChild(createScopedStyle(maplibreCss, this.mapNonce));
this.shadow.appendChild(createMapDOM());
}
}
customElements.define('moj-map', MojMap);