import { GeoMap } from './map.js' import { EventHandler } from './utils.js' import { GeoGraphics } from './geographics.js' import { MapList } from './maplist.js' import MapViewport from './mapviewport.js' import { ScatterContainer } from '../scatter.js' /** * The GeoLayer is a special PIXILayer, that recognizes other GeoLayers and * GeoGraphics. The layer can be adapted to a map and notifies all Geo-Children * of the Adaption. */ export class GeoLayer { constructor(displayObject, opts = {}) { if (displayObject == null || !(displayObject instanceof PIXI.DisplayObject)) { console.error( `You need to provide a displayObject to make a ${this.constructor.name} out of it.`, displayObject ) return null } else { this.geographics = [] // displayObject.map = this this.displayObject = displayObject this.pixiAddChild = displayObject.addChild.bind(displayObject) displayObject.addChild = (...elements) => { elements.forEach(element => { if (element instanceof GeoGraphics) { element.setLayer(this) this.geographics.push(element) this.pixiAddChild(element.graphics) } else { this.pixiAddChild(element) } }) } } this.name = opts['name'] != null ? opts.name : 'Unnamed Layer' this.parent = null this.parentMapLayerTransformedHandler = new EventHandler('onParentMapLayerTransformed') this.layers = [] this._visibility = { min: 0, max: Number.MAX_VALUE } } parentMapLayerTransformed(mapLayer) { this.layers.forEach(layer => { if (!(layer instanceof MapList)) { layer.parentMapLayerTransformed() } }) this.parentMapLayerTransformedHandler.call(null, mapLayer) this.rescaleChildren() } rescaleChildren() { let map = this.map if (this.rescale) { if (map != null) { let scale = map.image.scatter.scale this.displayObject.children.forEach(graphics => { graphics.scale.set(1 / scale, 1 / scale) }) } } let mapLayer = this.mapLayer if (this.visibility && mapLayer != null) { const zoom = mapLayer.mapview.zoom // TODO // Currently I dont know what elemnts was. // We just log an error and resolve this on a later point. if (zoom > this.visibility.min && zoom < this.visibility.max) { this.displayObject.children.forEach(it => (it.visible = true)) } else { this.displayObject.children.forEach(it => (it.visible = false)) } } } set visibility(value) { let { min = 0, max = Infinity } = value this._visibility = { min, max } } get visibility() { return this._visibility } /** * Alias for geoLayer.displayObject.addChild. * * @public * @param {GeoGraphics | PIXI.DisplayObject} element - Element to add to the displayObject. * @memberof GeoLayer */ addChild(element) { this.displayObject.addChild(element) } /** * Adapts to a map. If the maplayer should adapt to the parent maplayer, * no parameter must be specified. */ adapt(map = null) { if (!map) map = this.map if (map) { this.geographics.forEach(geographic => { geographic.adaptTo(map) }) this.layers.forEach(layer => { if (layer.adapt) layer.adapt(map) }) this.rescaleChildren() } else console.error('There was no map specified.', this) } removeFromParent() { if (this.parent) { this.parent.removeLayer(this) } } removeLayer(layer) { let idx = this.layers.indexOf(layer) if (idx != -1) { layer.parent = null this.layers.splice(idx, 1) if (layer.displayObject.parent) { layer.displayObject.parent.removeChild(layer.displayObject) } } else console.warn('Tried to remove layer that was not set.', this, layer) } remove(graphics) { if (graphics instanceof GeoGraphics) { let index = this.geographics.indexOf(geographics) if (index != -1) { this.displayObject.removeChild(geographics) } else { console.error('Could not remove geographics from geolayer.', this, geographics) } } else { this.displayObject.removeChild(graphics) } } set parent(parent) { this._parent = parent } get parent() { return this._parent } /** * Adds a GeoLayer as child to the GeoLayer. * * @public * @param {GeoLayer} layer - GeoLayer to add. * @memberof GeoLayer */ addLayer(layer) { if (layer instanceof GeoLayer) { layer.removeFromParent() this.layers.push(layer) layer.parent = this layer.parentChanged() this.displayObject.addChild(layer.displayObject) if (this.map) layer.geographics.forEach(geographics => geographics.adaptTo(this.map)) } else console.error('Could not place layer. Only MapLayer and GeoLayers can be child layers of GeoLayers.', layer) } parentChanged() { this.rescaleChildren() } //GeoLayers have to be children of a map layer, // therefore we can recursively get the map. get map() { return this.mapLayer ? this.mapLayer.map : null } get mapLayer() { return this._mapLayer ? this._mapLayer : this.parent.mapLayer } // clone(mapLayerClone) { // const opts = { // mapLayer: mapLayerClone, // map: mapLayerClone.map // } // let geoLayerClone = new GeoLayer(opts) // this.layers.forEach(layer => { // let layerClone = layer.clone(opts) // if (layerClone) { // geoLayerClone.placeLayer(layerClone) // } // }) // this.geographics.forEach(geographics => { // let clone = geographics.clone() // if (clone) { // geoLayerClone.place(clone) // } // }) // return geoLayerClone // } } /** * The map layer is responsible for showing certain maps, at a specific position It contains * a list of available maps and can switch between them seamlessly. GeoGraphics placed on the MapLayer itself * or child Geolayers will be adapted to maps and adjusted on map change automatically. * * The map layer is the 'king' of the geo layers. Every geolayer * needs a map layer at it's root. Otherwise they won't work- * * @export * @class MapLayer * @extends {GeoLayer} */ export class MapLayer extends GeoLayer { constructor( mapList, scatterContainer, displayObject, { onTransform = null, onChange = null, focus = null, zoom = null, viewport = null, name = null, mapChangeLocked = false } = {} ) { super(displayObject, { name }) this.transformHandler = new EventHandler('onTransform', { listeners: onTransform }) this.scatterContainer = scatterContainer this.changeHandler = new EventHandler('onChange', { listeners: onChange }) this.mapview = new MapViewport({ zoom, focus, viewport }) this.mapList = mapList // //TODO Implement error handling here. // this.maps = maps // if (opts.map) this.placeMap(opts.map) this.dynamicElements = new Map() this._mapChangeLocked = mapChangeLocked // Binds the transformed callback beforehand. this.transformed = this.transformed.bind(this) this.changeMap(mapList.active) } get mapChangeLocked() { return this._mapChangeLocked } lockMapChange() { this._mapChangeLocked = true } unlockMapChange() { this._mapChangeLocked = false } /** * Adapts all child layers and their GeoGraphics. * * This is called primarily on a map change. * * @private * @memberof MapLayer */ adapt() { this.layers.forEach(layer => { if (layer.adapt) layer.adapt(this.map) }) } transformed(e) { this.mapview.transformed(this.map) this.layers.forEach(layer => layer.parentMapLayerTransformed(this)) this.transformHandler.call(this) } /** * Clones the map layer- * * @param {ScatterContainer} scatterContainer - ScatterContainer of the app. * @param {PIXI.DisplayObject} [container=null] - Container of the newly created MapLayer. If null, an empty PIXI.Container will be created. * @returns * @memberof MapLayer */ clone(scatterContainer, container = null) { let mapList = this.mapList.clone() container = container == null ? new PIXI.Container() : container let mapLayerClone = new MapLayer(mapList, scatterContainer, container, { name: MapLayer.idx++, viewport: this.mapview.viewport, focus: this.mapview.focus, zoom: this.mapview.zoom, mapList }) mapLayerClone.childrenVisibility = this.childrenVisibility return mapLayerClone } /** * Helper function to quickly display the next map. * Order is defined by the key ordering of the maplist. * * @memberof MapLayer */ next() { let nextMap = this.mapList.next() this.changeMap(nextMap) } /** * Changes the map to the specified one, keeping the position and the zoom of the old map. * * @public * @param {GeoMap} map * @memberof MapLayer */ changeMap( name /* map , useScatterAsContainer = true // If set to false, the normal container is used. This is necessary when using submaps and the container need to be a RigidContainer.*/ ) { if (!this.mapChangeLocked) { console.log('🗺️ Change map to: ', name) let oldMap = this.map this.mapList.select(name) if (oldMap) { oldMap.unload() oldMap.onTransform.remove(this.transformed) } let map = this.map if (map) { map.load() this.scatterContainer.addChild(map.image) this.mapview.apply(map) map.image.addChild(this.displayObject) // A geolayer's displayObject is on the parent layer. // A maplayer's displayobject is always the child of the map. this.adapt() this.changeHandler.call(this, map, oldMap) //Call transform one time manually. this.transformed() map.onTransform.add(this.transformed) } else { console.error(`Could not change map to ${name}.`) } } } /** * Applies the mapviews focus to the map. * This may be useful, if the container was modified. * * @memberof MapLayer */ refocus() { this.mapview.apply(this.map) } /** * @public * @returns {GeoMap} - Returns the active map. * @readonly * @memberof MapLayer */ get map() { return this.mapList.map } /** * * This is required for the geo layers. * MapLayer requests from the geoLayers traverse up to the next MapLayer. * * @public * @returns {MapLayer} - Returns this MapLayer. * @readonly * @memberof MapLayer */ get mapLayer() { return this } /** * Cleans up the MapLayer. * * @public * @memberof MapLayer */ cleanup() { this.mapList.cleanup() } } MapLayer.idx = 0