/* eslint-disable @typescript-eslint/ban-ts-comment */
import { pushIn } from '../../core/util';
import Layer, { LayerJSONType } from '../Layer';
import TileLayer, { TileLayerOptionsType, TilesType } from './TileLayer';
import Size from '../../geo/Size';
const options: GroupTileLayerOptionsType = {
urlTemplate: '',
'maxCacheSize': 1024
};
const DEFAULT_TILESIZE = new Size(256, 256);
const EVENTS = 'show hide remove setzindex forcereloadstart';
function checkLayers(tileLayers: TileLayer[] | TileLayer): TileLayer[] {
if (!Array.isArray(tileLayers)) {
tileLayers = [tileLayers];
}
return tileLayers;
}
/**
* @classdesc
* A layer used to display a group of tile layers.
* Its performance is better than add TileLayers seperately and it can help prevent limits of active webgl contexts:
* "WARNING: Too many active WebGL contexts. Oldest context will be lost"
* @category layer
* @extends TileLayer
* @param {String|Number} id - tile layer's id
* @param {TileLayer[]} layers - TileLayers to add
* @param {Object} [options=null] - options defined in [TileLayer]{@link TileLayer#options}
* @example
* new GroupTileLayer("group-tiles",[
new maptalks.WMSTileLayer('wms', {
'urlTemplate' : 'https://demo.boundlessgeo.com/geoserver/ows',
'crs' : 'EPSG:3857',
'layers' : 'ne:ne',
'styles' : '',
'version' : '1.3.0',
'format': 'image/png',
'transparent' : true,
'uppercase' : true
}),
new maptalks.TileLayer('tile2',{
urlTemplate: 'http://korona.geog.uni-heidelberg.de/tiles/adminb/x={x}&y={y}&z={z}'
})
])
*/
class GroupTileLayer extends TileLayer {
layers: TileLayer[];
layerMap: Record;
//@internal
_groupChildren: any[];
/**
* Reproduce a GroupTileLayer from layer's profile JSON.
* @param layerJSON - layer's profile JSON
* @return
* @static
* @private
* @function
*/
static fromJSON(layerJSON: { [x: string]: any; }): GroupTileLayer {
if (!layerJSON || layerJSON['type'] !== 'GroupTileLayer') {
return null;
}
const layers = layerJSON['layers'].map(json => Layer.fromJSON(json));
return new GroupTileLayer(layerJSON['id'], layers, layerJSON['options']);
}
/**
* @param id - layer's id
* @param layers - TileLayers to add
* @param [options=null] - construct options
* @param [options.*=null] - options defined in [TileLayer]{@link TileLayer#options}
*/
constructor(id: string, layers: TileLayer[], options?: GroupTileLayerOptionsType) {
super(id, options);
this.layers = layers || [];
this._checkChildren();
this.layerMap = {};
this._groupChildren = [];
}
/**
* Get children TileLayer
*/
getLayers(): TileLayer[] {
return this.layers;
}
/**
* add tilelayers
* @param tileLayers
*/
addLayer(tileLayers: TileLayer[] = []) {
tileLayers = checkLayers(tileLayers);
const len = this.layers.length;
tileLayers.forEach(tileLayer => {
if (!(tileLayer instanceof TileLayer)) {
return;
}
if (this.layers.indexOf(tileLayer) === -1 && !this.layerMap[tileLayer.getId()]) {
this.layers.push(tileLayer);
}
});
//layers change
if (len !== this.layers.length) {
this._sortLayers();
this._refresh();
this._renderLayers();
}
return this;
}
/**
* remove tilelayers
* @param tileLayers
*/
removeLayer(tileLayers: TileLayer[] = []) {
tileLayers = checkLayers(tileLayers);
const len = this.layers.length;
tileLayers.forEach(tileLayer => {
if (!(tileLayer instanceof TileLayer)) {
//if tilelayer is id
tileLayer = this.layerMap[tileLayer];
}
if (!(tileLayer instanceof TileLayer)) {
return;
}
const index = this.layers.indexOf(tileLayer);
if (index >= 0) {
this.layers.splice(index, 1);
tileLayer._doRemove();
tileLayer.off(EVENTS, this._onLayerShowHide, this);
}
});
//layers change
if (len !== this.layers.length) {
this._refresh();
this._renderLayers();
}
return this;
}
/**
* clear tilelayers
*/
clearLayers() {
this.layers.forEach(layer => {
layer._doRemove();
layer.off(EVENTS, this._onLayerShowHide, this);
});
this.layers = [];
this._refresh();
this._renderLayers();
return this;
}
/**
* Export the GroupTileLayer's profile json.
* Layer's profile is a snapshot of the layer in JSON format.
* It can be used to reproduce the instance by [fromJSON]{@link Layer#fromJSON} method
* @return layer's profile JSON
*/
toJSON(): LayerJSONType {
const profile = {
'type': this.getJSONType(),
'id': this.getId(),
'layers': this.layers.map(layer => layer.toJSON()),
'options': this.config()
};
return profile;
}
getTileSize(id: number | string) {
const layer = this.getLayer(id);
if (!layer) {
return DEFAULT_TILESIZE;
}
return layer.getTileSize();
}
/**
* Get tiles at zoom (or current zoom)
* @param z
* @returns tiles
*/
getTiles(z: number, parentLayer: any): TilesType {
const layers = this.layers;
const tiles = [];
let count = 0;
for (let i = 0, l = layers.length; i < l; i++) {
const layer = layers[i];
if (!layer || !layer.options['visible'] || !layer.isVisible() || !layer.getMap()) {
continue;
}
const childGrid = layer.getTiles(z, parentLayer || this);
if (!childGrid || childGrid.count === 0) {
continue;
}
count += childGrid.count;
pushIn(tiles, childGrid.tileGrids);
}
return {
count: count,
tileGrids: tiles
};
}
onAdd() {
this._sortLayers();
this._refresh();
super.onAdd();
}
onRemove() {
this.layers.forEach(layer => {
layer._doRemove();
layer.off(EVENTS, this._onLayerShowHide, this);
});
this.layerMap = {};
this._groupChildren = [];
super.onRemove();
}
getLayer(id: string | number) {
return this.getChildLayer(id);
}
getChildLayer(id: string | number): TileLayer {
const layer = this.layerMap[id];
if (layer) {
return layer;
}
for (let i = 0; i < this._groupChildren.length; i++) {
const child = this._groupChildren[i].getChildLayer(id);
if (child) {
return child;
}
}
return null;
}
//@internal
_removeChildTileCache(layer: TileLayer) {
if (!layer) {
return this;
}
const renderer = this.getRenderer();
if (!renderer) {
return this;
}
let cache: any;
const id = layer.getId();
const validateCache = () => {
return cache && cache.info && cache.info.layer === id;
};
//clear LRU
if (renderer.tileCache) {
const keys = renderer.tileCache.keys();
keys.forEach((key: any) => {
cache = renderer.tileCache.get(key);
if (validateCache()) {
renderer.tileCache.remove(key);
}
});
}
//clear tilesInView cache
const tilesInView = renderer.tilesInView || {};
for (const key in tilesInView) {
cache = tilesInView[key];
if (validateCache()) {
delete tilesInView[key];
}
}
//cancel image load
const tilesLoading = renderer.tilesLoading || {};
for (const key in tilesLoading) {
cache = tilesLoading[key];
if (validateCache()) {
renderer.abortTileLoading(cache.image, cache.info);
}
}
return this;
}
//@internal
_onLayerShowHide(e: { type: string; target: any }) {
const { type, target } = e || {};
//listen tilelayer.remove() method fix #1629
if (type === 'remove' && target) {
this.layers.splice(this.layers.indexOf(target), 1);
target._doRemove();
target.off(EVENTS, this._onLayerShowHide, this);
this._refresh();
} else if (type === 'setzindex') {
this._sortLayers();
} else if (type === 'forcereloadstart') {
this._removeChildTileCache(target);
}
this._renderLayers();
}
// render all layers
//@internal
_renderLayers() {
const renderer = this.getRenderer();
if (renderer) {
renderer.setToRedraw();
}
return this;
}
// reset layerMap,_groupChildren,listen tilelayers events
//@internal
_refresh() {
const map = this.getMap();
this._groupChildren = [];
this.layerMap = {};
this.layers.forEach(layer => {
this.layerMap[layer.getId()] = layer;
// @ts-ignore
if (layer.getChildLayer) {
this._groupChildren.push(layer);
}
if (!layer.getMap()) {
layer._bindMap(map);
}
//remove old event handler
layer.off(EVENTS, this._onLayerShowHide, this);
layer.on(EVENTS, this._onLayerShowHide, this);
});
return this;
}
isVisible(): boolean {
if (!super.isVisible()) {
return false;
}
const children = this.layers;
for (let i = 0, l = children.length; i < l; i++) {
if (children[i].isVisible()) {
return true;
}
}
return false;
}
//@internal
_checkChildren() {
const ids = {};
this.layers.forEach(layer => {
const layerId = layer.getId();
if (ids[layerId]) {
throw new Error(`Duplicate child layer id (${layerId}) in the GroupTileLayer (${this.getId()})`);
} else {
ids[layerId] = 1;
}
});
}
//@internal
_sortLayers() {
this.layers.sort(function (a, b) {
return a.options.zIndex - b.options.zIndex;
});
}
}
GroupTileLayer.registerJSONType('GroupTileLayer');
GroupTileLayer.mergeOptions(options);
export default GroupTileLayer;
export type GroupTileLayerOptionsType = TileLayerOptionsType & {
maxCacheSize?: number;
}