import Class from '../core/Class';
import { isFunction, isNil, isNumber } from '../core/util';
import Eventable from '../core/Eventable';
import JSONAble from '../core/JSONAble';
import Renderable from '../renderer/Renderable';
import CollisionIndex from '../core/CollisionIndex';
import Geometry from '../geometry/Geometry';
import Browser from '../core/Browser';
import type { Map } from '../map';
import type { Marker, MultiPolygon, Polygon } from '../geometry';
import { CommonProjectionType } from '../geo/projection';
import Coordinate from '../geo/Coordinate';
import Point from '../geo/Point';
import LayerAbstractRenderer from '../renderer/layer/LayerAbstractRenderer';
import { MapStateCache } from '../map/MapStateCache';
/**
* 配置项
*
* @english
* @property options=null - base options of layer.
* @property options.attribution=null - the attribution of this layer, you can specify company or other information of this layer.
* @property options.minZoom=null - the minimum zoom to display the layer, set to -1 to unlimit it.
* @property options.maxZoom=null - the maximum zoom to display the layer, set to -1 to unlimit it.
* @property options.visible=true - whether to display the layer.
* @property options.opacity=1 - opacity of the layer, from 0 to 1.
* @property options.zIndex=undefined - z index of the layer
* @property options.hitDetect=true - Whether to enable hit detection for layers for cursor styles on this map, disable it to improve performance.
* @property options.renderer=canvas - renderer type, "canvas" in default.
* @property options.globalCompositeOperation=null - (Only for layer rendered with [CanvasRenderer]{@link renderer.CanvasRenderer}) globalCompositeOperation of layer's canvas 2d context.context.globalCompositeOperation, 'source-over' in default
* @property options.debugOutline='#0f0' - debug outline's color.
* @property options.cssFilter=null - css filter apply to canvas context's filter
* @property options.forceRenderOnMoving=false - force to render layer when map is moving
* @property options.forceRenderOnZooming=false - force to render layer when map is zooming
* @property options.forceRenderOnRotating=false - force to render layer when map is Rotating
* @property options.collisionScope=layer - layer's collision scope: layer or map
* @property options.maskClip=true - clip layer by mask(Polygon/MultiPolygon)
* @memberOf Layer
* @instance
*/
const options: LayerOptionsType = {
'attribution': null,
'minZoom': null,
'maxZoom': null,
'visible': true,
'opacity': 1,
// context.globalCompositeOperation, 'source-over' in default
'globalCompositeOperation': null,
'renderer': 'canvas',
'debugOutline': '#0f0',
'cssFilter': null,
'forceRenderOnMoving': false,
'forceRenderOnZooming': false,
'forceRenderOnRotating': false,
'collision': false,
'collisionScope': 'layer',
'hitDetect': (function () {
return !Browser.mobile;
})(),
'maskClip': true
};
/**
* layers的基础类,定义了所有layers公共方法。
* 抽象类,不做实例化打算
*
* @english
* @classdesc
* Base class for all the layers, defines common methods that all the layer classes share.
* It is abstract and not intended to be instantiated.
*
* @category layer
* @abstract
* @extends Class
* @mixes Eventable
* @mixes JSONAble
* @mixes Renderable
*/
class Layer extends JSONAble(Eventable(Renderable(Class))) {
//@internal
_canvas: HTMLCanvasElement;
//@internal
_renderer: any;
//@internal
_id: string
//@internal
_zIndex: number
//@internal
_drawTime: number
//@internal
_toRedraw: boolean
map: Map
parent: any;
//@internal
_mask: Polygon | MultiPolygon | Marker;
//@internal
_maskGeoJSON: Record;
//@internal
_loaded: boolean
//@internal
_collisionIndex: CollisionIndex
//@internal
_optionsHook?(conf?: any): void
//@internal
_silentConfig: boolean | undefined | any
options: LayerOptionsType;
getLayers?(): Layer[];
constructor(id: string, options?: LayerOptionsType) {
let canvas;
if (options) {
canvas = options.canvas;
delete options.canvas;
}
super(options);
this._canvas = canvas;
this.setId(id);
if (options) {
this.setZIndex(options.zIndex);
if (options.mask) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 未找到fromJSON属性
this.setMask(Geometry.fromJSON(options.mask));
}
}
this.proxyOptions();
}
/**
* 加载tile layer,不能被子类重写
*
* @english
* load the tile layer, can't be overrided by sub-classes
*/
load() {
if (!this.getMap()) {
return this;
}
if (this.onLoad()) {
this._initRenderer();
const zIndex = this.getZIndex();
if (!isNil(zIndex)) {
if (this._renderer) {
this._renderer.setZIndex(zIndex);
if (!this.isCanvasRender()) {
this._renderer.render();
}
}
}
this.onLoadEnd();
}
return this;
}
/**
* 获取layer Id
*
* @english
* Get the layer id
* @returns id
*/
getId(): string {
return this._id;
}
/**
* 为layer新设一个 Id
*
* @english
* Set a new id to the layer
* @param id - new layer id
* @return this
* @fires Layer#idchange
*/
setId(id: string): this {
const old = this._id;
if (!isNil(id)) {
id = id + '';
}
this._id = id;
/**
* idchange 事件
*
* @english
* idchange event.
*
* @event Layer#idchange
* @type {Object}
* @property {String} type - idchange
* @property {Layer} target - the layer fires the event
* @property {String} old - value of the old id
* @property {String} new - value of the new id
*/
this.fire('idchange', {
'type': 'idchange',
'target': this,
'old': old,
'new': id
});
return this;
}
/**
* 将图层添加至 map
*
* @english
* Adds itself to a map.
* @param map - map added to
* @return this
*/
addTo(map: Map): this {
map.addLayer(this);
return this;
}
/**
* 为layer 设置zIndex
*
* @engilsh
* Set a z-index to the layer
* @param zIndex - layer's z-index
* @return this
*/
setZIndex(zIndex: number): this {
this._zIndex = zIndex;
if (isNil(zIndex)) {
delete this.options['zIndex'];
} else {
this.options.zIndex = zIndex;
}
if (this.map) {
this.map._sortLayersByZIndex();
}
if (this._renderer) {
this._renderer.setZIndex(zIndex);
}
/**
* setzindex 事件
*
* @english
* setzindex event.
*
* @event Layer#setzindex
* @type {Object}
* @property {String} type - setzindex
* @property {Layer} target - the layer fires the event
* @property {Number} zIndex - value of the zIndex
*/
this.fire('setzindex', {
'type': 'setzindex',
'target': this,
zIndex
});
return this;
}
/**
* 获取layer 的 zIndex
*
* @english
* Get the layer's z-index
* @return
*/
getZIndex(): number {
return this._zIndex || 0;
}
/**
* 获取 layer 的 minZoom
*
* @english
* Get Layer's minZoom to display
* @return
*/
getMinZoom(): number {
const map = this.getMap();
const minZoom = this.options['minZoom'];
return map ? Math.max(map.getMinZoom(), minZoom || 0) : minZoom;
}
/**
* 获取layer 的 maxZoom
*
* @english
* Get Layer's maxZoom to display
* @return
*/
getMaxZoom(): number {
const map = this.getMap();
const maxZoom = this.options['maxZoom'];
return map ? Math.min(map.getMaxZoom(), isNil(maxZoom) ? Infinity : maxZoom) : maxZoom;
}
/**
* 获取 layer 的 opacity
*
* @english
* Get layer's opacity
* @returns {Number}
*/
getOpacity() {
return this.options['opacity'];
}
/**
* 设置 layer 的 opacity
*
* @english
* Set opacity to the layer
* @param opacity - layer's opacity
* @return this
*/
setOpacity(op: number): this {
this.config('opacity', op);
/**
* setopacity 事件
*
* @english
* setopacity event.
*
* @event Layer#setopacity
* @type {Object}
* @property {String} type - setopacity
* @property {Layer} target - the layer fires the event
* @property {Number} opacity - value of the opacity
*/
this.fire('setopacity', { type: 'setopacity', target: this, opacity: op });
return this;
}
/**
* layer 是否为 HTML5 Canvas 渲染
*
* @english
* If the layer is rendered by HTML5 Canvas.
* @return
* @protected
*/
isCanvasRender(): boolean {
const renderer = this._getRenderer();
return (renderer && (renderer instanceof LayerAbstractRenderer));
}
/**
* 获取图层所在 map
*
* @english
* Get the map that the layer added to
* @returns {Map}
*/
getMap() {
if (this.map) {
return this.map;
}
return null;
}
/**
* 获取 layer 所在map 的 projection
*
* @english
* Get projection of layer's map
* @returns
*/
getProjection(): CommonProjectionType {
const map = this.getMap();
return map ? map.getProjection() : null;
}
/**
* 将图层置顶
*
* @english
* Brings the layer to the top of all the layers
* @returns this
*/
bringToFront(): this {
const layers = this._getLayerList();
if (!layers.length) {
return this;
}
const topLayer = layers[layers.length - 1];
if (layers.length === 1 || topLayer === this) {
return this;
}
const max = topLayer.getZIndex();
this.setZIndex(max + 1);
return this;
}
/**
* 将图层置底
*
* @english
* Brings the layer under the bottom of all the layers
* @returns {Layer} this
*/
bringToBack(): this {
const layers = this._getLayerList();
if (!layers.length) {
return this;
}
const bottomLayer = layers[0];
if (layers.length === 1 || bottomLayer === this) {
return this;
}
const min = bottomLayer.getZIndex();
this.setZIndex(min - 1);
return this;
}
/**
* 显示图层
*
* @english
* Show the layer
* @returns this
*/
show(): this {
if (!this.options['visible']) {
this.options['visible'] = true;
const renderer = this.getRenderer();
if (renderer) {
renderer.show();
}
const map = this.getMap();
if (renderer && map && map.getRenderer()) {
//fire show in next frame to make sure layer is shown
map.getRenderer().callInNextFrame(() => {
map.getRenderer().callInNextFrame(() => {
this.fire('show');
});
});
} else {
/**
* show event.
*
* @event Layer#show
* @type {Object}
* @property {String} type - show
* @property {Layer} target - the layer fires the event
*/
this.fire('show');
}
}
return this;
}
/**
* 隐藏图层
*
* @english
* Hide the layer
* @returns this
*/
hide(): this {
if (this.options['visible']) {
this.options['visible'] = false;
const renderer = this.getRenderer();
if (renderer) {
renderer.hide();
}
const map = this.getMap();
if (renderer && map && map.getRenderer()) {
//fire hide in next frame to make sure layer is hidden
map.getRenderer().callInNextFrame(() => {
map.getRenderer().callInNextFrame(() => {
this.fire('hide');
});
});
} else {
/**
* hide事件
*
* @english
* hide event.
*
* @event Layer#hide
* @type {Object}
* @property {String} type - hide
* @property {Layer} target - the layer fires the event
*/
this.fire('hide');
}
}
// this.fire('hide');
return this;
}
/**
* layer 的当前 visible 状态
*
* @english
* Whether the layer is visible now.
* @return
*/
isVisible(): boolean {
const opacity = this.options['opacity'];
if (isNumber(opacity) && opacity <= 0) {
return false;
}
const { minZoom, maxZoom } = this.options;
if (isNumber(minZoom) || isNumber(maxZoom)) {
const map = this.map;
if (map) {
const cache = MapStateCache[map.id];
const zoom = cache ? cache.zoom : map.getZoom();
if ((!isNil(maxZoom) && maxZoom < zoom) ||
(!isNil(minZoom) && minZoom > zoom)) {
return false;
}
}
}
if (isNil(this.options['visible'])) {
this.options['visible'] = true;
}
return this.options['visible'];
}
/**
* 移除图层
*
* @english
* Remove itself from the map added to.
* @returns this
*/
remove(): this {
if (this.map) {
const renderer = this.map.getRenderer();
this.map.removeLayer(this);
if (renderer) {
renderer.setToRedraw();
}
} else {
this.fire('remove');
}
return this;
}
/**
* 获取 mask geometry
*
* @english
* Get the mask geometry of the layer
* @return {Geometry}
*/
getMask() {
return this._mask;
}
/**
* 设置mask geometry, 只显示掩码的区域
*
* @english
* Set a mask geometry on the layer, only the area in the mask will be displayed.
* @param {Geometry} mask - mask geometry, can only be a Marker with vector symbol, a Polygon or a MultiPolygon
* @returns {Layer} this
*/
setMask(mask: Polygon | MultiPolygon | Marker): this {
if (!((mask.type === 'Point' && (mask as Marker)._isVectorMarker()) || mask.type === 'Polygon' || mask.type === 'MultiPolygon')) {
throw new Error('Mask for a layer must be a marker with vector marker symbol or a Polygon(MultiPolygon).');
}
//@ts-expect-error Argument of type 'this' is not assignable to parameter of type 'OverlayLayer'.
mask._bindLayer(this);
if (mask.type === 'Point') {
mask.updateSymbol({
'markerLineColor': 'rgba(0, 0, 0, 0)',
'markerFillOpacity': 0
});
} else {
(mask as Polygon).setSymbol({
'lineColor': 'rgba(0, 0, 0, 0)',
'polygonOpacity': 0
});
}
this._mask = mask;
if (mask && mask.toGeoJSON) {
try {
this._maskGeoJSON = mask.toGeoJSON();
} catch (error) {
delete this._maskGeoJSON;
console.error(error);
}
}
this.options.mask = mask.toJSON();
if (!this.getMap() || this.getMap().isZooming()) {
return this;
}
const renderer = this._getRenderer();
if (renderer && renderer.setToRedraw) {
this._getRenderer().setToRedraw();
}
return this;
}
/**
* 移除mask
*
* @engilsh
* Remove the mask
* @returns {Layer} this
*/
removeMask(): this {
delete this._mask;
delete this._maskGeoJSON;
delete this.options.mask;
if (!this.getMap() || this.getMap().isZooming()) {
return this;
}
const renderer = this._getRenderer();
if (renderer && renderer.setToRedraw) {
this._getRenderer().setToRedraw();
}
return this;
}
/**
* 准备层的加载,是一个由子类重写的方法。
*
* @english
* Prepare Layer's loading, this is a method intended to be overrided by subclasses.
* @return true to continue loading, false to cease.
* @protected
*/
onLoad(): boolean {
return true;
}
onLoadEnd() {
}
/**
* 是否加载layer
*
* @english
* Whether the layer is loaded
* @return
*/
isLoaded(): boolean {
return !!this._loaded;
}
/**
* 获取collision index
*
* @english
* Get layer's collision index
* @returns {CollisionIndex}
*/
getCollisionIndex() {
if (this.options['collisionScope'] === 'layer') {
if (!this._collisionIndex) {
this._collisionIndex = new CollisionIndex();
}
return this._collisionIndex;
}
const map = this.getMap();
if (!map) {
return null;
}
return map.getCollisionIndex();
}
/**
* 清除 layer 的 collision index。
* 如果 collisionScope !== 'layer' 将忽略
*
* @english
* Clear layer's collision index.
* Will ignore if collisionScope is not layer
*/
clearCollisionIndex() {
if (this.options['collisionScope'] === 'layer' &&
this._collisionIndex) {
this._collisionIndex.clear();
}
return this;
}
getRenderer() {
return this._getRenderer();
}
onConfig(conf: { [key: string]: any }) {
const needUpdate = conf && Object.keys && Object.keys(conf).length > 0;
if (needUpdate && isNil(conf['animation'])) {
// options change Hook,subLayers Can realize its own logic,such as tileSize/tileSystem etc change
if (this._optionsHook && isFunction(this._optionsHook)) {
this._optionsHook(conf);
}
if (this._silentConfig) {
return;
}
const renderer = this.getRenderer();
if (renderer && renderer.setToRedraw) {
renderer.setToRedraw();
}
//auto update attribution control
if ('attribution' in conf) {
const map = this.getMap();
if (map && map.attributionControl && map.attributionControl._update) {
map.attributionControl._update();
}
}
}
}
onAdd() { }
onRendererCreate() { }
onCanvasCreate() { }
onRemove() { }
//@internal
_bindMap(parent: any, zIndex?: number) {
if (!parent) {
return;
}
this.parent = parent;
if (parent.isMap) {
this.map = parent;
} else {
this.map = parent.getMap();
}
if (!isNil(zIndex)) {
this.setZIndex(zIndex);
}
this._switchEvents('on', this);
this.onAdd();
this.fire('add');
}
//@internal
_initRenderer() {
let renderer = this.options['renderer'];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!this.constructor.getRendererClass || !renderer) {
return;
}
// for map's gl and gpu renderer, layer's renderer is fixed
renderer = this.getRendererOption();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const clazz = this.constructor.getRendererClass(renderer);
if (!clazz) {
throw new Error('Invalid renderer for Layer(' + this.getId() + '):' + renderer);
}
this._renderer = new clazz(this);
this._renderer.layer = this;
this._renderer.setZIndex(this.getZIndex());
this._switchEvents('on', this._renderer);
// some plugin of dom renderer doesn't implement onAdd
if (this._renderer.onAdd) {
this._renderer.onAdd();
}
this.onRendererCreate();
/**
* renderercreate 事件, 当 renderer 创建完成后触发
*
* @english
* renderercreate event, fired when renderer is created.
*
* @event Layer#renderercreate
* @type {Object}
* @property {String} type - renderercreate
* @property {Layer} target - the layer fires the event
* @property {Any} renderer - renderer of the layer
*/
this.fire('renderercreate', {
'type': 'renderercreate',
'target': this,
'renderer': this._renderer
});
}
getRendererOption() {
let renderer = this.options['renderer'];
const mapRenderer = this.getMap().getRenderer();
if (renderer !== 'dom') {
if (mapRenderer.isWebGL()) {
renderer = 'gl';
} else if (mapRenderer.isWebGPU()) {
renderer = 'gpu';
}
}
return renderer;
}
//@internal
_doRemove() {
this._loaded = false;
this._switchEvents('off', this);
this.onRemove();
if (this._renderer) {
this._switchEvents('off', this._renderer);
this._renderer.remove();
delete this._renderer;
}
delete this.map;
delete this._collisionIndex;
}
//@internal
_switchEvents(to, emitter) {
if (emitter && emitter.getEvents && this.getMap()) {
this.getMap()[to](emitter.getEvents(), emitter);
}
}
//@internal
_getRenderer() {
return this._renderer;
}
//@internal
_getLayerList() {
if (!this.map) {
return [];
}
const beginIndex = +!!this.map.getBaseLayer();
return this.map.getLayers().slice(beginIndex);
}
//@internal
_getMask2DExtent() {
if (!this._mask || !this.getMap()) {
return null;
}
const painter = this._mask._getMaskPainter();
if (!painter) {
return null;
}
return painter.get2DExtent();
}
toJSON(options?: any): LayerJSONType {
return {
type: 'Layer',
id: this.getId(),
options: options || this.config()
}
}
/**
* Reproduce a Layer from layer's JSON.
* @param {Object} layerJSON - layer's JSON
* @return {Layer}
*/
static fromJSON(layerJSON: { [key: string]: any }): Layer | null {
if (!layerJSON) {
return null;
}
const layerType = layerJSON['type'];
const clazz = Layer.getJSONClass(layerType) as any;
if (!clazz || !clazz.fromJSON) {
throw new Error('unsupported layer type:' + layerType);
}
return clazz.fromJSON(layerJSON);
}
identify(_coordinate: Coordinate, _options: LayerIdentifyOptionsType) {
// interface method
}
identifyAtPoint(_containerPoint: Point, _options: LayerIdentifyOptionsType) {
// interface method
}
}
Layer.mergeOptions(options);
const fire = Layer.prototype.fire;
Layer.prototype.fire = function (eventType: string, param) {
if (eventType === 'layerload') {
this._loaded = true;
}
if (this.map) {
if (!param) {
param = {
'type': null,
'target': null
};
}
param['type'] = eventType;
param['target'] = this;
this.map._onLayerEvent(param);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
fire.apply(this, arguments);
if (['show', 'hide'].indexOf(eventType) > -1) {
/**
* visiblechange 事件
* @english
* visiblechange event.
*
* @event Layer#visiblechange
* @type {Object}
* @property {String} type - visiblechange
* @property {Layer} target - the layer fires the event
* @property {Boolean} visible - value of visible
*/
this.fire('visiblechange', Object.assign({}, param, { visible: this.options.visible }));
}
return this;
};
export default Layer;
export type LayerOptionsType = {
attribution?: string,
minZoom?: number,
maxZoom?: number,
visible?: boolean,
opacity?: number,
zIndex?: number
globalCompositeOperation?: string,
renderer?: 'canvas' | 'gl' | 'gpu' | 'dom' | null,
debugOutline?: string,
cssFilter?: string,
forceRenderOnMoving?: boolean,
forceRenderOnZooming?: boolean,
forceRenderOnRotating?: boolean,
collision?: boolean,
collisionScope?: 'layer' | 'map',
hitDetect?: boolean,
canvas?: HTMLCanvasElement,
mask?: any,
drawImmediate?: boolean,
geometryEvents?: boolean,
geometryEventTolerance?: number,
maskClip?: boolean;
}
export type LayerJSONType = {
id: string;
type: string;
options: Record;
geometries?: Array;
layers?: Array;
}
export type LayerIdentifyOptionsType = {
onlyVisible?: boolean;
tolerance?: number;
}