import { GEOJSON_TYPES } from '../core/Constants'; import { isNil, UID, isObject, extend, isFunction, parseStyleRootPath } from '../core/util'; import Extent, { combineExtentAltitude } from '../geo/Extent'; import { Geometry } from '../geometry'; import { createFilter, getFilterFeature, compileStyle } from '@maptalks/feature-filter'; import Layer, { LayerOptionsType } from './Layer'; import GeoJSON from '../geometry/GeoJSON'; import { type OverlayLayerCanvasRenderer } from '../renderer'; import { HandlerFnResultType } from '../core/Eventable'; export function isGeometry(geo) { return geo && (geo instanceof Geometry); } /** * @property options.drawImmediate=false - (Only for layer rendered with [CanvasRenderer]{@link renderer.CanvasRenderer})
* In default, for performance reason, layer will be drawn in a frame requested by RAF(RequestAnimationFrame).
* Set drawImmediate to true to draw immediately.
* This is necessary when layer's drawing is wrapped with another frame requested by RAF. * @property options.geometryEventTolerance=1 - tolerance for geometry events * @memberOf OverlayLayer * @instance */ const options: OverlayLayerOptionsType = { 'drawImmediate': false, 'geometryEvents': true, 'geometryEventTolerance': 1 }; const TMP_EVENTS_ARR = []; /** * layers 的基础类,可用于 geometries 的添加移除 * 抽象类,不准备实例化 * * @english * @classdesc * Base class of all the layers that can add/remove geometries.
* It is abstract and not intended to be instantiated. * @category layer * @abstract * @extends Layer */ class OverlayLayer extends Layer { //@internal _maxZIndex: number //@internal _minZIndex: number //@internal _geoMap: Record; //@internal _geoList: Array //@internal _toSort: boolean //@internal _cookedStyles: any //@internal _clearing: boolean options: OverlayLayerOptionsType; //@internal _renderer: OverlayLayerCanvasRenderer; constructor(id: string, geometries?: OverlayLayerOptionsType | Array, options?: OverlayLayerOptionsType) { if (geometries && (!isGeometry(geometries) && !Array.isArray(geometries) && GEOJSON_TYPES.indexOf((geometries as any).type) < 0)) { options = geometries; geometries = null; } super(id, options); this._maxZIndex = 0; this._minZIndex = 0; this._initCache(); if (geometries) { this.addGeometry(geometries as Array); } const style = this.options['style']; if (style) { this.setStyle(style); } } getAltitude() { return 0; } // isGeometryListening(types) { // if (!this._geoList) { // return false; // } // if (!Array.isArray(types)) { // types = [types]; // } // for (let i = 0, l = this._geoList.length; i < l; i++) { // const geometry = this._geoList[i]; // if (!geometry) { // continue; // } // if (geometry.options.cursor) { // return true; // } // for (let j = 0; j < types.length; j++) { // if (geometry.listens(types[j])) { // return true; // } // } // } // return false; // } /** * 通过 id 获取 geometry * * @english * Get a geometry by its id * @param id - id of the geometry * @return */ getGeometryById(id: string | number): Geometry { if (isNil(id) || id === '') { return null; } if (!this._geoMap[id]) { return null; } return this._geoMap[id]; } /** * 获取所有geometries,如果提供 filter() 方法,则根据方法返回 * * @english * Get all the geometries or the ones filtered if a filter function is provided. * @param filter=undefined - a function to filter the geometries * @param context=undefined - context of the filter function, value to use as this when executing filter. * @return */ getGeometries(filter?: (geo: Geometry) => boolean, context?: any): Array { if (!filter) { return this._geoList.slice(0); } const result = []; let geometry, filtered; for (let i = 0, l = this._geoList.length; i < l; i++) { geometry = this._geoList[i]; if (context) { filtered = filter.call(context, geometry); } else { filtered = filter(geometry); } if (filtered) { result.push(geometry); } } return result; } /** * 获取第一个geometry, geometry 位于底部 * * @english * Get the first geometry, the geometry at the bottom. * @return first geometry */ getFirstGeometry(): Geometry { if (!this._geoList.length) { return null; } return this._geoList[0]; } /** * 获取最后一个geometry, geometry 位于上部 * * @english * Get the last geometry, the geometry on the top * @return last geometry */ getLastGeometry(): Geometry { const len = this._geoList.length; if (len === 0) { return null; } return this._geoList[len - 1]; } /** * 获取 geometries 个数 * * Get count of the geometries * @return count */ getCount(): number { return this._geoList.length; } /** * 获取 geometries 的 extent, 如果 layer 为空,返回 null * * @english * Get extent of all the geometries in the layer, return null if the layer is empty. * @return {Extent} - extent of the layer */ getExtent() { if (this.getCount() === 0) { return null; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore /src/gro/Extent.js-Ts 获取Extent符合参数的type const extent = new Extent(this.getProjection()); this.forEach(g => { const geoExtent = g.getExtent(); extent._combine(geoExtent); combineExtentAltitude(extent, geoExtent); }); return extent; } /** * 按顺序为图层中的每个 geometry 执行一次提供的回调。 * * @english * Executes the provided callback once for each geometry present in the layer in order. * @param fn - a callback function * @param context=undefined - callback's context, value to use as this when executing callback. * @return this */ forEach(fn: (geo: Geometry, index: number) => void, context?: any) { const copyOnWrite = this._geoList.slice(0); for (let i = 0, l = copyOnWrite.length; i < l; i++) { if (!context) { fn(copyOnWrite[i], i); } else { fn.call(context, copyOnWrite[i], i); } } return this; } /** * 创建一个包含所有通过由提供的函数实现的测试的 geometries 的 GeometryCollection。 * * @english * Creates a GeometryCollection with all the geometries that pass the test implemented by the provided function. * @param fn - Function to test each geometry * @param context=undefined - Function's context, value to use as this when executing function. * @return A GeometryCollection with all the geometries that pass the test */ filter(fn: (geo: Geometry) => boolean, context?: any): Array { const selected = []; const isFn = isFunction(fn); const filter = isFn ? fn : createFilter(fn); this.forEach(geometry => { const g = isFn ? geometry : getFilterFeature(geometry); if (context ? filter.call(context, g) : filter(g)) { selected.push(geometry); } }, this); return selected; } /** * layer 是否为空 * * @english * Whether the layer is empty. * @return {Boolean} */ isEmpty(): boolean { return !this._geoList.length; } /** * 为 layer 添加 geometries * * @english * Adds one or more geometries to the layer * @param geometries - one or more geometries * @param fitView=false - automatically set the map to a fit center and zoom for the geometries * @param fitView.easing=out - default animation type * @param fitView.duration=map.options.zoomAnimationDuration - default animation time * @param fitView.step=null - step function during animation, animation frame as the parameter * @return this */ addGeometry(geometries: Geometry | Array, fitView?: boolean | addGeometryFitViewOptions) { if (!geometries) { return this; } if ((geometries as Geometry).type === 'FeatureCollection') { return this.addGeometry(GeoJSON.toGeometry(geometries), fitView); } else if (!Array.isArray(geometries)) { const count = arguments.length; // eslint-disable-next-line prefer-rest-params const last = arguments[count - 1]; // eslint-disable-next-line prefer-rest-params geometries = Array.prototype.slice.call(arguments, 0, count - 1); fitView = last; if (last && isObject(last) && (('type' in last) || isGeometry(last))) { (geometries as Array).push(last as Geometry); fitView = false; } return this.addGeometry(geometries, fitView); } else if (geometries.length === 0) { return this; } this._initCache(); let extent; if (fitView) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore /src/gro/Extent.js-Ts 允许Extent不传参数 extent = new Extent(); } this._toSort = this._maxZIndex > 0; const geos = []; for (let i = 0, l = geometries.length; i < l; i++) { let geo = geometries[i]; if (!(geo && (GeoJSON._isGeoJSON(geo) || isGeometry(geo)))) { throw new Error('Invalid geometry to add to layer(' + this.getId() + ') at index:' + i); } if (geo.getLayer && geo.getLayer() === this) { continue; } if (!isGeometry(geo)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore 未找到fromJSON属性 geo = Geometry.fromJSON(geo); if (Array.isArray(geo)) { for (let ii = 0, ll = geo.length; ii < ll; ii++) { this._add(geo[ii], extent, i); geos.push(geo[ii]); } } } // geojson to Geometry may be null if (!geo) { throw new Error('Invalid geometry to add to layer(' + this.getId() + ') at index:' + i); } if (!Array.isArray(geo)) { this._add(geo, extent, i); geos.push(geo); } } const map: any = this.getMap(); if (map) { this._getRenderer().onGeometryAdd(geos); if (extent && !isNil(extent.xmin)) { const center = extent.getCenter(); const z = map.getFitZoom(extent); if (isObject(fitView)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const step = isFunction(fitView.step) ? fitView.step : () => undefined; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore 当前 map 接口中目前没有animateTo方法 map.animateTo({ center, zoom: z, }, extend({ duration: map.options.zoomAnimationDuration, easing: 'out', }, fitView), step); } else if (fitView === true) { map.setCenterAndZoom(center, z); } } } /** * addgeo 事件 * * addgeo event. * * @event OverlayLayer#addgeo * @type {Object} * @property {String} type - addgeo * @property {OverlayLayer} target - layer * @property {Geometry[]} geometries - the geometries to add */ this.fire('addgeo', { 'type': 'addgeo', 'target': this, 'geometries': geometries }); return this; } /** * 所有 geometries 最小的 zIndex * * @english * Get minimum zindex of geometries */ getGeoMinZIndex() { return this._minZIndex; } /** * 所有 geometries 最大的 zIndex * * @english * Get maximum zindex of geometries */ getGeoMaxZIndex() { return this._maxZIndex; } //@internal _add(geo: Geometry, extent?: Extent, i?: number) { if (!this._toSort) { this._toSort = geo.getZIndex() !== 0; } this._updateZIndex(geo.getZIndex()); const geoId = geo.getId(); if (!isNil(geoId)) { if (!isNil(this._geoMap[geoId])) { throw new Error('Duplicate geometry id in layer(' + this.getId() + '):' + geoId + ', at index:' + i); } this._geoMap[geoId] = geo; } const internalId = UID(); geo._setInternalId(internalId); this._geoList.push(geo); this.onAddGeometry(geo); geo._bindLayer(this); if (geo.onAdd) { geo.onAdd(); } if (extent) { const geoExtent = geo.getExtent(); extent._combine(geoExtent); combineExtentAltitude(extent, geoExtent); } /** * add 事件 * * @english * add event. * * @event Geometry#add * @type {Object} * @property {String} type - add * @property {Geometry} target - geometry * @property {Layer} layer - the layer added to. */ geo._fireEvent('add', { 'layer': this }); if (this._cookedStyles) { this._styleGeometry(geo); } } /** * 移除一个或多个geometries * * @english * Removes one or more geometries from the layer * @param geometries - geometry ids or geometries to remove * @returns this */ removeGeometry(geometries: Geometry | Geometry[]) { if (!Array.isArray(geometries)) { return this.removeGeometry([geometries]); } for (let i = geometries.length - 1; i >= 0; i--) { if (!(geometries[i] instanceof Geometry)) { geometries[i] = this.getGeometryById(geometries[i] as unknown as string); } if (!geometries[i] || this !== geometries[i].getLayer()) continue; geometries[i].remove(); } return this; } /** * 清除 layer * * @english * Clear all geometries in this layer * @returns this */ clear() { this._clearing = true; this.forEach(geo => { geo.remove(); }); this._geoMap = {}; const old = this._geoList; this._geoList = []; const renderer = this._getRenderer(); if (renderer) { renderer.onGeometryRemove(old); if (renderer.clearImageData) { renderer.clearImageData(); delete renderer._lastGeosToDraw; } } this._clearing = false; /** * clear 事件 * * @english * clear event. * * @event OverlayLayer#clear * @type {Object} * @property {String} type - clear * @property {OverlayLayer} target - layer */ this.fire('clear'); return this; } /** * 移除geometry 回调函数 * * @english * Called when geometry is being removed to clear the context concerned. * @param geometry - the geometry instance to remove * @protected */ onRemoveGeometry(geometry: Geometry) { if (!geometry || this._clearing) { return; } //考察geometry是否属于该图层 if (this !== geometry.getLayer()) { return; } const internalId = geometry._getInternalId(); if (isNil(internalId)) { return; } const geoId = geometry.getId(); if (!isNil(geoId)) { delete this._geoMap[geoId]; } const idx = this._findInList(geometry); if (idx >= 0) { this._geoList.splice(idx, 1); } if (this._getRenderer()) { this._getRenderer().onGeometryRemove([geometry]); } } /** * 获取 layer 的 style * * @english * Gets layer's style. * @return layer's style */ getStyle(): any | any[] { if (!this.options['style']) { return null; } return this.options['style']; } /** * layer 设置 style, 用样式符号对满足条件的 geometries进行样式修改 * 基于[mapbox-gl-js's style specification], {https://www.mapbox.com/mapbox-gl-js/style-spec/#types-filter}. * * @english * Sets style to the layer, styling the geometries satisfying the condition with style's symbol.
* Based on filter type in [mapbox-gl-js's style specification]{https://www.mapbox.com/mapbox-gl-js/style-spec/#types-filter}. * @param style - layer's style * @returns this * @fires OverlayLayer#setstyle * @example * layer.setStyle([ { 'filter': ['==', 'count', 100], 'symbol': {'markerFile' : 'foo1.png'} }, { 'filter': ['==', 'count', 200], 'symbol': {'markerFile' : 'foo2.png'} } ]); */ setStyle(style: any | any[]) { this.options.style = style; style = parseStyleRootPath(style); this._cookedStyles = compileStyle(style); this.forEach(function (geometry) { this._styleGeometry(geometry); }, this); /** * setstyle 事件 * @english * setstyle event. * * @event OverlayLayer#setstyle * @type {Object} * @property {String} type - setstyle * @property {OverlayLayer} target - layer * @property {Object|Object[]} style - style to set */ this.fire('setstyle', { 'type': 'setstyle', 'target': this, 'style': style }); return this; } //@internal _styleGeometry(geometry: Geometry): boolean { if (!this._cookedStyles) { return false; } const g = getFilterFeature(geometry); for (let i = 0, len = this._cookedStyles.length; i < len; i++) { if (this._cookedStyles[i]['filter'](g) === true) { geometry._setExternSymbol(this._cookedStyles[i]['symbol']); return true; } } return false; } /** * 移除 style * * @english * Removes layers' style * @returns this * @fires OverlayLayer#removestyle */ removeStyle() { if (!this.options.style) { return this; } delete this.options.style; delete this._cookedStyles; this.forEach(function (geometry) { geometry._setExternSymbol(null); }, this); /** * removestyle 事件 * @english * removestyle event. * * @event OverlayLayer#removestyle * @type {Object} * @property {String} type - removestyle * @property {OverlayLayer} target - layer */ this.fire('removestyle'); return this; } onAddGeometry(geo: Geometry) { const style = this.getStyle(); if (style) { this._styleGeometry(geo); } } hide(): this { for (let i = 0, l = this._geoList.length; i < l; i++) { this._geoList[i].onHide(); } return Layer.prototype.hide.call(this); } //@internal _initCache() { if (!this._geoList) { this._geoList = []; this._geoMap = {}; } } //@internal _updateZIndex(...zIndex: number[]) { this._maxZIndex = Math.max(this._maxZIndex, Math.max(...zIndex)); this._minZIndex = Math.min(this._minZIndex, Math.min(...zIndex)); } //@internal _sortGeometries() { if (!this._toSort) { return; } this._maxZIndex = 0; this._minZIndex = 0; this._geoList.sort((a, b) => { this._updateZIndex(a.getZIndex(), b.getZIndex()); return this._compare(a, b); }); this._toSort = false; } //@internal _compare(a, b) { if (a.getZIndex() === b.getZIndex()) { return a._getInternalId() - b._getInternalId(); } return a.getZIndex() - b.getZIndex(); } //binarySearch //@internal _findInList(geo: Geometry): number { const len = this._geoList.length; if (len === 0) { return -1; } this._sortGeometries(); let low = 0, high = len - 1, middle; while (low <= high) { middle = Math.floor((low + high) / 2); if (this._geoList[middle] === geo) { return middle; } else if (this._compare(this._geoList[middle], geo) > 0) { high = middle - 1; } else { low = middle + 1; } } return -1; } onGeometryEvent(param?: HandlerFnResultType) { return this._onGeometryEvent(param); } //@internal _onGeometryEvent(param?: HandlerFnResultType) { if (!param || !param['target']) { return; } const type = param['type']; if (type === 'idchange') { this._onGeometryIdChange(param); } else if (type === 'zindexchange') { this._onGeometryZIndexChange(param); } else if (type === 'positionchange') { this._onGeometryPositionChange(param); } else if (type === 'shapechange') { this._onGeometryShapeChange(param); } else if (type === 'symbolchange') { this._onGeometrySymbolChange(param); } else if (type === 'show') { this._onGeometryShow(param); } else if (type === 'hide') { this._onGeometryHide(param); } else if (type === 'propertieschange') { this._onGeometryPropertiesChange(param); } } //@internal _onGeometryIdChange(param: HandlerFnResultType) { if (param['new'] === param['old']) { if (this._geoMap[param['old']] && this._geoMap[param['old']] === param['target']) { return; } } if (!isNil(param['new'])) { if (this._geoMap[param['new']]) { throw new Error('Duplicate geometry id in layer(' + this.getId() + '):' + param['new']); } this._geoMap[param['new']] = param['target']; } if (!isNil(param['old']) && param['new'] !== param['old']) { delete this._geoMap[param['old']]; } } //@internal _onGeometryZIndexChange(param: HandlerFnResultType) { if (param['old'] !== param['new']) { this._updateZIndex(param['new']); this._toSort = true; if (this._getRenderer()) { this._getRenderer().onGeometryZIndexChange(param); } } } //@internal _onGeometryPositionChange(param: HandlerFnResultType) { if (this._getRenderer()) { this._getRenderer().onGeometryPositionChange(param); } } //@internal _onGeometryShapeChange(param: HandlerFnResultType) { if (this._getRenderer()) { this._getRenderer().onGeometryShapeChange(param); } } //@internal _onGeometrySymbolChange(param: HandlerFnResultType) { if (this._getRenderer()) { this._getRenderer().onGeometrySymbolChange(param); } } //@internal _onGeometryShow(param: HandlerFnResultType) { if (this._getRenderer()) { this._getRenderer().onGeometryShow(param); } } //@internal _onGeometryHide(param: HandlerFnResultType) { if (this._getRenderer()) { this._getRenderer().onGeometryHide(param); } } //@internal _onGeometryPropertiesChange(param: HandlerFnResultType) { if (this._getRenderer()) { this._getRenderer().onGeometryPropertiesChange(param); } } //@internal _hasGeoListeners(eventTypes: string | Array): boolean { if (!eventTypes) { return false; } if (!Array.isArray(eventTypes)) { TMP_EVENTS_ARR[0] = eventTypes; eventTypes = TMP_EVENTS_ARR; } const geos = this.getGeometries() || []; for (let i = 0, len = geos.length; i < len; i++) { const geometry = geos[i]; if (!geometry) { continue; } if (geometry.options.cursor) { return true; } for (let j = 0, len1 = eventTypes.length; j < len1; j++) { const eventType = eventTypes[j]; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const listens = geometry.listens(eventType); if (listens > 0) { return true; } } } return false; } //override for typing //@internal _getRenderer(): OverlayLayerCanvasRenderer { return super._getRenderer() as OverlayLayerCanvasRenderer; } } OverlayLayer.mergeOptions(options); export default OverlayLayer; export type OverlayLayerOptionsType = LayerOptionsType & { drawImmediate?: boolean, geometryEvents?: boolean, geometryEventTolerance?: number, style?: any; } export type addGeometryFitViewOptions = { easing?: string, duration?: number, step?: (frame) => void }