576 lines
18 KiB
JavaScript
576 lines
18 KiB
JavaScript
import PIXIApp from '../app.js'
|
|
import { CoordinateDisplay } from '../../../../js/display.js'
|
|
import { DOMLayer, PIXILayer } from '../../../../src/layers/js/layer.js'
|
|
import { MapLayer } from './geolayer.js'
|
|
import { RigidScatterContainer } from './scatter.js'
|
|
import { EventHandler } from './utils.js'
|
|
import { Points } from '../../utils.js'
|
|
import Logging from '../../logging.js'
|
|
import { MapList } from './maplist.js'
|
|
|
|
/**
|
|
* MapApp is responsible for showing fullscreen
|
|
* map applications.
|
|
*
|
|
*/
|
|
export default class MapApp extends PIXIApp {
|
|
constructor(opts = {}) {
|
|
super(opts)
|
|
|
|
// Default values.
|
|
opts = Object.assign(
|
|
{
|
|
debug: false,
|
|
maps: {}, //required
|
|
view: null, //required
|
|
wrapper: null, // Required if you want to use DOM Overlays.
|
|
startmap: null,
|
|
coordsLogging: false,
|
|
overlays: {},
|
|
keycodes: {},
|
|
showHotkeys: false,
|
|
imageMapZoomHeight: 256, //Defines the zoomvalue 1 for all image maps inside the mapapp.
|
|
focus: null,
|
|
zoom: 1,
|
|
onMapChanged: null,
|
|
onSizeChanged: null,
|
|
onTransform: null
|
|
},
|
|
opts,
|
|
{
|
|
alpha: 0.5
|
|
}
|
|
)
|
|
this.submaps = []
|
|
this.overlayElements = new Map()
|
|
this.debug = opts.debug
|
|
this.fpsLogging = opts.fpsLogging
|
|
this.showHotkeys = opts.showHotkeys
|
|
this.keycodes = this._extractKeyCodes(opts.keycodes)
|
|
this.coordsLogging = opts.coordsLogging
|
|
this.overlays = opts.overlays
|
|
this.focus = opts.focus
|
|
this.zoom = opts.zoom
|
|
this.wrapper = opts.wrapper
|
|
|
|
this.onMapChanged = new EventHandler('mapChanged', {
|
|
listeners: opts.onMapChanged
|
|
})
|
|
this.onSizeChanged = new EventHandler('sizeChanged', {
|
|
listeners: opts.onSizeChanged
|
|
})
|
|
this.onTransform = new EventHandler('transformed', {
|
|
listeners: opts.onTransform
|
|
})
|
|
|
|
/**
|
|
* When in debug mode, this allows the user to copy the center coordinates to the clipboard.
|
|
*/
|
|
if (this.debug) {
|
|
this.DRAW_MODES = {
|
|
PIXI_POINT: 0,
|
|
PATH_MODE: 1,
|
|
POLYGON_MODE: 2,
|
|
|
|
getName: function(num) {
|
|
let result = null
|
|
for (const [key, val] of Object.entries(this)) {
|
|
if (val == num) result = key
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
this.drawMode = this.DRAW_MODES.PIXI_POINT
|
|
this.drawData = []
|
|
}
|
|
|
|
this.mapList = new MapList(opts.startmap ? opts.startmap : null, opts.maps ? opts.maps : {})
|
|
|
|
this._setupKeyboardUtils()
|
|
}
|
|
|
|
_setupMapLayer() {
|
|
this.mapContainer = new PIXI.Container()
|
|
this.mapLayer = new MapLayer(this.scene, this.mapContainer, {
|
|
name: 'Map Layer',
|
|
focus: this.focus,
|
|
zoom: this.zoom
|
|
})
|
|
|
|
this.mapLayer.changeHandler.add(this._mapChanged.bind(this))
|
|
|
|
if (this.mapList.map) this.mapLayer.changeMap(this.mapList.map)
|
|
}
|
|
|
|
setup() {
|
|
super.setup()
|
|
|
|
// TODO get access to fps display
|
|
let fpsDisplay
|
|
this.stage.children.forEach(element => {
|
|
if (element.refreshFps) fpsDisplay = element
|
|
})
|
|
|
|
this.pixiLayer = new PIXILayer({
|
|
name: 'Pixi Root',
|
|
container: this.scene
|
|
})
|
|
|
|
this._setupMapLayer()
|
|
|
|
this.ui = new PIXI.Container()
|
|
this.scene.addChild(this.ui)
|
|
|
|
if (this.fpsLogging && fpsDisplay) this.ui.addChild(fpsDisplay)
|
|
|
|
if (this.coordsLogging) {
|
|
this.coordsDisplay = new CoordinateDisplay(this)
|
|
this.ui.addChild(this.coordsDisplay)
|
|
}
|
|
|
|
this.__dragging = false
|
|
this.__events = new Map()
|
|
|
|
const scene = this.scene
|
|
scene.interactive = true
|
|
scene.on('pointercancel', this.__onEnd.bind(this))
|
|
scene.on('pointerdown', this.__onStart.bind(this))
|
|
scene.on('pointermove', this.__onMove.bind(this))
|
|
scene.on('pointerout', this.__onEnd.bind(this))
|
|
scene.on('pointerup', this.__onEnd.bind(this))
|
|
scene.on('pointerupoutside', this.__onEnd.bind(this))
|
|
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* You can add Dom elements directly to the app's DomLayer by calling
|
|
* appendChild(). The elements will be forced to use an absolute positioning.
|
|
* This shall prevent unwanted sideeffects with the app.
|
|
*
|
|
* @param {HTMLElement} element - Element to add to DOMRoot.
|
|
* @memberof MapApp
|
|
*/
|
|
appendChild(element) {
|
|
if (!this.wrapper)
|
|
console.error(
|
|
`When using DOMElements on ${this.constructor.name} you must specify a wrapper in the constructor.`
|
|
)
|
|
if (!(element instanceof HTMLElement))
|
|
console.error(
|
|
'You can only append HTMLElements to DOM. If you want to add PIXI elements to the scene call app.scene.addChild(element).'
|
|
)
|
|
else {
|
|
Object.assign(element.style, { position: 'absolute' })
|
|
this.wrapper.appendChild(element)
|
|
}
|
|
}
|
|
|
|
layout(width, height) {
|
|
this.scene.resize(width, height)
|
|
this.mapLayer.mapview.update()
|
|
this.onSizeChanged.call(this)
|
|
}
|
|
|
|
sceneFactory() {
|
|
return new RigidScatterContainer(this.width, this.height, this.renderer, {
|
|
app: this,
|
|
showBounds: true,
|
|
showTouches: true,
|
|
showPolygon: true,
|
|
container: this
|
|
})
|
|
}
|
|
|
|
selectMap(key) {
|
|
let result = this.mapList.select(key)
|
|
console.log('Select map', key, result)
|
|
if (result && this.mapLayer) {
|
|
this.mapLayer.changeMap(this.mapList.map)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds and sets a map to the mapapp.
|
|
*
|
|
* @param {string} key - Name of the map.
|
|
* @param {GeoMap} map - Map to add.
|
|
* @memberof MapApp
|
|
*/
|
|
setMap(key, map) {
|
|
this.addMap(key, map)
|
|
this.selectMap(key)
|
|
}
|
|
|
|
addMap(key, map) {
|
|
if (this.mapLayer != null) this.mapLayer.mapList.add(key, map)
|
|
if (this.mapList) this.mapList.add(key, map)
|
|
//This is necessary as there is a state, when the mapList
|
|
else console.error('Cannot access mapLayer. It was not initialized yet.')
|
|
}
|
|
|
|
addMaps(mapObject) {
|
|
for (let [key, val] of Object.entries(mapObject)) {
|
|
this.addMap(key, val)
|
|
}
|
|
}
|
|
|
|
transformed(event) {
|
|
this.onTransform.call(this, event)
|
|
}
|
|
|
|
_mapChanged(lastMap) {
|
|
if (lastMap) {
|
|
lastMap.flushHandlers()
|
|
}
|
|
|
|
/**
|
|
* TODO: A problem is that the map layer has no container
|
|
* on its own and it lives in the ScatterContainer.
|
|
* Here we guarantee, that the layer order is as it
|
|
* is defined in the layers.
|
|
*/
|
|
// this.pixiLayer.layers.forEach(layer => {
|
|
// if (layer !== this.mapLayer) {
|
|
// layer.parent.container.removeChild(layer.container)
|
|
// layer.parent.container.addChild(layer.container)
|
|
// }
|
|
// })
|
|
|
|
this.map.onTransform.add(this.transformed.bind(this))
|
|
|
|
this.transformed()
|
|
this.onMapChanged.call(this, this.map)
|
|
|
|
if (this.ui && this.ui.parent) {
|
|
const parent = this.ui.parent
|
|
parent.removeChild(this.ui)
|
|
parent.addChild(this.ui)
|
|
}
|
|
}
|
|
|
|
_doesOverlayElementExist(layer, type, name) {
|
|
let layerElements = this.overlayElements.get(layer)
|
|
return layerElements != undefined && layerElements[type] != null && layerElements[type][name] != null
|
|
}
|
|
|
|
_getOverlayElement(layer, type, name) {
|
|
let layerElements = this.overlayElements.get(layer)
|
|
return layerElements[type][name]
|
|
}
|
|
|
|
_setOverlayElement(layer, type, name, value) {
|
|
let obj = this.overlayElements.get(layer)
|
|
if (obj == undefined) obj = {}
|
|
|
|
if (obj[type] == null) obj[type] = {}
|
|
|
|
obj[type][name] = value
|
|
|
|
this.overlayElements.set(layer, obj)
|
|
}
|
|
|
|
place(layer) {
|
|
if (layer instanceof PIXILayer) {
|
|
this.pixiLayer.place(layer)
|
|
} else if (layer instanceof DOMLayer) {
|
|
this.domLayer.place(layer)
|
|
} else {
|
|
console.error('Could not add layer to ', this, layer)
|
|
}
|
|
}
|
|
|
|
addMapOverlay(layer) {
|
|
this.mapLayer.place(layer)
|
|
}
|
|
|
|
/**
|
|
* Copies the current coordinates to the clipboard.
|
|
*/
|
|
locationToClipboard() {
|
|
let hidden = document.createElement('input')
|
|
document.body.appendChild(hidden)
|
|
hidden.value = '"location":' + JSON.stringify(app.mapLayer.mapview.focus)
|
|
hidden.readOnly = true
|
|
hidden.select()
|
|
document.execCommand('Copy')
|
|
this.showNotification('Copied location to clipboard.')
|
|
document.body.removeChild(hidden)
|
|
}
|
|
|
|
pathToClipboard() {
|
|
let hidden = document.createElement('input')
|
|
document.body.appendChild(hidden)
|
|
|
|
this.drawData.push(this.mapLayer.mapview.focus)
|
|
|
|
if (this.drawMode == this.DRAW_MODES.POLYGON_MODE) {
|
|
let data = {
|
|
type: 'Polygon',
|
|
coordinates: [this.drawData]
|
|
}
|
|
|
|
hidden.value = '"geometry":' + JSON.stringify(data)
|
|
} else {
|
|
// PATH_MODE is default.
|
|
hidden.value = JSON.stringify(this.drawData)
|
|
}
|
|
hidden.select()
|
|
document.execCommand('Copy')
|
|
this.showNotification('Location appended in clipboard.')
|
|
document.body.removeChild(hidden)
|
|
}
|
|
|
|
get map() {
|
|
return this.mapList.map
|
|
}
|
|
|
|
get activeMapKey() {
|
|
return this.mapLayer.active
|
|
}
|
|
|
|
getRelativePosition(x, y) {
|
|
return {
|
|
x: x * app.width,
|
|
y: y * app.height
|
|
}
|
|
}
|
|
|
|
clearDrawData() {
|
|
this.drawData = []
|
|
}
|
|
|
|
showNotification(msg) {
|
|
let notification = document.createElement('div')
|
|
notification.classList.add('notification')
|
|
|
|
let text = document.createElement('p')
|
|
text.innerHTML = msg
|
|
notification.appendChild(text)
|
|
|
|
/**
|
|
* TODO: move styling (except opacity) to css.
|
|
*/
|
|
Object.assign(notification.style, {
|
|
opacity: 0,
|
|
top: 0,
|
|
left: 0,
|
|
position: 'fixed',
|
|
display: 'flex',
|
|
width: '100%',
|
|
height: '100%',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
pointerEvents: 'none'
|
|
})
|
|
|
|
document.body.appendChild(notification)
|
|
|
|
let popAnimation = new TimelineLite({
|
|
onComplete: () => {
|
|
notification.parentNode.removeChild(notification)
|
|
}
|
|
})
|
|
let animationSpeed = 0.5
|
|
|
|
popAnimation
|
|
.to(notification, animationSpeed, {
|
|
opacity: 1
|
|
})
|
|
.to(notification, animationSpeed, {
|
|
opacity: 0
|
|
})
|
|
}
|
|
|
|
_currentLocationToString() {}
|
|
|
|
_matchKeyCode(event, keyCode) {
|
|
// If keycode does not exist or is invalid - return.
|
|
if (!keyCode || keyCode.key == null) return false
|
|
|
|
let code = keyCode.key
|
|
if (event.shiftKey) code -= 32
|
|
const key = event.which == code || event.keyCode == code || event.charCode == code
|
|
|
|
const ctrl = keyCode.ctrlKey == event.ctrlKey
|
|
const shift = keyCode.shiftKey == event.shiftKey
|
|
const alt = keyCode.altKey == event.altKey
|
|
|
|
return key && ctrl && shift && alt
|
|
}
|
|
|
|
_setupKeyboardUtils() {
|
|
document.body.addEventListener('keypress', event => {
|
|
this._checkForKeyCode(event)
|
|
})
|
|
}
|
|
|
|
_checkForKeyCode(event) {
|
|
if (this._matchKeyCode(event, this.keycodes.copyCoordinate)) {
|
|
event.preventDefault()
|
|
|
|
switch (this.drawMode) {
|
|
case this.DRAW_MODES.PIXI_POINT:
|
|
this.locationToClipboard()
|
|
break
|
|
case this.DRAW_MODES.PATH_MODE:
|
|
case this.DRAW_MODES.POLYGON_MODE:
|
|
this.pathToClipboard()
|
|
break
|
|
default:
|
|
console.error(
|
|
`Draw mode is not implemented yet: ${this.DRAW_MODES.getName(this.drawMode)}(${this.drawMode}).`
|
|
)
|
|
}
|
|
}
|
|
|
|
if (this._matchKeyCode(event, this.keycodes.togglePathMode)) {
|
|
if (this.drawMode == this.DRAW_MODES.PATH_MODE) {
|
|
this.showNotification('Path Mode disabled.')
|
|
this._resetDrawMode()
|
|
} else {
|
|
this.drawMode = this.DRAW_MODES.PATH_MODE
|
|
this.showNotification('Path Mode enabled.')
|
|
this.clearDrawData()
|
|
}
|
|
}
|
|
|
|
//When SHIFT+P is pressed POLYGON-MODE is toggled:
|
|
if (this._matchKeyCode(event, this.keycodes.togglePolygonMode)) {
|
|
if (this.drawMode == this.DRAW_MODES.POLYGON_MODE) {
|
|
this.showNotification('Polygon Mode disabled.')
|
|
this._resetDrawMode()
|
|
} else {
|
|
this.drawMode = this.DRAW_MODES.POLYGON_MODE
|
|
this.showNotification('Polygon Mode enabled.')
|
|
this.clearDrawData()
|
|
}
|
|
}
|
|
|
|
// When SHIFT+X is pressed toggle crosshair
|
|
if (this.keycodes.toggleUi && this._matchKeyCode(event, this.keycodes.toggleUi)) {
|
|
if (this.pixiUiLayer.visible) {
|
|
this.pixiUiLayer.hide()
|
|
this.domUiLayer.hide()
|
|
} else {
|
|
this.pixiUiLayer.show()
|
|
this.domUiLayer.show()
|
|
}
|
|
}
|
|
}
|
|
|
|
_resetDrawMode() {
|
|
this.drawMode = this.DRAW_MODES.PIXI_POINT
|
|
}
|
|
|
|
_extractKeyCodes(keycodeText) {
|
|
let out = {}
|
|
|
|
for (let [name, combinationString] of Object.entries(keycodeText)) {
|
|
let keys = combinationString.split('+')
|
|
out[name] = {
|
|
key: null,
|
|
ctrlKey: false,
|
|
shiftKey: false,
|
|
altKey: false
|
|
}
|
|
let errors = []
|
|
|
|
const special = ['shift', 'ctrl', 'alt']
|
|
keys.forEach(key => {
|
|
if (key.length == 1) {
|
|
if (out[name].key) {
|
|
const error = 'Tried to set multiple keys as keycode. This is currently not supported.'
|
|
errors.push(error)
|
|
} else out[name].key = key.charCodeAt(0)
|
|
} else {
|
|
key = key.toLowerCase()
|
|
if (special.indexOf(key) != -1) {
|
|
out[name][key + 'Key'] = true
|
|
} else {
|
|
const error = 'Wrong / non-implemented special character OR typo in key: ' + key
|
|
errors.push(error)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Disable hotkey if an error occured.
|
|
if (errors.length > 0)
|
|
out[name] = {
|
|
key: null,
|
|
ctrlKey: false,
|
|
shiftKey: false,
|
|
altKey: false
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
__onStart(event) {
|
|
this.__dragging = true
|
|
|
|
let hittedSubmap = null
|
|
let center = null
|
|
for (const submap of this.submaps) {
|
|
const radius = submap.container.width / 2
|
|
const distance = Points.distance(submap.center, event.data.global)
|
|
if (distance < radius) {
|
|
hittedSubmap = submap
|
|
center = submap.center
|
|
}
|
|
}
|
|
|
|
this.__events.set(event.data.identifier, {
|
|
event,
|
|
submap: hittedSubmap,
|
|
center
|
|
})
|
|
}
|
|
|
|
__onMove(event) {
|
|
if (this.__dragging) {
|
|
const myevent = this.__events.get(event.data.identifier)
|
|
|
|
if (myevent && myevent.submap) {
|
|
const submap = myevent.submap
|
|
const center = submap.center
|
|
const radius = submap.container.width / 2
|
|
const distance = Points.distance(center, event.data.global) / submap.scatter.scale
|
|
if (distance > radius) {
|
|
//submap.resize((distance) * 2, .2)
|
|
//submap.centerAt(myevent.center)
|
|
}
|
|
}
|
|
|
|
// for (const submap of this.submaps) {
|
|
// const center = submap.center
|
|
// const radius = submap.container.width / 2
|
|
// const distance = Points.distance(center, event.data.global) / submap.scatter.scale
|
|
// const inside = distance < radius + 10
|
|
|
|
// console.log(distance, radius)
|
|
|
|
// if (inside) {
|
|
// // (this.width + 80) / 2 * this.scatter.scale
|
|
// //const width = (submap.width + 80) / 2 * submap.scatter.scale
|
|
// //console.log(width)
|
|
|
|
// if (distance > radius) {
|
|
// submap.resize((distance) * 2, .2)
|
|
// }
|
|
// } else {
|
|
// if (distance < radius + 20) {
|
|
// //submap.resize((distance - 30) * 2, .2)
|
|
// }
|
|
// }
|
|
// }
|
|
}
|
|
}
|
|
|
|
__onEnd(event) {
|
|
this.__dragging = false
|
|
this.__events.delete(event.data.identifier)
|
|
}
|
|
}
|