iwmlib/lib/pixi/maps/mapapp.js

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)
}
}