UNPKG

@advisr/v-mapbox

Version:

Mapbox with Vue 💚

2,134 lines (1,952 loc) • 55.8 kB
/*! * @advisr/v-mapbox v1.12.0 * (c) 2022 GeoSpoc Dev Team * @license MIT */ import Vue, { ref } from 'vue'; import promisify from 'map-promisified'; var layerEvents = [ 'mousedown', 'mouseup', 'click', 'dblclick', 'mousemove', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu', 'touchstart', 'touchend', 'touchcancel', ]; var withEventsMixin = Vue.extend({ methods: { /** * Emit Vue event with additional data * * @param {string} name EventName * @param {object} [data={}] Additional data */ $_emitEvent(name, data = {}) { this.$emit(name, { map: this.map, component: this, ...data, }); }, /** * Emit Vue event with Mapbox event as additional data * * @param {Record<string, any>} event - Event Payload * @param {Record<string, any>} data - Data to be added to event */ $_emitMapEvent(event, data = {}) { this.$_emitEvent(event.type, { mapboxEvent: event, ...data }); }, }, }); const mapboxSourceProps = { sourceId: { type: String, required: true, }, source: { type: [Object, String], default: undefined, }, }; const mapboxLayerStyleProps = { layerId: { type: String, required: true, }, layer: { type: Object, required: true, }, before: { type: String, default: undefined, }, }; const componentProps = { clearSource: { type: Boolean, default: true, }, replaceSource: { type: Boolean, default: false, }, replace: { type: Boolean, default: false, }, }; var layerMixin = Vue.extend({ mixins: [withEventsMixin], inject: ['mapbox', 'map'], props: { ...mapboxSourceProps, ...mapboxLayerStyleProps, ...componentProps, }, data() { return { initial: true, }; }, computed: { sourceLoaded() { return this.map ? this.map.isSourceLoaded(this.sourceId) : false; }, mapLayer() { return this.map ? this.map.getLayer(this.layerId) : null; }, mapSource() { return this.map ? this.map.getSource(this.sourceId) : null; }, }, watch: { before(layer, oldLayer) { if (layer !== oldLayer) { this.move(layer); } }, }, created() { if (this.layer.minzoom) { this.$watch('layer.minzoom', function (next) { if (this.initial) return; this.map.setLayerZoomRange(this.layerId, next, this.layer.maxzoom); }); } if (this.layer.maxzoom) { this.$watch('layer.maxzoom', function (next) { if (this.initial) return; this.map.setLayerZoomRange(this.layerId, this.layer.minzoom, next); }); } if (this.layer.paint) { this.$watch( 'layer.paint', function (next) { if (this.initial) return; if (next) { for (const prop of Object.keys(next)) { this.map.setPaintProperty(this.layerId, prop, next[prop]); } } }, { deep: true }, ); } if (this.layer.layout) { this.$watch( 'layer.layout', function (next) { if (this.initial) return; if (next) { for (const prop of Object.keys(next)) { this.map.setLayoutProperty(this.layerId, prop, next[prop]); } } }, { deep: true }, ); } if (this.layer.filter) { this.$watch( 'layer.filter', function (next) { if (this.initial) return; this.map.setFilter(this.layerId, next); }, { deep: true }, ); } }, beforeDestroy() { if (this.map && this.map.loaded()) { try { this.map.removeLayer(this.layerId); } catch (err) { this.$_emitEvent('layer-does-not-exist', { layerId: this.sourceId, error: err, }); } if (this.clearSource) { try { this.map.removeSource(this.sourceId); } catch (err) { this.$_emitEvent('source-does-not-exist', { sourceId: this.sourceId, error: err, }); } } } }, methods: { $_emitLayerMapEvent(event) { return this.$_emitMapEvent(event, { layerId: this.layerId }); }, $_bindLayerEvents(events) { Object.keys(this.$listeners).forEach((eventName) => { if (events.includes(eventName)) { this.map.on(eventName, this.layerId, this.$_emitLayerMapEvent); } }); }, $_unbindEvents(events) { if (this.map) { events.forEach((eventName) => { this.map.off(eventName, this.layerId, this.$_emitLayerMapEvent); }); } }, $_watchSourceLoading(data) { if (data.dataType === 'source' && data.sourceId === this.sourceId) { this.$_emitEvent('layer-source-loading', { sourceId: this.sourceId }); this.map.off('dataloading', this.$_watchSourceLoading); } }, move(beforeId) { this.map.moveLayer(this.layerId, beforeId); this.$_emitEvent('layer-moved', { layerId: this.layerId, beforeId: beforeId, }); }, remove() { this.map.removeLayer(this.layerId); this.map.removeSource(this.sourceId); this.$_emitEvent('layer-removed', { layerId: this.layerId }); this.$destroy(); }, }, render() {}, }); var CanvasLayer = Vue.extend({ name: 'MglCanvasLayer', mixins: [layerMixin], inject: ['mapbox', 'map'], props: { source: { type: Object, required: true, }, layer: { type: Object, default: null, }, }, computed: { canvasElement() { return this.mapSource ? this.mapSource.getCanvas() : null; }, }, watch: { coordinates(val) { if (this.initial) return; this.mapSource.setCoordinates(val); }, }, created() { this.$_deferredMount(); }, methods: { $_deferredMount() { const source = { type: 'canvas', ...this.source, }; this.map.on('dataloading', this.$_watchSourceLoading); try { this.map.addSource(this.sourceId, source); } catch (err) { if (this.replaceSource) { this.map.removeSource(this.sourceId); this.map.addSource(this.sourceId, source); } } this.$_addLayer(); this.$_bindLayerEvents(layerEvents); this.initial = false; }, $_addLayer() { const existed = this.map.getLayer(this.layerId); if (existed) { if (this.replace) { this.map.removeLayer(this.layerId); } else { this.$_emitEvent('layer-exists', { layerId: this.layerId }); return existed; } } const layer = { id: this.layerId, source: this.sourceId, type: 'raster', ...this.layer, }; this.map.addLayer(layer, this.before); this.$_emitEvent('added', { layerId: this.layerId, canvas: this.canvasElement, }); }, }, }); var GeojsonLayer = Vue.extend({ name: 'MglGeojsonLayer', mixins: [layerMixin], computed: { getSourceFeatures() { return (filter) => { if (this.map) { return this.map.querySourceFeatures(this.sourceId, { filter }); } return null; }; }, getRenderedFeatures() { return (geometry, filter) => { if (this.map) { return this.map.queryRenderedFeatures(geometry, { layers: [this.layerId], filter, }); } return null; }; }, getClusterExpansionZoom() { return (clusterId) => { return new Promise((resolve, reject) => { if (this.mapSource) { this.mapSource.getClusterExpansionZoom(clusterId, (err, zoom) => { if (err) { return reject(err); } return resolve(zoom); }); } else { return reject( new Error(`Map source with id ${this.sourceId} not found.`), ); } }); }; }, getClusterChildren() { return (clusterId) => { return new Promise((resolve, reject) => { const source = this.mapSource; if (source) { source.getClusterChildren(clusterId, (err, features) => { if (err) { return reject(err); } return resolve(features); }); } else { return reject( new Error(`Map source with id ${this.sourceId} not found.`), ); } }); }; }, getClusterLeaves() { return (...args) => { return new Promise((resolve, reject) => { if (this.mapSource) { this.mapSource.getClusterLeaves(...args, (err, features) => { if (err) { return reject(err); } return resolve(features); }); } else { return reject( new Error(`Map source with id ${this.sourceId} not found.`), ); } }); }; }, }, created() { if (this.source) { this.$watch( 'source.data', function (next) { if (this.initial) return; this.mapSource.setData(next); }, { deep: true }, ); } this.$_deferredMount(); }, methods: { $_deferredMount() { // this.map = payload.map; this.map.on('dataloading', this.$_watchSourceLoading); if (this.source) { const source = { type: 'geojson', ...this.source, }; try { this.map.addSource(this.sourceId, source); } catch (err) { if (this.replaceSource) { this.map.removeSource(this.sourceId); this.map.addSource(this.sourceId, source); } } } this.$_addLayer(); this.$_bindLayerEvents(layerEvents); this.map.off('dataloading', this.$_watchSourceLoading); this.initial = false; }, $_addLayer() { const existed = this.map.getLayer(this.layerId); if (existed) { if (this.replace) { this.map.removeLayer(this.layerId); } else { this.$_emitEvent('layer-exists', { layerId: this.layerId }); return existed; } } const layer = { id: this.layerId, source: this.sourceId, ...this.layer, }; this.map.addLayer(layer, this.before); this.$_emitEvent('added', { layerId: this.layerId }); }, setFeatureState(featureId, state) { if (this.map) { const params = { id: featureId, source: this.source }; return this.map.setFeatureState(params, state); } }, getFeatureState(featureId) { if (this.map) { const params = { id: featureId, source: this.source }; return this.map.getFeatureState(params); } }, removeFeatureState(featureId, sourceLayer, key) { if (this.map) { const params = { id: featureId, source: this.source, sourceLayer, }; return this.map.removeFeatureState(params, key); } }, }, }); var ImageLayer = Vue.extend({ name: 'MglImageLayer', mixins: [layerMixin], created() { if (this.source) { if (this.source.coordinates) { this.$watch( 'source.coordinates', function (next) { if (this.initial) return; if (next) { this.mapSource.setCoordinates(next); } }, { deep: true }, ); } if (this.source.url) { this.$watch( 'source.url', function (next) { if (this.initial) return; if (next) { this.mapSource.updateImage({ url: next, coordinates: this.source.coordinates, }); } }, { deep: true }, ); } } this.$_deferredMount(); }, methods: { $_deferredMount() { const source = { type: 'image', ...this.source, }; this.map.on('dataloading', this.$_watchSourceLoading); try { this.map.addSource(this.sourceId, source); } catch (err) { if (this.replaceSource) { this.map.removeSource(this.sourceId); this.map.addSource(this.sourceId, source); } } this.$_addLayer(); this.$_bindLayerEvents(layerEvents); this.initial = false; }, $_addLayer() { const existed = this.map.getLayer(this.layerId); if (existed) { if (this.replace) { this.map.removeLayer(this.layerId); } else { this.$_emitEvent('layer-exists', { layerId: this.layerId }); return existed; } } const layer = { id: this.layerId, source: this.sourceId, type: 'raster', ...this.layer, }; this.map.addLayer(layer, this.before); this.$_emitEvent('added', { layerId: this.layerId }); }, }, }); var RasterLayer = Vue.extend({ name: 'MglRasterLayer', mixins: [layerMixin], created() { this.$_deferredMount(); }, methods: { $_deferredMount() { const source = { type: 'raster', ...this.source, }; this.map.on('dataloading', this.$_watchSourceLoading); try { this.map.addSource(this.sourceId, source); } catch (err) { if (this.replaceSource) { this.map.removeSource(this.sourceId); this.map.addSource(this.sourceId, source); } } this.$_addLayer(); this.$_bindLayerEvents(layerEvents); this.map.off('dataloading', this.$_watchSourceLoading); this.initial = false; }, $_addLayer() { const existed = this.map.getLayer(this.layerId); if (existed) { if (this.replace) { this.map.removeLayer(this.layerId); } else { this.$_emitEvent('layer-exists', { layerId: this.layerId }); return existed; } } const layer = { id: this.layerId, type: 'raster', source: this.sourceId, ...this.layer, }; this.map.addLayer(layer, this.before); this.$_emitEvent('added', { layerId: this.layerId }); }, }, }); var VectorLayer = Vue.extend({ name: 'MglVectorLayer', mixins: [layerMixin], computed: { getSourceFeatures() { return (filter) => { if (this.map) { return this.map.querySourceFeatures(this.sourceId, { sourceLayer: this.layer['source-layer'], filter, }); } return null; }; }, getRenderedFeatures() { return (geometry, filter) => { if (this.map) { return this.map.queryRenderedFeatures(geometry, { layers: [this.layerId], filter, }); } return null; }; }, }, watch: { filter(filter) { if (this.initial) return; this.map.setFilter(this.layerId, filter); }, }, created() { this.$_deferredMount(); }, methods: { $_deferredMount() { const source = { type: 'vector', ...this.source, }; this.map.on('dataloading', this.$_watchSourceLoading); try { this.map.addSource(this.sourceId, source); } catch (err) { if (this.replaceSource) { this.map.removeSource(this.sourceId); this.map.addSource(this.sourceId, source); } } this.$_addLayer(); this.$_bindLayerEvents(layerEvents); this.map.off('dataloading', this.$_watchSourceLoading); this.initial = false; }, $_addLayer() { const existed = this.map.getLayer(this.layerId); if (existed) { if (this.replace) { this.map.removeLayer(this.layerId); } else { this.$_emitEvent('layer-exists', { layerId: this.layerId }); return existed; } } const layer = { id: this.layerId, source: this.sourceId, ...this.layer, }; this.map.addLayer(layer, this.before); this.$_emitEvent('added', { layerId: this.layerId }); }, setFeatureState(featureId, state) { if (this.map) { const params = { id: featureId, source: this.sourceId, 'source-layer': this.layer['source-layer'], }; return this.map.setFeatureState(params, state); } }, getFeatureState(featureId) { if (this.map) { const params = { id: featureId, source: this.source, 'source-layer': this.layer['source-layer'], }; return this.map.getFeatureState(params); } }, }, }); var VideoLayer = Vue.extend({ name: 'MglVideoLayer', mixins: [layerMixin], computed: { video() { return this.map.getSource(this.sourceId).getVideo(); }, }, created() { if (this.source && this.source.coordinates) { this.$watch('source.coordinates', function (next) { if (this.initial) return; this.mapSource.setCoordinates(next); }); } this.$_deferredMount(); }, methods: { $_deferredMount() { const source = { type: 'video', ...this.source, }; this.map.on('dataloading', this.$_watchSourceLoading); try { this.map.addSource(this.sourceId, source); } catch (err) { if (this.replaceSource) { this.map.removeSource(this.sourceId); this.map.addSource(this.sourceId, source); } } this.$_addLayer(); this.$_bindLayerEvents(layerEvents); this.initial = false; }, $_addLayer() { const existed = this.map.getLayer(this.layerId); if (existed) { if (this.replace) { this.map.removeLayer(this.layerId); } else { this.$_emitEvent('layer-exists', { layerId: this.layerId }); return existed; } } const layer = { id: this.layerId, source: this.sourceId, type: 'background', ...this.layer, }; this.map.addLayer(layer, this.before); this.$_emitEvent('added', { layerId: this.layerId }); }, }, }); var mapEvents = { boxzoomcancel: { name: 'boxzoomcancel' }, boxzoomend: { name: 'boxzoomcancel' }, boxzoomstart: { name: 'boxzoomstart' }, click: { name: 'click' }, contextmenu: { name: 'contextmenu' }, data: { name: 'data' }, dataloading: { name: 'dataloading' }, dblclick: { name: 'dblclick' }, drag: { name: 'drag' }, dragend: { name: 'dragend' }, dragstart: { name: 'dragstart' }, error: { name: 'error' }, idle: { name: 'idle' }, load: { name: 'load' }, mousedown: { name: 'mousedown' }, mouseenter: { name: 'mouseenter' }, mouseleave: { name: 'mouseleave' }, mousemove: { name: 'mousemove' }, mouseout: { name: 'mouseout' }, mouseover: { name: 'mouseover' }, mouseup: { name: 'mouseup' }, move: { name: 'move' }, moveend: { name: 'moveend' }, movestart: { name: 'movestart' }, pitch: { name: 'pitch' }, pitchend: { name: 'pitchend' }, pitchstart: { name: 'pitchstart' }, remove: { name: 'remove' }, render: { name: 'render' }, resize: { name: 'resize' }, rotate: { name: 'rotate' }, rotateend: { name: 'rotateend' }, rotatestart: { name: 'rotatestart' }, sourcedata: { name: 'sourcedata' }, sourcedataloading: { name: 'sourcedataloading' }, styledata: { name: 'styledata' }, styledataloading: { name: 'styledataloading' }, styleimagemissing: { name: 'styleimagemissing' }, touchcancel: { name: 'touchcancel' }, touchend: { name: 'touchend' }, touchmove: { name: 'touchmove' }, touchstart: { name: 'touchstart' }, webglcontextlost: { name: 'webglcontextlost' }, webglcontextrestored: { name: 'webglcontextrestored' }, wheel: { name: 'wheel' }, zoom: { name: 'zoom' }, zoomend: { name: 'zoomend' }, zoomstart: { name: 'zoomstart' }, }; const props = { antialias: { type: [Boolean, undefined] , default: false, required: false, }, attributionControl: { type: [Boolean, undefined] , default: true, required: false, }, bearing: { type: [Number, undefined] , default: 0, required: false, }, bearingSnap: { type: [Number, undefined] , default: 7, required: false, }, bounds: { type: [Object, Array, undefined] , default: undefined, required: false, }, boxZoom: { type: [Boolean, undefined] , default: true, required: false, }, center: { type: [Object, Array, undefined] , default: undefined, required: false, }, clickTolerance: { type: [Number, undefined] , default: 3, required: false, }, collectResourceTiming: { type: [Boolean, undefined] , default: false, required: false, }, crossSourceCollisions: { type: [Boolean, undefined] , default: true, required: false, }, container: { type: String , required: false, default: () => `map-${('' + Math.random()).split('.')[1]}`, }, customAttribution: { type: [String, Array, undefined] , default: null, required: false, }, dragPan: { type: [Boolean, undefined] , default: true, required: false, }, dragRotate: { type: [Boolean, undefined] , default: true, required: false, }, doubleClickZoom: { type: [Boolean, undefined] , default: true, required: false, }, hash: { type: [Boolean, String, undefined] , default: false, required: false, }, fadeDuration: { type: [Number, undefined] , default: 300, required: false, }, failIfMajorPerformanceCaveat: { type: [Boolean, undefined] , default: false, required: false, }, fitBoundsOptions: { type: [Object, undefined] , default: undefined, required: false, }, interactive: { type: [Boolean, undefined] , default: true, required: false, }, keyboard: { type: [Boolean, undefined] , default: true, required: false, }, locale: { type: [Object, undefined] , default: undefined, required: false, }, localIdeographFontFamily: { type: [String, undefined] , default: 'sans-serif', required: false, }, logoPosition: { type: [String, undefined] , default: 'bottom-left', validator: (val) => ['top-left', 'top-right', 'bottom-left', 'bottom-right'].includes(val), required: false, }, maxBounds: { type: [Array, Object, undefined] , default: undefined, required: false, }, maxPitch: { type: [Number, undefined] , default: 0, required: false, }, maxZoom: { type: [Number, undefined] , default: 22, required: false, }, minPitch: { type: [Number, undefined] , default: 0, required: false, }, minZoom: { type: [Number, undefined] , default: 0, required: false, }, preserveDrawingBuffer: { type: [Boolean, undefined] , default: false, required: false, }, pitch: { type: [Number, undefined] , default: 0, required: false, }, pitchWithRotate: { type: [Boolean, undefined] , default: true, required: false, }, refreshExpiredTiles: { type: [Boolean, undefined] , default: true, required: false, }, renderWorldCopies: { type: [Boolean, undefined] , default: true, required: false, }, scrollZoom: { type: [Boolean, undefined] , default: () => true, required: false, }, mapStyle: { type: [String, Object, undefined] , default: undefined, required: true, }, trackResize: { type: [Boolean, undefined] , default: true, required: false, }, transformRequest: { type: [Function, undefined] , default: undefined, required: false, }, touchZoomRotate: { type: [Boolean, undefined] , default: () => true, required: false, }, touchPitch: { type: [Boolean, undefined] , default: () => true, required: false, }, zoom: { type: [Number, undefined] , default: 0, required: false, }, maxTileCacheSize: { type: [Number, undefined] , default: null, required: false, }, accessToken: { type: [String, undefined] , default: undefined, required: false, }, /** * Reference(mapbox): https://docs.mapbox.com/mapbox-gl-js/example/mapbox-gl-rtl-text/ * Reference(v-mapbox): ./GlMap.vue#L89 */ RTLTextPluginUrl: { type: [String, undefined] , default: undefined, required: false, }, /** * Reference(mapbox): https://docs.mapbox.com/mapbox-gl-js/api/map/#map#setlight * Reference(v-mapbox): ./mixins/withWatchers.js#L43 */ light: { type: [Object, undefined] , default: undefined, required: false, }, /** * Reference(mapbox): https://docs.mapbox.com/mapbox-gl-js/api/map/#map#showtileboundaries * Reference(v-mapbox): ./mixins/withWatchers.js#L25 */ tileBoundaries: { type: [Boolean, undefined] , default: false, required: false, }, /** * Reference(mapbox): https://docs.mapbox.com/mapbox-gl-js/api/map/#map#showcollisionboxes * Reference(v-mapbox): ./mixins/withWatchers.js#L22 */ collisionBoxes: { type: [Boolean, undefined] , default: false, required: false, }, /** * Reference(mapbox): https://docs.mapbox.com/mapbox-gl-js/api/map/#map#repaint * Reference(v-mapbox): ./mixins/withWatchers.js#L28 */ repaint: { type: [Boolean, undefined] , default: false, required: false, }, }; var options = props; const watchers = { maxBounds(next) { this.map.setMaxBounds(next); }, minZoom(next) { this.map.setMinZoom(next); }, maxZoom(next) { this.map.setMaxZoom(next); }, minPitch(next) { this.map.setMinPitch(next); }, maxPitch(next) { this.map.setMaxPitch(next); }, mapStyle(next) { this.map.setStyle(next); }, // TODO: make 'bounds' synced prop // bounds (next) { this.map.fitBounds(next, { linear: true, duration: 0 }) }, collisionBoxes(next) { this.map.showCollisionBoxes = next; }, tileBoundaries(next) { this.map.showTileBoundaries = next; }, repaint(next) { this.map.repaint = next; }, zoom(next) { this.map.setZoom(next); }, center(next) { this.map.setCenter(next); }, bearing(next) { this.map.setBearing(next); }, pitch(next) { this.map.setPitch(next); }, light(next) { this.map.setLight(next); }, }; /** * @param {object} prop - property name * @param {Function} callback - callback function * @param {object} next - next value * @param {object} prev - previous value */ function watcher(prop, callback, next, prev) { if (this.initial) return; if (this.$listeners[`update:${prop}`]) { if (this.propsIsUpdating[prop]) { this._watcher.active = false; this.$nextTick(() => { this._watcher.active = true; }); } else { this._watcher.active = true; callback(next, prev); } this.propsIsUpdating[prop] = false; } else { callback(next, prev); } } /** * @returns {object} wrapper */ function makeWatchers() { const wrappers = {}; Object.entries(watchers).forEach((prop) => { wrappers[prop[0]] = function (next, prev) { return watcher.call(this, prop[0], prop[1].bind(this), next, prev); }; }); return wrappers; } var withWatchers = Vue.extend({ watch: makeWatchers(), }); var withPrivateMethods = Vue.extend({ setup() { const container = ref(); return { container }; }, methods: { $_updateSyncedPropsFabric(prop, data) { return () => { this.propsIsUpdating[prop] = true; const info = typeof data === 'function' ? data() : data; return this.$emit(`update:${prop}`, info); }; }, $_bindPropsUpdateEvents() { const syncedProps = [ { events: ['moveend'], prop: 'center', getter: this.map.getCenter.bind(this.map), }, { events: ['zoomend'], prop: 'zoom', getter: this.map.getZoom.bind(this.map), }, { events: ['rotate'], prop: 'bearing', getter: this.map.getBearing.bind(this.map), }, { events: ['pitch'], prop: 'pitch', getter: this.map.getPitch.bind(this.map), }, { events: ['moveend', 'zoomend', 'rotate', 'pitch'], prop: 'bounds', getter: () => { let newBounds = this.map.getBounds(); if (this.$props.bounds instanceof Array) { newBounds = newBounds.toArray(); } return newBounds; }, }, ]; syncedProps.forEach(({ events, prop, getter }) => { events.forEach((event) => { if (this.$listeners[`update:${prop}`]) { this.map.on(event, this.$_updateSyncedPropsFabric(prop, getter)); } }); }); }, $_loadMap() { return this.mapboxPromise.then((mapbox) => { this.mapbox = mapbox.default ? mapbox.default : mapbox; return new Promise((resolve) => { if (this.accessToken) this.mapbox.accessToken = this.accessToken; const map = new this.mapbox.Map({ ...this._props, container: this.container, style: this.mapStyle, }); map.on('load', () => resolve(map)); }); }); }, $_RTLTextPluginError(error) { this.$emit('rtl-plugin-error', { map: this.map, error: error }); }, $_bindMapEvents(events) { Object.keys(this.$listeners).forEach((eventName) => { if (events.includes(eventName)) { this.map.on(eventName, this.$_emitMapEvent); } }); }, $_unbindEvents(events) { events.forEach((eventName) => { this.map.off(eventName, this.$_emitMapEvent); }); }, }, }); var withAsyncActions = Vue.extend({ created() { this.actions = {}; }, methods: { $_registerAsyncActions(map) { this.actions = { ...promisify(map), stop() { this.map.stop(); const updatedProps = { pitch: this.map.getPitch(), zoom: this.map.getZoom(), bearing: this.map.getBearing(), center: this.map.getCenter(), }; Object.entries(updatedProps).forEach((prop) => { this.$_updateSyncedPropsFabric(prop[0], prop[1])(); }); return Promise.resolve(updatedProps); }, }; }, }, }); var script$2 = Vue.extend({ name: 'MglMap', mixins: [withWatchers, withAsyncActions, withPrivateMethods, withEventsMixin], provide() { const self = this; return { get mapbox() { return self.mapbox; }, get map() { return self.map; }, get actions() { return self.actions; }, }; }, props: { mapboxGl: { type: Object, default: null, }, ...options, }, emits: ['load', ...Object.keys(mapEvents)], data() { return { initial: true, initialized: false, }; }, computed: { loaded() { return this.map ? this.map.loaded() : false; }, version() { return this.map ? this.map.version : null; }, isStyleLoaded() { return this.map ? this.map.isStyleLoaded() : false; }, areTilesLoaded() { return this.map ? this.map.areTilesLoaded() : false; }, isMoving() { return this.map ? this.map.isMoving() : false; }, canvas() { return this.map ? this.map.getCanvas() : null; }, canvasContainer() { return this.map ? this.map.getCanvasContainer() : null; }, images() { return this.map ? this.map.listImages() : null; }, }, created() { this.map = null; this.propsIsUpdating = {}; this.mapboxPromise = this.mapboxGl ? Promise.resolve(this.mapboxGl) : import('mapbox-gl'); }, mounted() { this.$_loadMap().then((map) => { this.map = map; if ( this.RTLTextPluginUrl !== undefined && this.mapbox.getRTLTextPluginStatus() !== 'loaded' ) { this.mapbox.setRTLTextPlugin( this.RTLTextPluginUrl, this.$_RTLTextPluginError, ); } const eventNames = Object.keys(mapEvents); this.$_bindMapEvents(eventNames); this.$_registerAsyncActions(map); this.$_bindPropsUpdateEvents(); this.initial = false; this.initialized = true; this.$emit('load', { map, component: this }); }); }, beforeDestroy() { this.$nextTick(() => { if (this.map) this.map.remove(); }); }, }); function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { if (typeof shadowMode !== 'boolean') { createInjectorSSR = createInjector; createInjector = shadowMode; shadowMode = false; } // Vue.extend constructor export interop. const options = typeof script === 'function' ? script.options : script; // render functions if (template && template.render) { options.render = template.render; options.staticRenderFns = template.staticRenderFns; options._compiled = true; // functional template if (isFunctionalTemplate) { options.functional = true; } } // scopedId if (scopeId) { options._scopeId = scopeId; } let hook; if (moduleIdentifier) { // server build hook = function (context) { // 2.3 injection context = context || // cached call (this.$vnode && this.$vnode.ssrContext) || // stateful (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional // 2.2 with runInNewContext: true if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { context = __VUE_SSR_CONTEXT__; } // inject component styles if (style) { style.call(this, createInjectorSSR(context)); } // register component module identifier for async chunk inference if (context && context._registeredComponents) { context._registeredComponents.add(moduleIdentifier); } }; // used by ssr in case component is cached and beforeCreate // never gets called options._ssrRegister = hook; } else if (style) { hook = shadowMode ? function (context) { style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot)); } : function (context) { style.call(this, createInjector(context)); }; } if (hook) { if (options.functional) { // register for functional component in vue file const originalRender = options.render; options.render = function renderWithStyleInjection(h, context) { hook.call(context); return originalRender(h, context); }; } else { // inject component registration as beforeCreate hook const existing = options.beforeCreate; options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; } } return script; } /* script */ const __vue_script__$2 = script$2; /* template */ var __vue_render__$2 = function () { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c( "div", { staticClass: "mgl-map-wrapper" }, [_vm._m(0), _vm._v(" "), _vm.initialized ? _vm._t("default") : _vm._e()], 2 ) }; var __vue_staticRenderFns__$2 = [ function () { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c("div", { ref: "container", attrs: { id: _vm.container } }) }, ]; __vue_render__$2._withStripped = true; /* style */ const __vue_inject_styles__$2 = undefined; /* scoped */ const __vue_scope_id__$2 = undefined; /* module identifier */ const __vue_module_identifier__$2 = undefined; /* functional template */ const __vue_is_functional_template__$2 = false; /* style inject */ /* style inject SSR */ /* style inject shadow dom */ const __vue_component__$2 = /*#__PURE__*/normalizeComponent( { render: __vue_render__$2, staticRenderFns: __vue_staticRenderFns__$2 }, __vue_inject_styles__$2, __vue_script__$2, __vue_scope_id__$2, __vue_is_functional_template__$2, __vue_module_identifier__$2, false, undefined, undefined, undefined ); var GlMap = __vue_component__$2; var withSelfEventsMixin = Vue.extend({ methods: { $_emitSelfEvent(event, data = {}) { this.$_emitMapEvent(event, { control: this.control, ...data }); }, /** * Bind events for markers, popups and controls. * MapboxGL JS emits this events on popup or marker object, * so we treat them as 'self' events of these objects * * @param {object} events - events to bind * @param {any} emitter - object to bind events to */ $_bindSelfEvents(events, emitter) { Object.keys(this.$listeners).forEach((eventName) => { if (events.includes(eventName)) { emitter.on(eventName, this.$_emitSelfEvent); } }); }, $_unbindSelfEvents(events, emitter) { if (events.length === 0) return; if (!emitter) return; events.forEach((eventName) => { emitter.off(eventName, this.$_emitSelfEvent); }); }, }, }); // import withRegistration from "../../../lib/withRegistration"; var controlMixin = Vue.extend({ mixins: [withEventsMixin, withSelfEventsMixin], inject: ['mapbox', 'map', 'actions'], props: { position: { type: String, default: 'top-right', }, }, watch: { position() { this.$_removeControl(); this.$_addControl(); }, }, beforeDestroy() { this.$_removeControl(); }, methods: { $_addControl() { try { this.map.addControl(this.control, this.position); } catch (err) { this.$_emitEvent('error', { error: err }); return; } this.$_emitEvent('added', { control: this.control }); }, $_removeControl() { try { if (this.map && this.control) { this.map.removeControl(this.control); } } catch (err) { this.$_emitEvent('error', { error: err }); return; } this.$_emitEvent('removed', { control: this.control }); }, }, }); var AttributionControl = Vue.extend({ name: 'AttributionControl', mixins: [controlMixin], props: { compact: { type: Boolean, default: true, }, customAttribution: { type: [String, Array], default: undefined, }, }, created() { this.control = new this.mapbox.AttributionControl(this.$props); this.$_addControl(); }, render() {}, }); var FullscreenControl = Vue.extend({ name: 'FullscreenControl', mixins: [controlMixin], props: { container: { type: HTMLElement, default: undefined, }, }, created() { this.control = new this.mapbox.FullscreenControl(this.$props); this.$_addControl(); }, render() {}, }); const geolocationEvents = { trackuserlocationstart: 'trackuserlocationstart', trackuserlocationend: 'trackuserlocationend', geolocate: 'geolocate', error: 'error', }; var GeolocateControl = Vue.extend({ name: 'GeolocateControl', mixins: [withEventsMixin, withSelfEventsMixin, controlMixin], props: { positionOptions: { type: Object, default() { return { enableHighAccuracy: false, timeout: 6000, }; }, }, fitBoundsOptions: { type: Object, default: () => ({ maxZoom: 15 }), }, trackUserLocation: { type: Boolean, default: false, }, showAccuracyCircle: { type: Boolean, default: false, }, showUserLocation: { type: Boolean, default: true, }, }, created() { const GeolocateControl = this.mapbox.GeolocateControl; this.control = new GeolocateControl(this.$props); this.$_addControl(); this.$_bindSelfEvents(Object.keys(geolocationEvents), this.control); }, methods: { trigger() { if (this.control) { return this.control.trigger(); } }, }, render() {}, }); class SlotControl { constructor(options = {}, element) { this.options = options; this.element = element; } onAdd() { this.element.className = 'mapboxgl-ctrl mapboxgl-ctrl-group'; return this.element; } onRemove() { this.element.parentNode.removeChild(this.element); } } var IControl = Vue.extend({ name: 'IControl', mixins: [controlMixin], mounted() { this.control = new SlotControl(this.$attrs, this.$el); this.$_addControl(); }, render(createElement) { return createElement('div', this.$slots.default); }, }); var NavigationControl = Vue.extend({ name: 'NavigationControl', mixins: [controlMixin], props: { showCompass: { type: Boolean, default: true, }, showZoom: { type: Boolean, default: true, }, visualizePitch: { type: Boolean, default: true, }, }, created() { this.control = new this.mapbox.NavigationControl(this.$props); this.$_addControl(); }, render() {}, }); var ScaleControl = Vue.extend({ name: 'ScaleControl', mixins: [controlMixin], props: { maxWidth: { type: Number, default: 150, }, unit: { type: String, default: 'metric', validator(value) { return ['imperial', 'metric', 'nautical'].includes(value); }, }, }, watch: { unit(next, prev) { if (this.control && next !== prev) { this.control.setUnit(next); } }, }, created() { this.control = new this.mapbox.ScaleControl(this.$props); this.$_addControl(); }, render() {}, }); const markerEvents = { drag: 'drag', dragstart: 'dragstart', dragend: 'dragend', }; const markerDOMEvents = { click: 'click', mouseenter: 'mouseenter', mouseleave: 'mouseleave', }; var script$1 = Vue.extend({ name: 'MapMarker', mixins: [withEventsMixin, withSelfEventsMixin], inject: ['mapbox', 'map'], provide() { const self = this; return { get marker() { return self.marker; }, }; }, props: { // mapbox marker options offset: { type: [Object, Array], default: () => [0, -14], }, coordinates: { type: Array, required: true, }, color: { type: String, default: 'blue', }, anchor: { type: String, default: 'center', }, draggable: { type: Boolean, default: false, }, }, data() { return { initial: true, marker: undefined, }; }, watch: { coordinates(lngLat) { if (this.initial) return; this.marker.setLngLat(lngLat); }, draggable(next) { if (this.initial) return; this.marker.setDraggable(next); }, }, mounted() { const markerOptions = { ...this.$props, }; if (this.$slots.marker) { markerOptions.element = this.$slots.marker[0].elm; } this.marker = new this.mapbox.Marker(markerOptions); if (this.$listeners['update:coordinates']) { this.marker.on('dragend', (event) => { let newCoordinates; if (this.coordinates instanceof Array) { newCoordinates = [ event.target._lngLat.lng, event.target._lngLat.lat, ]; } else { newCoordinates = event.target._lngLat; } this.$emit('update:coordinates', newCoordinates); }); } const eventNames = Object.keys(markerEvents); this.$_bindSelfEvents(eventNames, this.marker); this.initial = false; this.$_addMarker(); }, beforeDestroy() { if (this.map !== undefined && this.marker !== undefined) { this.marker.remove(); } }, methods: { $_addMarker() { this.marker.setLngLat(this.coordinates).addTo(this.map); this.$_bindMarkerDOMEvents(); this.$_emitEvent('added', { marker: this.marker }); }, $_emitSelfEvent(event) { this.$_emitMapEvent(event, { marker: this.marker }); }, $_bindMarkerDOMEvents() { Object.keys(this.$listeners).forEach((key) => { if (Object.values(markerDOMEvents).includes(key)) { this.marker._element.addEventListener(key, (event) => { this.$_emitSelfEvent(event); }); } }); }, remove() { this.marker.remove(); this.$_emitEvent('removed'); }, togglePopup() { return this.marker.togglePopup(); }, }, }); /* script */ const __vue_script__$1 = script$1; /* template */ var __vue_render__$1 = function () { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c( "div", { staticClass: "mgl-hidden" }, [_vm._t("marker"), _vm._v(" "), _vm.marker ? _vm._t("default") : _vm._e()], 2 ) }; var __vue_staticRenderFns__$1 = []; __vue_render__$1._withStripped = true; /* style */ const __vue_inject_styles__$1 = undefined; /* scoped */ const __vue_scope_id__$1 = undefined; /* module identifier */ const __vue_module_identifier__$1 = undefined; /* functional template */ const __vue_is_functional_template__$1 = false; /* style inject */ /* style inject SSR */ /* style inject shadow dom */ const __vue_component__$1 = /*#__PURE__*/normalizeComponent( { render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 }, __vue_inject_styles__$1, __vue_script__$1, __vue_scope_id__$1, __vue_is_functional_template__$1, __vue_module_identifier__$1, false, undefined, undefined, undefined ); var Marker = __vue_component__$1; const popupEvents = { open: 'open', close: 'close', }; /** * Popup component. * * @see See [Mapbox Gl JS Popup](https://www.mapbox.com/mapbox-gl-js/api/#popup) */ var script = Vue.extend({ name: 'MglPopup', mixins: [withEventsMixin, withSelfEventsMixin], inject: { mapbox: { default: null, }, map: { default: null, }, marker: { default: null, }, }, props: { /** * Mapbox GL popup option. * Space-separated CSS class names to add to popup container */ className: { type: String, default: undefined, }, /** * If `true`, a close button will appear in the top right corner of the popup. * Mapbox GL popup option. */ closeButton: { type: Boolean, default: true, }, /** * Mapbox GL popup option. * If `true`, the popup will closed when the map is clicked. . */ closeOnClick: { type: Boolean, default: true, }, /** * Mapbox GL popup option. * If `true`, the popup will closed when the map moves. */ closeOnMove: { type: Boolean, default: false, }, /** * Mapbox GL popup option. * If `true`, the popup will try to focus the first focusable element inside the popup. */ focusAfterOpen: { type: Boolean, default: true, }, /** * Mapbox GL popup option. * A string indicating the popup's location relative to the coordinate set. * If unset the anchor will be dynamically set to ensure the popup falls within the map container with a preference for 'bottom' . * 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right' */ anchor: { validator(value) { let allowedValues = [ 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right', ]; return typeof value === 'string' && allowedValues.includes(value); }, default: undefined, }, /** * Mapbox GL popup option. * A pixel offset applied to the popup's location * a single number specifying a distance from the popup's location * a PointLike specifying a constant offset * an object of Points specifing an offset for each anchor position Negative offsets indicate left and up. */ offset: { type: [Number, Object, Array], default: () => [0, 0], }, coordinates: { type: Array, default: ()