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);