import PIXIApp from '../app.js' import { CoordinateDisplay } from './display.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() } /** * Log boundaries for a shown map. * This is for development purposes only, if you want * to find the boundaries of a shown map. * * @memberof MapApp */ logMapBoundaries() { let map = this.mapLayer.map let boundaries = { min: this.mapLayer.mapview.coordinatesFromWindowPoint(map, { x: 0, y: 0 }), max: this.mapLayer.mapview.coordinatesFromWindowPoint(map, { x: 0, y: 0 }) } console.log(JSON.stringify(boundaries)) } _setupMapLayer() { this.mapContainer = new PIXI.Container() this.mapLayer = new MapLayer(this.mapList, this.scene, this.mapContainer, { name: 'Root Map Layer', focus: this.focus, zoom: this.zoom, onChange: this._mapChanged.bind(this) }) } setup() { super.setup() // TODO get access to fps display let fpsDisplay = null this.stage.children.forEach(element => { if (element.refreshFps) fpsDisplay = element }) this._setupMapLayer() this.ui = new PIXI.Container() this.scene.addChild(this.ui) if (this.fpsLogging != null && fpsDisplay != null) 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.mapList) this.mapList.add(key, map) 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() } this.transformed() this.onMapChanged.call(this, this.map) this.map.onTransform.add(this.transformed.bind(this)) 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) } /** * 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) } }