UNPKG

tchen-vuelayers

Version:

Web map Vue components with the power of OpenLayers

1,081 lines (939 loc) 27.9 kB
/** * VueLayers * Web map Vue components with the power of OpenLayers * * @package vuelayers * @author Vladimir Vershinin <ghettovoice@gmail.com> * @version 0.11.1 * @license MIT * @copyright (c) 2017-2019, Vladimir Vershinin <ghettovoice@gmail.com> */ import _Object$defineProperties from '@babel/runtime-corejs2/core-js/object/define-properties'; import _typeof from '@babel/runtime-corejs2/helpers/esm/typeof'; import _objectSpread from '@babel/runtime-corejs2/helpers/esm/objectSpread'; import _Promise from '@babel/runtime-corejs2/core-js/promise'; import _Object$values from '@babel/runtime-corejs2/core-js/object/values'; import { defaults } from 'ol/control'; import { defaults as defaults$1 } from 'ol/interaction'; import VectorLayer from 'ol/layer/Vector'; import Collection from 'ol/Collection'; import Map from 'ol/Map'; import WebGLMap from 'ol/WebGLMap'; import VectorSource from 'ol/source/Vector'; import View from 'ol/View'; import { merge } from 'rxjs/_esm5/internal/observable/merge'; import { distinctUntilChanged } from 'rxjs/_esm5/internal/operators/distinctUntilChanged'; import { map } from 'rxjs/_esm5/internal/operators/map'; import { throttleTime } from 'rxjs/_esm5/internal/operators/throttleTime'; import Vue from 'vue'; import { olCmp, overlaysContainer, layersContainer, interactionsContainer, featuresContainer, projTransforms as projTransforms$1 } from '../mixin'; import projTransforms from '../mixin/proj-transforms'; import { RENDERER_TYPE, getFeatureId, IndexedCollectionAdapter, EPSG_3857, MAX_ZOOM, MIN_ZOOM, ZOOM_FACTOR } from '../ol-ext'; import { observableFromOlEvent, observableFromOlChangeEvent } from '../rx-ext'; import { hasMap, hasView } from '../util/assert'; import { isEqual, coalesce, isFunction, isPlainObject, noop, pick } from '../util/minilo'; import mergeDescriptors from '../util/multi-merge-descriptors'; import { makeWatchers } from '../util/vue-helpers'; import _toConsumableArray from '@babel/runtime-corejs2/helpers/esm/toConsumableArray'; import _Array$isArray from '@babel/runtime-corejs2/core-js/array/is-array'; import { distinctUntilKeyChanged } from 'rxjs/_esm5/internal/operators/distinctUntilKeyChanged'; import _Object$assign from '@babel/runtime-corejs2/core-js/object/assign'; var props = { /** * Options for default controls added to the map by default. Set to `false` to disable all map controls. Object * value is used to configure controls. * @type {Object|boolean} * @todo remove when vl-control-* components will be ready */ controls: { type: [Object, Boolean], default: true }, /** * The element to listen to keyboard events on. For example, if this option is set to `document` the keyboard * interactions will always trigger. If this option is not specified, the element the library listens to keyboard * events on is the component root element. * @type {string|Element|Document} */ keyboardEventTarget: [String, Element, Document], /** * When set to `true`, tiles will be loaded during animations. * @type {boolean} */ loadTilesWhileAnimating: { type: Boolean, default: false }, /** * When set to `true`, tiles will be loaded while interacting with the map. * @type {boolean} */ loadTilesWhileInteracting: { type: Boolean, default: false }, /** * The minimum distance in pixels the cursor must move to be detected as a map move event instead of a click. * Increasing this value can make it easier to click on the map. * @type {Number} */ moveTolerance: { type: Number, default: 1 }, /** * The ratio between physical pixels and device-independent pixels (dips) on the device. * @type {number} */ pixelRatio: { type: Number, default: function _default() { return window.devicePixelRatio || 1; } }, /** * Renderer. By default, **Canvas** and **WebGL** renderers are tested for support in that order, * and the first supported used. **Note** that the **Canvas** renderer fully supports vector data, * but **WebGL** can only render **Point** geometries. * @type {string|string[]} * @default ['canvas', 'webgl'] */ renderer: { type: String, default: RENDERER_TYPE.CANVAS, validator: function validator(value) { return _Object$values(RENDERER_TYPE).includes(value); } }, /** * Root element `tabindex` attribute value. Value should be provided to allow keyboard events on map. * @type {number|string} */ tabindex: [String, Number], /** * Projection for input/output coordinates in plain data. * @type {string} */ dataProjection: String, wrapX: { type: Boolean, default: true } }; var computed = { mapCtor: function mapCtor() { switch (this.renderer) { case RENDERER_TYPE.WEBGL: return WebGLMap; case RENDERER_TYPE.CANVAS: default: return Map; } } }; var methods = { /** * @return {Map} * @protected */ createOlObject: function createOlObject() { /* eslint-disable-next-line new-cap */ var map$$1 = new this.mapCtor({ loadTilesWhileAnimating: this.loadTilesWhileAnimating, loadTilesWhileInteracting: this.loadTilesWhileInteracting, pixelRatio: this.pixelRatio, moveTolerance: this.moveTolerance, keyboardEventTarget: this.keyboardEventTarget, controls: this._controls, interactions: this._interactions, layers: this._layers, overlays: this._overlays, view: this._view }); map$$1.set('dataProjection', this.dataProjection); this._defaultOverlay.setMap(map$$1); return map$$1; }, /** * @return {IndexedCollectionAdapter} * @protected */ getLayersTarget: function getLayersTarget() { hasMap(this); if (this._layersTarget == null) { this._layersTarget = new IndexedCollectionAdapter(this.$map.getLayers(), function (layer) { return layer.get('id'); }); } return this._layersTarget; }, /** * @return {IndexedCollectionAdapter} * @protected */ getInteractionsTarget: function getInteractionsTarget() { hasMap(this); if (this._interactionsTarget == null) { this._interactionsTarget = new IndexedCollectionAdapter(this.$map.getInteractions(), function (interaction) { return interaction.get('id'); }); } return this._interactionsTarget; }, /** * @return {function} * @protected */ getDefaultInteractionsSorter: function getDefaultInteractionsSorter() { // sort interactions by priority in asc order // the higher the priority, the earlier the interaction handles the event return function (a, b) { var ap = a.get('priority') || 0; var bp = b.get('priority') || 0; return ap === bp ? 0 : ap - bp; }; }, /** * @return {SourceCollectionAdapter} * @protected */ getFeaturesTarget: function getFeaturesTarget() { if (this._featuresTarget == null) { this._featuresTarget = new IndexedCollectionAdapter(this._defaultOverlayFeatures, function (feature) { return getFeatureId(feature); }); } return this._featuresTarget; }, /** * @return {IndexedCollectionAdapter} * @protected */ getOverlaysTarget: function getOverlaysTarget() { hasMap(this); if (this._overlaysTarget == null) { this._overlaysTarget = new IndexedCollectionAdapter(this.$map.getOverlays(), function (overlay) { return overlay.getId(); }); } return this._overlaysTarget; }, /** * @param {number[]} pixel * @return {number[]} Coordinates in the map view projection. */ getCoordinateFromPixel: function getCoordinateFromPixel(pixel) { hasMap(this); var coordinate = this.$map.getCoordinateFromPixel(pixel); return this.pointToDataProj(coordinate); }, /** * @returns {Object} * @protected */ getServices: function getServices() { var vm = this; return mergeDescriptors(olCmp.methods.getServices.call(this), layersContainer.methods.getServices.call(this), interactionsContainer.methods.getServices.call(this), overlaysContainer.methods.getServices.call(this), featuresContainer.methods.getServices.call(this), { get map() { return vm.$map; }, get view() { return vm.$view; }, get viewContainer() { return vm; } }); }, /** * Triggers focus on map container. * @return {void} */ focus: function focus() { this.$el.focus(); }, /** * @param {number[]} pixel * @param {function((Feature), ?Layer): *} callback * @param {Object} [opts] * @return {*|undefined} */ forEachFeatureAtPixel: function forEachFeatureAtPixel(pixel, callback) { var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; hasMap(this); return this.$map.forEachFeatureAtPixel(pixel, callback, opts); }, /** * @param {number[]} pixel * @param {function(Layer, ?(number[]|Uint8Array)): *} callback * @param {function(Layer): boolean} [layerFilter] * @return {*|undefined} */ forEachLayerAtPixel: function forEachLayerAtPixel(pixel, callback, layerFilter) { hasMap(this); return this.$map.forEachLayerAtPixel(pixel, callback, undefined, layerFilter); }, /** * @param {View|Vue|undefined} view * @return {void} * @protected */ setView: function setView(view) { view = view instanceof Vue ? view.$view : view; view || (view = new View()); if (view !== this._view) { this._view = view; } if (this.$map && view !== this.$map.getView()) { this.$map.setView(view); } }, /** * @return {void} * @protected */ mount: function mount() { hasMap(this); this.$map.setTarget(this.$el); this.subscribeAll(); this.updateSize(); }, /** * @return {void} * @protected */ unmount: function unmount() { hasMap(this); this.clearLayers(); this.clearInteractions(); this.clearOverlays(); this.unsubscribeAll(); this.$map.setTarget(undefined); }, /** * Updates map size and re-renders map. * @return {Promise} */ refresh: function refresh() { var _this = this; this.updateSize(); return this.render().then(function () { return olCmp.methods.refresh.call(_this); }); }, /** * @return {Promise} */ render: function render() { var _this2 = this; return new _Promise(function (resolve) { hasMap(_this2); _this2.$map.once('postrender', function () { return resolve(); }); _this2.$map.render(); }); }, /** * @return {void} * @protected */ subscribeAll: function subscribeAll() { subscribeToMapEvents.call(this); }, /** * Updates map size. * @return {void} */ updateSize: function updateSize() { hasMap(this); this.$map.updateSize(); } }; var watch = _objectSpread({}, makeWatchers(['keyboardEventTarget', 'loadTilesWhileAnimating', 'loadTilesWhileInteracting', 'moveTolerance', 'pixelRatio', 'renderer'], function () { return olCmp.methods.scheduleRecreate; }), { controls: function controls(value) { if (value === false) { this._controls.clear(); return; } value = _typeof(value) === 'object' ? value : undefined; this._controls.clear(); this._controls.extend(defaults(value).getArray()); }, wrapX: function wrapX(value) { if (this._defaultOverlay != null) { var source = new VectorSource({ features: this._defaultOverlayFeatures, wrapX: value }); this._defaultOverlay.setSource(source); } }, dataProjection: function dataProjection(value) { if (this.$map) { this.$map.set('dataProjection', value); this.scheduleRefresh(); } } }); /** * Container for **layers**, **interactions**, **controls** and **overlays**. It responsible for viewport * rendering and low level interaction events. * * @title vl-map * @alias module:map/map * @vueProto * * @fires module:map/map#click * @fires module:map/map#dblclick * @fires module:map/map#singleclick * @fires module:map/map#pointerdrag * @fires module:map/map#pointermove * @fires module:map/map#movestart * @fires module:map/map#moveend * @fires module:map/map#postrender * @fires module:map/map#precompose * @fires module:map/map#postcompose * * @vueSlot default Default slot for all child components. */ var script = { name: 'vl-map', mixins: [olCmp, layersContainer, interactionsContainer, overlaysContainer, featuresContainer, projTransforms], props: props, computed: computed, methods: methods, watch: watch, created: function created() { var _this3 = this; this._view = new View(); this._controls = this.controls !== false ? defaults(_typeof(this.controls) === 'object' ? this.controls : undefined) : new Collection(); this._interactions = defaults$1(); this._layers = new Collection(); this._overlays = new Collection(); // prepare default overlay this._defaultOverlayFeatures = new Collection(); this._defaultOverlay = new VectorLayer({ source: new VectorSource({ features: this._defaultOverlayFeatures, wrapX: this.wrapX }) }); _Object$defineProperties(this, /** @lends module:map/map# */ { /** * OpenLayers map instance. * @type {Map|undefined} */ $map: { enumerable: true, get: function get() { return _this3.$olObject; } }, /** * OpenLayers view instance. * @type {View|undefined} */ $view: { enumerable: true, get: function get() { return _this3._view; } } }); } }; /** * Subscribe to OL map events. * * @return {void} * @private */ function subscribeToMapEvents() { var _this4 = this; hasMap(this); hasView(this); var ft = 100; // pointer var pointerEvents = merge(observableFromOlEvent(this.$map, ['click', 'dblclick', 'singleclick']), observableFromOlEvent(this.$map, ['pointerdrag', 'pointermove']).pipe(throttleTime(ft), distinctUntilChanged(function (a, b) { return isEqual(a.coordinate, b.coordinate); }))).pipe(map(function (evt) { return _objectSpread({}, evt, { coordinate: _this4.pointToDataProj(evt.coordinate) }); })); // other var otherEvents = observableFromOlEvent(this.$map, ['movestart', 'moveend', 'postrender', 'precompose', 'postcompose']); var events = merge(pointerEvents, otherEvents); this.subscribeTo(events, function (evt) { return _this4.$emit(evt.type, evt); }); } /** * A click with no dragging. A double click will fire two of this. * @event module:map/map#click * @type {MapBrowserEvent} */ /** * A true double click, with no dragging. * @event module:map/map#dblclick * @type {MapBrowserEvent} */ /* script */ var __vue_script__ = script; /* template */ var __vue_render__ = function __vue_render__() { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c('div', { class: [_vm.$options.name], attrs: { "tabindex": _vm.tabindex } }, [_vm._t("default")], 2); }; var __vue_staticRenderFns__ = []; /* style */ var __vue_inject_styles__ = undefined; /* scoped */ var __vue_scope_id__ = undefined; /* module identifier */ var __vue_module_identifier__ = undefined; /* functional template */ var __vue_is_functional_template__ = false; /* component normalizer */ function __vue_normalize__(template, style, script$$1, scope, functional, moduleIdentifier, createInjector, createInjectorSSR) { var component = (typeof script$$1 === 'function' ? script$$1.options : script$$1) || {}; // For security concerns, we use only base name in production mode. component.__file = "map.vue"; if (!component.render) { component.render = template.render; component.staticRenderFns = template.staticRenderFns; component._compiled = true; if (functional) component.functional = true; } component._scopeId = scope; return component; } /* style inject */ /* style inject SSR */ var Map$1 = __vue_normalize__({ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, undefined, undefined); /** * @vueProps */ var props$1 = /** @lends module:map/view# */ { /** * The center coordinate in the view projection. * @type {number[]} * @default [0, 0] * @vueSync */ center: { type: Array, default: function _default() { return [0, 0]; }, validator: function validator(value) { return value.length === 2; } }, constrainRotation: { type: [Boolean, Number], default: true }, enableRotation: { type: Boolean, default: true }, /** * The extent that constrains the center defined in the view projection, * in other words, center cannot be set outside this extent. * @default undefined */ extent: { type: Array, validator: function validator(value) { return value.length === 4; } }, maxResolution: Number, minResolution: Number, /** * @default 28 */ maxZoom: { type: Number, default: MAX_ZOOM }, /** * @default 0 */ minZoom: { type: Number, default: MIN_ZOOM }, /** * @type {string} * @default EPSG:3857 */ projection: { type: String, default: EPSG_3857 }, resolution: Number, resolutions: Array, /** * The initial rotation for the view in **radians** (positive rotation clockwise). * @type {number} * @vueSync */ rotation: { type: Number, default: 0 }, /** * Zoom level used to calculate the resolution for the view as `int` value. Only used if `resolution` is not defined. * @type {number} * @default 0 * @vueSync */ zoom: { type: Number, default: MIN_ZOOM }, /** * @default 2 */ zoomFactor: { type: Number, default: ZOOM_FACTOR } /** * @vueComputed */ }; var computed$1 = /** @lends module:map/view# */ { viewZoom: function viewZoom() { if (this.rev && this.$view) { return Math.round(this.$view.getZoom()); } return this.zoom; }, viewRotation: function viewRotation() { if (this.rev && this.$view) { return this.$view.getRotation(); } return this.rotation; }, viewResolution: function viewResolution() { if (this.rev && this.$view) { return this.$view.getResolution(); } return this.resolution; }, viewCenter: function viewCenter() { if (this.rev && this.$view) { return this.pointToDataProj(this.$view.getCenter()); } }, viewCenterViewProj: function viewCenterViewProj() { if (this.rev && this.$view) { return this.$view.getCenter(); } }, /** * @return {ProjectionLike} */ resolvedDataProjection: function resolvedDataProjection() { // exclude this.projection from lookup to allow view rendering in projection // that differs from data projection return coalesce(this.$viewContainer && this.$viewContainer.resolvedDataProjection, this.$options.dataProjection, this.viewProjection); } }; /** * @vueMethods */ var methods$1 = /** @lends module:map/view# */ { /** * @see {@link https://openlayers.org/en/latest/apidoc/ol.View.html#animate} * @param {...(AnimationOptions|function(boolean))} args * @return {Promise} Resolves when animation completes */ animate: function animate() { var _this = this; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } hasView(this); var cb = noop; if (isFunction(args[args.length - 1])) { cb = args[args.length - 1]; args = args.slice(0, args.length - 1); } args.forEach(function (opts) { if (!_Array$isArray(opts.center)) return; opts.center = _this.pointToViewProj(opts.center); }); return new _Promise(function (resolve) { var _this$$view; return (_this$$view = _this.$view).animate.apply(_this$$view, _toConsumableArray(args).concat([function (complete) { cb(complete); resolve(complete); }])); }); }, /** * @return {View} * @protected */ createOlObject: function createOlObject() { return new View({ center: this.pointToViewProj(this.center), constrainRotation: this.constrainRotation, enableRotation: this.enableRotation, extent: this.extent ? this.extentToViewProj(this.extent) : undefined, maxResolution: this.maxResolution, minResolution: this.minResolution, maxZoom: this.maxZoom, minZoom: this.minZoom, projection: this.projection, resolution: this.resolution, resolutions: this.resolutions, rotation: this.rotation, zoom: this.zoom, zoomFactor: this.zoomFactor }); }, /** * @see {@link https://openlayers.org/en/latest/apidoc/ol.View.html#fit} * @param {Object|Extent|Geometry|Vue} geometryOrExtent * @param {FitOptions} [options] * @return {Promise} Resolves when view changes */ fit: function fit(geometryOrExtent) { var _this2 = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; hasView(this); // transform from GeoJSON, vl-feature to ol.Feature if (isPlainObject(geometryOrExtent)) { geometryOrExtent = this.readGeometryInDataProj(geometryOrExtent); } else if (geometryOrExtent instanceof Vue) { geometryOrExtent = geometryOrExtent.$geometry; } var cb = options.callback || noop; return new _Promise(function (resolve) { _this2.$view.fit(geometryOrExtent, _objectSpread({}, options, { callback: function callback(complete) { cb(complete); resolve(complete); } })); }); }, /** * @return {void} * @protected */ mount: function mount() { this.$viewContainer && this.$viewContainer.setView(this); this.subscribeAll(); }, /** * @return {void} * @protected */ unmount: function unmount() { this.unsubscribeAll(); this.$viewContainer && this.$viewContainer.setView(undefined); }, /** * @return {void} * @protected */ subscribeAll: function subscribeAll() { subscribeToViewChanges.call(this); } }; var watch$1 = { center: function center(value) { value = this.pointToViewProj(value); if (this.$view && !this.$view.getAnimating() && !isEqual(value, this.viewCenterViewProj)) { this.$view.setCenter(value); } }, resolution: function resolution(value) { if (this.$view && !this.$view.getAnimating() && value !== this.viewResolution) { this.$view.setResolution(value); } }, zoom: function zoom(value) { value = Math.round(value); if (this.$view && !this.$view.getAnimating() && value !== this.viewZoom) { this.$view.setZoom(value); } }, rotation: function rotation(value) { if (this.$view && !this.$view.getAnimating() && value !== this.viewRotation) { this.$view.setRotation(value); } }, minZoom: function minZoom(value) { if (this.$view && value !== this.$view.getMinZoom()) { this.$view.setMinZoom(value); } }, maxZoom: function maxZoom(value) { if (this.$view && value !== this.$view.getMaxZoom()) { this.$view.setMaxZoom(value); } }, resolvedDataProjection: function resolvedDataProjection() { if (this.$view) { this.$view.setCenter(this.pointToViewProj(this.center)); } } }; /** * Represents a simple **2D view** of the map. This is the component to act upon to change the **center**, * **resolution**, and **rotation** of the map. * * @title View `vl-view` component * @alias module:map/view * @vueProto * * @vueSlot default [scoped] Default scoped slot with current state: center, zoom, rotation & etc. */ var script$1 = { name: 'vl-view', mixins: [olCmp, projTransforms$1], props: props$1, computed: computed$1, methods: methods$1, watch: watch$1, stubVNode: { empty: function empty() { return this.$options.name; } }, /** * @this module:map/view */ created: function created() { var _this3 = this; _Object$defineProperties(this, /** @lends module:map/view# */ { /** * @type {View|undefined} */ $view: { enumerable: true, get: function get() { return _this3.$olObject; } }, $viewContainer: { enumerable: true, get: function get() { return _this3.$services && _this3.$services.viewContainer; } } }); } }; /** * Subscribe to OpenLayers significant events * @return {void} * @private */ function subscribeToViewChanges() { var _this4 = this; hasView(this); var ft = 1000 / 60; var resolution = observableFromOlChangeEvent(this.$view, 'resolution', true, ft); var zoom = resolution.pipe(map(function () { return { prop: 'zoom', value: Math.round(_this4.$view.getZoom()) }; }), distinctUntilKeyChanged('value')); var changes = merge(observableFromOlChangeEvent(this.$view, 'center', true, ft, function () { return _this4.pointToDataProj(_this4.$view.getCenter()); }), observableFromOlChangeEvent(this.$view, 'rotation', true, ft), resolution, zoom); this.subscribeTo(changes, function (_ref) { var prop = _ref.prop, value = _ref.value; ++_this4.rev; _this4.$emit("update:".concat(prop), value); }); } /* script */ var __vue_script__$1 = script$1; /* template */ var __vue_render__$1 = function __vue_render__() { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c('i', { class: [_vm.$options.name], staticStyle: { "display": "none !important" } }, [_vm._t("default", null, { center: _vm.viewCenter, zoom: _vm.viewZoom, resolution: _vm.viewResolution, rotation: _vm.viewRotation })], 2); }; var __vue_staticRenderFns__$1 = []; /* style */ var __vue_inject_styles__$1 = undefined; /* scoped */ var __vue_scope_id__$1 = undefined; /* module identifier */ var __vue_module_identifier__$1 = undefined; /* functional template */ var __vue_is_functional_template__$1 = false; /* component normalizer */ function __vue_normalize__$1(template, style, script, scope, functional, moduleIdentifier, createInjector, createInjectorSSR) { var component = (typeof script === 'function' ? script.options : script) || {}; // For security concerns, we use only base name in production mode. component.__file = "view.vue"; if (!component.render) { component.render = template.render; component.staticRenderFns = template.staticRenderFns; component._compiled = true; if (functional) component.functional = true; } component._scopeId = scope; return component; } /* style inject */ /* style inject SSR */ var View$1 = __vue_normalize__$1({ 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, undefined, undefined); function plugin(Vue$$1) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (plugin.installed) { return; } plugin.installed = true; options = pick(options, 'dataProjection'); _Object$assign(Map$1, options); _Object$assign(View$1, options); Vue$$1.component(Map$1.name, Map$1); Vue$$1.component(View$1.name, View$1); } export default plugin; export { Map$1 as Map, View$1 as View, plugin as install };