UNPKG

hmpps-open-layers-map

Version:

A native Web Component for displaying maps using OpenLayers.

183 lines (182 loc) 7.45 kB
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);