ndf-elements
Version:
My collection of useful custom elements.
311 lines (260 loc) • 9.59 kB
JavaScript
/**
* Easily embed a map
*
* powered by Leaflet.js - optionally with a GeoJSON feed.
* Contains accessibility fixes and enhancements.
*
* @copyright © Nick Freear, 27-Nov-2021.
*
* @see ../demo/my-map.html
* @see https://codepen.io/nfreear/pen/KKeJKov
* @see https://leafletjs.com/
*
* @status beta
* @since 1.0.0
*/
import MyElement from '../MyElement.js';
const { fetch } = window;
const LEAFLET_CDN_LIBS = [
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
'https://unpkg.com/leaflet-i18n@0.3.3/Leaflet.i18n.js',
'https://unpkg.com/leaflet.translate@0.6.0/Leaflet.translate.js',
'https://unpkg.com/leaflet.a11y@0.6.0/Leaflet.a11y.js'
];
// Some defaults.
const DEF = {
// Greenwich Observatory, London, UK. (Was: Central London 51.505,-0.09)
lat: 51.476852,
long: -0.000500,
zoom: 13, // Was 14
// See: github:Leaflet/Leaflet/pull/8418 (Was: 'https://{s}.tile.op..')
osmTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
};
const TEMPLATE = `
<template>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<style>
.map-desc {
color: #333;
font: 1rem sans-serif;
max-width: 35rem;
margin: 0 auto;
padding: 0 .2rem;
}
#my-map {
border: 1px solid #ddd;
border-radius: .2rem;
height: 82vh; /* Was: 78 vh */
max-height: 100%; /* Was: 540px */
min-height: 180px;
}
.my-loading {
font-size: 1.3rem;
margin: 1.3rem;
}
.leaflet-marker-icon {
border: 1px solid var(--marker-color, navy);
border-radius: 5px;
}
.leaflet-container .leaflet-control-attribution {
background: rgba(255,255,255, 0.9);
}
a[ href *= "maptiler.com" ] {
background-image: url(https://www.klokantech.com/img/projects/maptiler-logo.svg);
background-position: right;
background-repeat: no-repeat;
background-size: contain;
display: inline-block;
padding-right: 1.1rem;
}
</style>
<div id="desc" class="map-desc"><slot><p> Description of the map. </p></slot></div>
<div id="my-map" role="region" aria-describedby="desc" part="map">
<p class="my-loading">Loading map…</p>
</div>
</template>
`;
export class MyMapElement extends MyElement {
static getTag () {
return 'my-map';
}
get latLng () { return this.getLatLong(this); }
get zoom () { return parseInt(this.getAttribute('zoom') || DEF.zoom); }
getLatLong (elem) {
const str = elem.getAttribute('latlng') || elem.dataset.latlng || `${DEF.lat}, ${DEF.long}`;
const parts = str.split(/, ?/) || [0, 0];
return this._leaflet.latLng(parseFloat(parts[0]), parseFloat(parts[1]));
// return [parseFloat(parts[0]), parseFloat(parts[1])];
}
/** The CSS selector to use to query for HTML `<marker>` elements.
* @default '[latlng]' - Any HTML element with a `latlng` attribute.
*/
get markerSelector () {
return this.getAttribute('marker-selector') || '[latlng]';
}
/** The name of a marker JS constructor available on the `L` global.
* @default 'Marker'
*/
get markerClass () {
return this.getAttribute('marker-class') || 'Marker';
}
/** Asynchronously get references to Leaflet and the map object.
* @return { L, map } (Promise)
*/
async getLeafletMap () {
await this._whenReady(() => this.$$.map && this.$$.L, 'getLeafletMap');
return { L: this.$$.L, map: this.$$.map };
}
async connectedCallback () {
this.$$ = {};
// const caption = this.getAttribute('caption') || 'A caption for the map.';
const geojson = this.getAttribute('geojson') || null; // GeoJSON URL is relative to the HTML page!
const attribution = null;
const ATTRS = { latLng: {}, zoom: this.zoom, geojson, tileUrl: this.tileUrl, attribution };
await this._initialize(ATTRS);
console.debug('<my-map>:', this.$$.L.version, this.$$, this);
}
async _initialize (attr) {
// .
this._attachLocalTemplate(TEMPLATE); // Was: await this.getTemplate('my-map');
const mapElem = this.shadowRoot.querySelector('#my-map');
// this.shadowRoot.querySelector('#caption').textContent = attr.caption;
const L = await this._importLeafletLibs();
if (L.translate) {
L.translate.load(this.lang);
}
const map = L.map(mapElem, { a11yPlugin: true }).setView(this.latLng, attr.zoom);
const tiles = L.tileLayer(attr.tileUrl, {
apiKey: this.apiKey,
attribution: this.attribution
}).addTo(map);
// Was: L.control.scale({ maxWidth: 200 }).addTo(map);
/* L.DomEvent.on(tiles, 'tileerror', (ev) => {
console.error('Tile error:', ev);
}); */
this.$$ = {
...attr, latLng: this.latLng, map, mapElem, tiles, L
};
if (attr.geojson) {
try {
await this.loadGeoJson(attr.geojson).then(res => res.addTo(map));
} catch (ex) {
if (ex instanceof SyntaxError) {
console.error('JSON Parsing Error -', ex);
} else {
console.error(ex);
}
}
}
this._queryAddChildMarkers();
const PATH = this.shadowRoot.querySelector('.leaflet-overlay-pane path');
PATH && PATH.setAttribute('part', 'path');
}
/** Load non-module Javascript for side-effects (Leaflet added to window).
*/
async _importLeafletLibs () {
await this._importJs(LEAFLET_CDN_LIBS);
const { L } = window;
await this._whenReady(() => L && L.Map, 'import Leaflet'); // Was: L && L.i18n && L.l10n,
this.$$.L = L;
return L;
}
get _leaflet () {
return this.$$.L;
}
get apiKey () {
const KEY = this.getAttribute('api-key') || null;
console.debug('my-map API key:', KEY);
return KEY;
}
get tileUrl () {
return this.getAttribute('tile-url') || this.getAttribute('tileUrl') || DEF.osmTileUrl;
}
get attribution () {
const YEAR = new Date().getFullYear();
const ATTR = this.getAttribute('attribution') || 'Map data © {OSM} contributors, Imagery © {MB}';
return ATTR
.replace('{OSM}', '<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>')
.replace('{MB}', '<a href="https://www.mapbox.com/">Mapbox</a>')
.replace('{TF}', '<a href="http://www.thunderforest.com/">Thunderforest</a>')
.replace('{MT}', '<a href="https://www.maptiler.com/copyright/" part="att mt">MapTiler</a>')
.replace('{NLS}', '<a href="https://maps.nls.uk/projects/api/" part="att nls">National Library of Scotland</a>')
.replace('{gb1in}', '<a href="https://maps.nls.uk/projects/api/#gb1inch" title="Great Britain, Ordnance Survey one-inch to the mile (1:63,360), \'Hills\' edition, 1885-1903">GB 1in</a>')
.replace('{OS}', '<a href="https://osdatahub.os.uk/legal/overview">Ordnance Survey</a>')
.replace('{OSC}', `Contains <a href="https://osdatahub.os.uk/legal/overview" title="Ordnance Survey">OS data</a> © Crown copyright and database rights ${YEAR}`)
;
}
/** Construct a marker object.
*/
marker (latLng, options = {}) {
return new this._leaflet[this.markerClass](latLng, options);
}
_queryAddChildMarkers () {
const markerElems = this.querySelectorAll(this.markerSelector) || [];
const markers = [...markerElems].map((markerEl, idx) => {
const latLng = this.getLatLong(markerEl);
const data = markerEl.dataset;
const alt = data.alt || markerEl.title || markerEl.textContent;
const popupContent = data.popup || markerEl.textContent;
const options = { alt, ...data };
const marker = this.marker(latLng, options).addTo(this.$$.map);
if (popupContent) {
marker.bindPopup(popupContent);
}
return { latLng, popupContent, options, marker };
});
console.debug('queryAddChildMarkers:', markers);
}
async loadGeoJson (geojson) {
const resp = await fetch(geojson); // './data/landmarks.geo.json');
const geoJsonFeatures = await resp.json();
console.debug('Features:', geoJsonFeatures);
// const template = document.getElementById('geojson');
// const geoJsonFeatures = JSON.parse(template.content.textContent);
let count = 1;
const res = this._leaflet.geoJSON(geoJsonFeatures,
{
pointToLayer: (feat, latLng) => this._svgMarker(feat, latLng, count++),
onEachFeature: (feature, layer) => {
// does this feature have a property named popupContent?
if (feature.properties && feature.properties.popupContent) {
layer.bindPopup(feature.properties.popupContent);
// Was: layer.on('popupopen', ev => this._accessibilityFixPopup(ev));
}
}
});
// .addTo(map)
return res;
}
_svgMarker (feature, latLng, count) {
const name = feature.properties.name || '[none]';
const SVG_ID = 'fa-map-pin'; // 'fa-location-dot';
console.debug(`Marker ${count}: ${name}`, latLng, feature);
return this._leaflet.marker(latLng, {
draggable: false, // Accessibility fix (outline) ?
icon: this._leaflet.divIcon({
className: 'my-icon',
html: `<button aria-label="${name}">
<svg viewBox="0 0 384 512" aria-hidden="true">
<title>${name}</title>
<use part="marker" href="#${SVG_ID}"/>
</svg>
</button>`
})
});
}
// DEPRECATED.
_markerIcon () {
const L = window.L;
return L.icon({
iconSize: [25, 41],
iconAnchor: [10, 41],
popupAnchor: [2, -40],
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png'
});
}
}
// End.