import mapboxgl from "mapbox-gl"; var commandRegistry = {}; export function registerCustomElement(settings) { const options = Object.assign( { onMount() {} }, settings ); if (options.token) { mapboxgl.accessToken = options.token; } window.customElements.define( "elm-mapbox-map", class MapboxMap extends HTMLElement { constructor() { super(); this._refreshExpiredTiles = true; this._renderWorldCopies = true; this.interactive = true; this._eventRegistrationQueue = {}; this._eventListenerMap = new Map(); } get mapboxStyle() { return this._style; } set mapboxStyle(value) { if (this._map) this._map.setStyle(value); this._style = value; } get minZoom() { return this._minZoom; } set minZoom(value) { if (this._map) this._map.setMinZoom(value); this._minZoom = value; } get maxZoom() { return this._maxZoom; } set maxZoom(value) { if (this._map) this._map.setMaxZoom(value); this._maxZoom = value; } get map() { return this._map; } get maxBounds() { return this._maxBounds; } set maxBounds(value) { if (this._map) this._map.setMaxBounds(value); this._maxBounds = value; } get renderWorldCopies() { return this._renderWorldCopies; } set renderWorldCopies(value) { if (this._map) this._map.setRenderWorldCopies(value); this._renderWorldCopies = value; } get center() { return this._center; } set center(value) { if (this._map) this._map.setCenter(value); this._center = value; } get zoom() { return this._zoom; } set zoom(value) { if (this._map) this._map.setZoom(value); this._zoom = value; } get bearing() { return this._bearing; } set bearing(value) { if (this._map) this._map.setBearing(value); this._bearing = value; } get pitch() { return this._pitch; } set pitch(value) { if (this._map) this._map.setPitch(value); this._pitch = value; } get featureState() { return this._featureState; } set featureState(value) { // TODO: Clean this up function makeId({ id, source, sourceLayer }) { return `${id}::${source}::${sourceLayer}`; } if (this._map) { const map = new Map( this._featureState.map(([feature, state]) => [ makeId(feature), { feature, state } ]) ); value.forEach(([feature, state]) => { const id = makeId(feature); if (map.has(id)) { const prevValue = map.get(id).state; const keys = Object.keys(prevValue); let newValue = {}; keys.forEach(k => { if (state[k] === undefined) { newValue[k] = undefined; } }); this._map.setFeatureState( feature, Object.assign(newValue, state) ); } else { this._map.setFeatureState(feature, state); } map.delete(id); }); map.forEach(({ feature, state }) => { const keys = Object.keys(state); let newValue = {}; keys.forEach(k => { newValue[k] = undefined; }); this._map.setFeatureState(feature, newValue); }); } this._featureState = value; } addEventListener(type, fn, ...args) { if (this._map) { const wrapped = e => fn( new Proxy(e, { has: (obj, prop) => prop in obj || (prop === "features" && obj.point) || (prop === "perPointFeatures" && obj.points) || ( (prop.slice(0, 2) === "is" || prop.slice(0, 3) === "get") && prop in this._map && typeof this._map[prop] === "function" ) , get: (obj, prop) => { if (prop in obj) { return obj[prop]; } else if (prop === "features" && obj.point) { return this._map.queryRenderedFeatures(obj.point, { layers: this.eventFeaturesLayers, filter: this.eventFeaturesFilter }); } else if (prop === "perPointFeatures" && obj.points) { return obj.points.map(point => this._map.queryRenderedFeatures(point, { layers: this.eventFeaturesLayers, filter: this.eventFeaturesFilter }) ); } else if ( (prop.slice(0, 2) === "is" || prop.slice(0, 3) === "get") && prop in this._map && typeof this._map[prop] === "function" ) { try { return this._map[prop](); } catch (_) { return undefined; } } return undefined; } }) ); this._eventListenerMap.set(fn, wrapped); return this._map.on(type, wrapped); } else { this._eventRegistrationQueue[type] = this._eventRegistrationQueue[type] || []; return this._eventRegistrationQueue[type].push(fn); } } removeEventListener(type, fn, ...args) { if (this._map) { const wrapped = this._eventListenerMap.get(fn); this._eventListenerMap.delete(fn); return this._map.off(type, wrapped); } else { const queue = this._eventRegistrationQueue[type] || []; const index = queue.findIndex(fn); if (index >= 0) { queue.splice(index, 1); } return; } } _createMapInstance() { let mapOptions = { container: this, style: this._style, minZoom: this._minZoom || 0, maxZoom: this._maxZoom || 22, interactive: this.interactive, attributionControl: false, logoPosition: this.logoPosition || "bottom-left", refreshExpiredTiles: this._refreshExpiredTiles, maxBounds: this._maxBounds, renderWorldCopies: this._renderWorldCopies }; if (this._center) { mapOptions.center = this._center; } if (this._zoom) { mapOptions.zoom = this._zoom; } if (this._bearing) { mapOptions.bearing = this._bearing; } if (this._pitch) { mapOptions.pitch = this._pitch; } this._map = new mapboxgl.Map(mapOptions); Object.entries(this._eventRegistrationQueue).forEach( ([type, listeners]) => { listeners.forEach(listener => { this.addEventListener(type, listener); }); } ); this._eventRegistrationQueue = {}; options.onMount(this._map, this); if (commandRegistry[this.id]) { this._map.on("load", () => { var cmd; while ((cmd = commandRegistry[this.id].shift())) { cmd(this._map); } }); } return this._map; } connectedCallback() { if (this.token) { mapboxgl.accessToken = this.token; } this.style.display = "block"; this.style.width = "100%"; this.style.height = "100%"; this._upgradeProperty("mapboxStyle"); this._upgradeProperty("minZoom"); this._upgradeProperty("maxZoom"); this._upgradeProperty("maxBounds"); this._upgradeProperty("renderWorldCopies"); this._upgradeProperty("center"); this._upgradeProperty("zoom"); this._upgradeProperty("bearing"); this._upgradeProperty("pitch"); this._upgradeProperty("featureState"); this._map = this._createMapInstance(); } _upgradeProperty(prop) { if (this.hasOwnProperty(prop)) { let value = this[prop]; delete this[prop]; this[prop] = value; } } disconnectedCallback() { this._map.remove(); delete this._map; } } ); } export function registerPorts(elmApp, settings = {}) { const options = Object.assign( { outgoingPort: "elmMapboxOutgoing", incomingPort: "elmMapboxIncoming", easingFunctions: { linear: t => t } }, settings ); if (elmApp.ports && elmApp.ports[options.outgoingPort]) { function processOptions(opts) { if (opts.easing) { return Object.assign({}, opts, { easing: options.easingFunctions[opts.easing] }); } return opts; } function waitForMap(target, cb) { const el = document.getElementById(target); if (el) { cb(el.map); } else { var queue = commandRegistry[target]; if (!queue) queue = commandRegistry[target] = []; queue.push(cb); } } elmApp.ports[options.outgoingPort].subscribe(event => { waitForMap(event.target, function(map) { switch (event.command) { case "resize": return map.resize(); case "fitBounds": return map.fitBounds(event.bounds, processOptions(event.options)); case "panBy": return map.panBy(event.offset, processOptions(event.options)); case "panTo": return map.panTo(event.location, processOptions(event.options)); case "zoomTo": return map.zoomTo(event.zoom, processOptions(event.options)); case "zoomIn": return map.zoomIn(processOptions(event.options)); case "zoomOut": return map.zoomOut(processOptions(event.options)); case "rotateTo": return map.rotateTo(event.bearing, processOptions(event.options)); case "jumpTo": return map.jumpTo(processOptions(event.options)); case "easeTo": return map.easeTo(processOptions(event.options)); case "flyTo": return map.flyTo(processOptions(event.options)); case "stop": return map.stop(); case "setRTLTextPlugin": return map.setRTLTextPlugin(event.url); case "getBounds": return elmApp.ports[options.incomingPort].send({ type: "getBounds", id: event.requestId, bounds: map.getBounds().toArray() }); case "queryRenderedFeatures": return elmApp.ports[options.incomingPort].send({ type: "queryRenderedFeatures", id: event.requestId, features: event.query ? map.queryRenderedFeatures(processOptions(event.options)) : map.queryRenderedFeatures( event.query, processOptions(event.options) ) }); } }); }); } else { console.warn( `Expected Elm App to expose ${ options.outgoingPort } port. Please add https://github.com/gampleman/elm-mapbox/blob/master/examples/MapCommands.elm to your project and import it from your Main file.` ); } return elmApp; } export const supported = mapboxgl.supported;