462 lines
12 KiB
JavaScript
462 lines
12 KiB
JavaScript
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
|
|
*/
|
|
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)
|
|
}
|
|
|
|
//Todo: rename to mapviewport.
|
|
/**
|
|
* Returns the MapViewport of this map layer.
|
|
*
|
|
* @readonly
|
|
* @member {MapViewport}
|
|
* @memberof MapLayer
|
|
*/
|
|
get mapview() {
|
|
return this._mapview
|
|
}
|
|
|
|
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.update(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 {MapLayer} - Returns the cloned MapLayer.
|
|
* @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
|