Added the maps-module to the iwmlib.
Migrated a ot of the content from the tuesch to the iwmlib. This is before the decoupeling of the layers.
31
dist/iwmlib.js
vendored
@ -184,7 +184,7 @@
|
||||
let pre = document.createElement('pre');
|
||||
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
|
||||
// let re = /Doctest\.expect\(([\s\S]*)[\,\s\S]*([\s\S]*)\)/g
|
||||
let lines = text.value.split('\n');
|
||||
let lines = text.value ? text.value.split('\n') : text.split('\n');
|
||||
let better = [];
|
||||
for (let line of lines) {
|
||||
if (replaceExpect && line.trim().startsWith('Doctest.expect(')) {
|
||||
@ -508,6 +508,19 @@
|
||||
|
||||
static toLine(event) {
|
||||
return `${event.type} #${event.target.id} ${event.clientX} ${event.clientY}`
|
||||
let result = event.type;
|
||||
let selector = this.selector(event.target);
|
||||
result += ' selector: ' + selector;
|
||||
if (event.target != document.querySelector(selector)) console.log('Cannot resolve', selector);
|
||||
let keys = ['layerX', 'layerY', 'pageX', 'pageY', 'clientX', 'clientY'];
|
||||
for (let key of keys) {
|
||||
try {
|
||||
result += ' ' + key + ':' + event[key];
|
||||
} catch (e) {
|
||||
console.log('Invalid key: ' + key);
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static compareExtractedWithSimulated() {
|
||||
@ -2651,6 +2664,7 @@
|
||||
if (found != null) {
|
||||
this.interaction.addTarget(key, found);
|
||||
}
|
||||
console.log(this.target);
|
||||
}
|
||||
let size = this.interaction.current.size;
|
||||
let limit = this.logInteractionsAbove;
|
||||
@ -10980,6 +10994,21 @@
|
||||
}
|
||||
|
||||
speak() {
|
||||
/**
|
||||
* This is a little bit ugly, but imho the most elegant of all dirty solutions.
|
||||
*
|
||||
5ht * Within the plugins we have no knowledge of other cards and such. But must differentiate the
|
||||
* clicks by their corresponding owner. The SpeechUtterance just takes a text and has no knowledge
|
||||
* about the node that is currently read to the user.
|
||||
*
|
||||
* This means, that we can identify same text, but not differentiate same text on different nodes.
|
||||
* To account for that, we add the node to the speechSynthesis object (#benefitsOfJavaScript) and
|
||||
* have access to the node, by - let's say - expanding the functionality of the SpeechSynthesis object.
|
||||
*
|
||||
* SO -17.07.19
|
||||
*/
|
||||
|
||||
let activeNode = window.speechSynthesis['speechPluginNode'];
|
||||
this._updateText();
|
||||
}
|
||||
|
||||
|
5266
dist/iwmlib.pixi.js
vendored
@ -90,7 +90,7 @@ export default class Doctest {
|
||||
let pre = document.createElement('pre')
|
||||
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
|
||||
// let re = /Doctest\.expect\(([\s\S]*)[\,\s\S]*([\s\S]*)\)/g
|
||||
let lines = text.value.split('\n')
|
||||
let lines = text.value ? text.value.split('\n') : text.split('\n')
|
||||
let better = []
|
||||
for (let line of lines) {
|
||||
if (replaceExpect && line.trim().startsWith('Doctest.expect(')) {
|
||||
|
105
lib/examples/maps.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"osm": {
|
||||
"projection": "mercator",
|
||||
"type": "deepzoom",
|
||||
"tiles": {
|
||||
"clip": {
|
||||
"minLevel": 0,
|
||||
"maxLevel": 20,
|
||||
"startCol": 275200,
|
||||
"startRow": 180992,
|
||||
"bounds": {
|
||||
"min": [
|
||||
48.458353,
|
||||
8.96484374976547
|
||||
],
|
||||
"max": [
|
||||
48.5747899110263,
|
||||
9.14062499976523
|
||||
]
|
||||
}
|
||||
},
|
||||
"tileSize": 512,
|
||||
"format": "png",
|
||||
"overlap": 0,
|
||||
"type": "map",
|
||||
"height": 131072,
|
||||
"width": 131072,
|
||||
"path": "./var/stadtplan/2018",
|
||||
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
|
||||
},
|
||||
"stadtplan": {
|
||||
"projection": "mercator",
|
||||
"type": "deepzoom",
|
||||
"icon": "./var/img/map_thumbnails/stadtplan.jpg",
|
||||
"tiles": {
|
||||
"compression": [
|
||||
"dds"
|
||||
],
|
||||
"clip": {
|
||||
"minLevel": 12,
|
||||
"maxLevel": 20,
|
||||
"startCol": 275200,
|
||||
"startRow": 180992,
|
||||
"bounds": {
|
||||
"min": [
|
||||
48.458353,
|
||||
8.96484374976547
|
||||
],
|
||||
"max": [
|
||||
48.5747899110263,
|
||||
9.14062499976523
|
||||
]
|
||||
}
|
||||
},
|
||||
"tileSize": 512,
|
||||
"format": "png",
|
||||
"overlap": 0,
|
||||
"type": "map",
|
||||
"height": 131072,
|
||||
"width": 131072,
|
||||
"path": "./var/stadtplan/2018",
|
||||
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
|
||||
},
|
||||
"options": {
|
||||
"cover": true
|
||||
}
|
||||
},
|
||||
"luftbild": {
|
||||
"projection": "mercator",
|
||||
"type": "deepzoom",
|
||||
"icon": "./var/img/map_thumbnails/luftbild.jpg",
|
||||
"tiles": {
|
||||
"compression": [
|
||||
"dds"
|
||||
],
|
||||
"clip": {
|
||||
"minLevel": 12,
|
||||
"maxLevel": 20,
|
||||
"startCol": 275200,
|
||||
"startRow": 180992,
|
||||
"bounds": {
|
||||
"min": [
|
||||
48.458353,
|
||||
8.96484374976547
|
||||
],
|
||||
"max": [
|
||||
48.5747899110263,
|
||||
9.14062499976523
|
||||
]
|
||||
}
|
||||
},
|
||||
"tileSize": 512,
|
||||
"format": "png",
|
||||
"overlap": 0,
|
||||
"type": "map",
|
||||
"height": 131072,
|
||||
"width": 131072,
|
||||
"path": "./var/luftbild/2018",
|
||||
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
|
||||
},
|
||||
"options": {
|
||||
"cover": true
|
||||
}
|
||||
}
|
||||
}
|
@ -999,6 +999,7 @@ export class InteractionMapper extends InteractionDelegate {
|
||||
if (found != null) {
|
||||
this.interaction.addTarget(key, found)
|
||||
}
|
||||
console.log(this.target)
|
||||
}
|
||||
let size = this.interaction.current.size
|
||||
let limit = this.logInteractionsAbove
|
||||
|
BIN
lib/pixi/assets/maps/osm/0/0/0.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
lib/pixi/assets/maps/osm/1/0/0.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
lib/pixi/assets/maps/osm/1/0/1.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
lib/pixi/assets/maps/osm/1/1/0.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
lib/pixi/assets/maps/osm/1/1/1.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
lib/pixi/assets/maps/osm/2/0/0.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
lib/pixi/assets/maps/osm/2/0/1.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
lib/pixi/assets/maps/osm/2/0/2.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
lib/pixi/assets/maps/osm/2/0/3.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
lib/pixi/assets/maps/osm/2/1/0.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
lib/pixi/assets/maps/osm/2/1/1.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
lib/pixi/assets/maps/osm/2/1/2.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
lib/pixi/assets/maps/osm/2/1/3.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
lib/pixi/assets/maps/osm/2/2/0.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/2/2/1.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
lib/pixi/assets/maps/osm/2/2/2.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
lib/pixi/assets/maps/osm/2/2/3.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
lib/pixi/assets/maps/osm/2/3/0.png
Normal file
After Width: | Height: | Size: 887 B |
BIN
lib/pixi/assets/maps/osm/2/3/1.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
lib/pixi/assets/maps/osm/2/3/2.png
Normal file
After Width: | Height: | Size: 814 B |
BIN
lib/pixi/assets/maps/osm/2/3/3.png
Normal file
After Width: | Height: | Size: 982 B |
BIN
lib/pixi/assets/maps/osm/3/0/0.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/0/1.png
Normal file
After Width: | Height: | Size: 942 B |
BIN
lib/pixi/assets/maps/osm/3/0/2.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
lib/pixi/assets/maps/osm/3/0/3.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
lib/pixi/assets/maps/osm/3/0/4.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
lib/pixi/assets/maps/osm/3/0/5.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
lib/pixi/assets/maps/osm/3/0/6.png
Normal file
After Width: | Height: | Size: 874 B |
BIN
lib/pixi/assets/maps/osm/3/0/7.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/1/0.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
lib/pixi/assets/maps/osm/3/1/1.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
lib/pixi/assets/maps/osm/3/1/2.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
lib/pixi/assets/maps/osm/3/1/3.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
lib/pixi/assets/maps/osm/3/1/4.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
lib/pixi/assets/maps/osm/3/1/5.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
lib/pixi/assets/maps/osm/3/1/6.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
lib/pixi/assets/maps/osm/3/1/7.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/0.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/1.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/2.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/3.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/4.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/5.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/6.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
lib/pixi/assets/maps/osm/3/2/7.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
lib/pixi/assets/maps/osm/3/3/0.png
Normal file
After Width: | Height: | Size: 258 B |
BIN
lib/pixi/assets/maps/osm/3/3/1.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
lib/pixi/assets/maps/osm/3/3/2.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
lib/pixi/assets/maps/osm/3/3/3.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
lib/pixi/assets/maps/osm/3/3/4.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
lib/pixi/assets/maps/osm/3/3/5.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
lib/pixi/assets/maps/osm/3/3/6.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
lib/pixi/assets/maps/osm/3/3/7.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
lib/pixi/assets/maps/osm/3/4/0.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/4/1.png
Normal file
After Width: | Height: | Size: 203 B |
BIN
lib/pixi/assets/maps/osm/3/4/2.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
lib/pixi/assets/maps/osm/3/4/3.png
Normal file
After Width: | Height: | Size: 653 B |
BIN
lib/pixi/assets/maps/osm/3/4/4.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
lib/pixi/assets/maps/osm/3/4/5.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
lib/pixi/assets/maps/osm/3/4/6.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
lib/pixi/assets/maps/osm/3/4/7.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
lib/pixi/assets/maps/osm/3/5/0.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/5/1.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/5/2.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
lib/pixi/assets/maps/osm/3/5/3.png
Normal file
After Width: | Height: | Size: 237 B |
BIN
lib/pixi/assets/maps/osm/3/5/4.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/5/5.png
Normal file
After Width: | Height: | Size: 349 B |
BIN
lib/pixi/assets/maps/osm/3/5/6.png
Normal file
After Width: | Height: | Size: 570 B |
BIN
lib/pixi/assets/maps/osm/3/5/7.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
lib/pixi/assets/maps/osm/3/6/0.png
Normal file
After Width: | Height: | Size: 815 B |
BIN
lib/pixi/assets/maps/osm/3/6/1.png
Normal file
After Width: | Height: | Size: 984 B |
BIN
lib/pixi/assets/maps/osm/3/6/2.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
lib/pixi/assets/maps/osm/3/6/3.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
lib/pixi/assets/maps/osm/3/6/4.png
Normal file
After Width: | Height: | Size: 904 B |
BIN
lib/pixi/assets/maps/osm/3/6/5.png
Normal file
After Width: | Height: | Size: 952 B |
BIN
lib/pixi/assets/maps/osm/3/6/6.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
lib/pixi/assets/maps/osm/3/6/7.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
lib/pixi/assets/maps/osm/3/7/0.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/7/1.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/7/2.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/7/3.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/7/4.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/7/5.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/7/6.png
Normal file
After Width: | Height: | Size: 103 B |
BIN
lib/pixi/assets/maps/osm/3/7/7.png
Normal file
After Width: | Height: | Size: 103 B |
20
lib/pixi/assets/maps/pixabay/europe-license.txt
Normal file
@ -0,0 +1,20 @@
|
||||
author: Hans Braxmeier (https://pixabay.com/de/users/hans-2/)
|
||||
url: https://pixabay.com/de/illustrations/karte-mitteleuropa-zentraleuropa-1804891/
|
||||
license:
|
||||
Vereinfachte Pixabay Lizenz:
|
||||
|
||||
Unsere Lizenz gibt Nutzern umfassende Freiheiten und Sicherheit - und gleichzeitig schützt sie die Bildautoren und unsere Community vor Mißbrauch. Die rechtliche Seite halten wir dabei so einfach wie möglich und fassen alle wichtigen Punkte hier zusammen.
|
||||
|
||||
Was ist erlaubt?
|
||||
- Du darfst alle Pixabay Inhalte kostenlos nutzen, für kommerzielle und nicht-kommerzielle Anwendungen, gedruckt und digital. Beachte dabei die Einschränkungen unter "Was ist nicht erlaubt".
|
||||
- Du musst weder vom Bildautor noch von Pixabay eine Genehmigung einholen und auch eine Quellenangabe ist nicht erforderlich, wobei wir uns über eine freiwillige Nennung freuen.
|
||||
- Du darfst Pixabay Inhalte verändern.
|
||||
|
||||
|
||||
Was ist nicht erlaubt?
|
||||
|
||||
Dieser Abschnitt betrifft ausschließlich Bildnutzer und nicht die jeweiligen Bildautoren.
|
||||
- Verbreite oder verkaufe die Werke nicht auf anderen Bilder-, Wallpaper- bzw. Vertriebsseiten.
|
||||
- Verkaufe keine unveränderten Kopien von Bildern, z.B. auf einer Stockfoto-Seite, als Poster oder als Ausdruck auf einem physischen Produkt.
|
||||
- Erkennbare Personen dürfen nicht in einer anstößigen Weise dargestellt werden.
|
||||
- Suggeriere nicht, dass Bildautor oder erkennbare Personen bzw. Marken auf den Bildern Dein Produkt empfehlen oder befürworten.
|
BIN
lib/pixi/assets/maps/pixabay/europe.jpg
Normal file
After Width: | Height: | Size: 730 KiB |
After Width: | Height: | Size: 160 KiB |
@ -0,0 +1,4 @@
|
||||
lincense: Public Domain
|
||||
date: 29-10-2019
|
||||
source: https://commons.wikimedia.org/wiki/File:BlankMap-World.svg
|
||||
author: Canuckguy
|
@ -60,3 +60,52 @@ window.List = List
|
||||
window.LabeledGraphics = LabeledGraphics
|
||||
window.FontInfo = FontInfo
|
||||
window.Text = Text
|
||||
|
||||
//Maps
|
||||
import MapView from './maps/mapview.js'
|
||||
import { GeoMap, ImageMap, DeepZoomMap } from './maps/map.js'
|
||||
import { MapData, DeepZoomMapData } from './maps/mapdata.js'
|
||||
|
||||
window.MapData = MapData
|
||||
window.DeepZoomMapData = DeepZoomMapData
|
||||
|
||||
window.GeoMap = GeoMap
|
||||
window.ImageMap = ImageMap
|
||||
window.DeepZoomMap = DeepZoomMap
|
||||
|
||||
import Mercator from './maps/projections/mercator.js'
|
||||
import Robinson from './maps/projections/robinson.js'
|
||||
window.Projection = {
|
||||
Mercator,
|
||||
Robinson
|
||||
}
|
||||
|
||||
window.MapView = MapView
|
||||
|
||||
import MapApp from './maps/mapapp.js'
|
||||
|
||||
window.MapApp = MapApp
|
||||
|
||||
import { AdvancedScatterContainer, RigidContainer, CompactScatter, CoverScatter } from './maps/scatter.js'
|
||||
|
||||
window.AdvancedScatterContainer = AdvancedScatterContainer
|
||||
window.RigidContainer = RigidContainer
|
||||
window.CompactScatter = CompactScatter
|
||||
window.CoverScatter = CoverScatter
|
||||
|
||||
import { GeoLayer, MapLayer } from './maps/geolayer.js'
|
||||
window.GeoLayer = GeoLayer
|
||||
window.MapLayer = MapLayer
|
||||
|
||||
import { GeoGraphics, GeoPoint, GeoLine, GeoShape, GeoMultiShape } from './maps/geographics.js'
|
||||
|
||||
window.GeoGraphics = GeoGraphics
|
||||
window.GeoPoint = GeoPoint
|
||||
window.GeoLine = GeoLine
|
||||
window.GeoShape = GeoShape
|
||||
window.GeoMultiShape = GeoMultiShape
|
||||
|
||||
|
||||
import Overlay from "./maps/overlay.js"
|
||||
|
||||
window.Overlay = Overlay
|
||||
|
187
lib/pixi/graphics/label.js
Normal file
@ -0,0 +1,187 @@
|
||||
import { Points } from '../../utils.js'
|
||||
|
||||
/* globals PIXI */
|
||||
|
||||
export class FlagType {
|
||||
static get bottomLeft() {
|
||||
return { x: 1, y: -1 }
|
||||
}
|
||||
static get bottomRight() {
|
||||
return { x: -1, y: -1 }
|
||||
}
|
||||
static get topLeft() {
|
||||
return { x: -1, y: 1 }
|
||||
}
|
||||
static get topRight() {
|
||||
return { x: 1, y: 1 }
|
||||
}
|
||||
|
||||
static toString(flagType) {
|
||||
let str = ''
|
||||
if (flagType.x && flagType.y) {
|
||||
if (flagType.y == 1) str += 'bottom'
|
||||
else if (flagType.y == -1) str += 'top'
|
||||
else str += '_INVALID_Y_'
|
||||
|
||||
if (flagType.x == 1) str += 'Right'
|
||||
else if (flagType.x == -1) str += 'Left'
|
||||
else str += '_INVALID_X_'
|
||||
} else str = 'Invalid FlagType: ' + flagType.toString()
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
export class FlagPolygon extends PIXI.Polygon {
|
||||
constructor({
|
||||
type = FlagType.bottomLeft,
|
||||
width = 100,
|
||||
height = 30,
|
||||
notchSize = 10,
|
||||
notchWidth = null,
|
||||
notchHeight = null,
|
||||
originOffset = { x: 0, y: 0 }
|
||||
} = {}) {
|
||||
let points = []
|
||||
|
||||
let dimensions = { x: width, y: height }
|
||||
dimensions = Points.multiply(dimensions, type)
|
||||
|
||||
notchWidth = notchWidth == null ? notchSize : notchWidth
|
||||
notchHeight = notchHeight == null ? notchSize : notchHeight
|
||||
|
||||
notchSize = { x: notchWidth, y: notchHeight }
|
||||
notchSize = Points.multiply(notchSize, type)
|
||||
|
||||
originOffset = Points.multiply(originOffset, type)
|
||||
|
||||
let point = new PIXI.Point(originOffset.x, originOffset.y)
|
||||
points.push(point.clone())
|
||||
|
||||
point.y += notchSize.y
|
||||
points.push(point.clone())
|
||||
|
||||
point.y += dimensions.y
|
||||
points.push(point.clone())
|
||||
|
||||
point.x += dimensions.x
|
||||
points.push(point.clone())
|
||||
|
||||
point.y -= dimensions.y
|
||||
points.push(point.clone())
|
||||
|
||||
point.x -= dimensions.x - notchSize.x
|
||||
points.push(point.clone())
|
||||
|
||||
// close polygon
|
||||
points.push(points[0].clone())
|
||||
|
||||
super(points)
|
||||
|
||||
this.type = type
|
||||
this.dimensions = dimensions
|
||||
this.notchSize = notchSize
|
||||
this.originOffset = originOffset
|
||||
}
|
||||
|
||||
getPoint(i) {
|
||||
if (i >= 0) {
|
||||
let idx = i * 2
|
||||
return [this.points[idx], this.points[idx + 1]]
|
||||
} else {
|
||||
let idx = (Math.floor(this.points.length / 2) + i) * 2
|
||||
return [this.points[idx], this.points[idx + 1]]
|
||||
}
|
||||
}
|
||||
|
||||
get notch() {
|
||||
let points = [this.getPoint(0), this.getPoint(1), this.getPoint(-2), this.getPoint(0)]
|
||||
let notchPolygon = []
|
||||
|
||||
points.forEach(point => {
|
||||
notchPolygon = notchPolygon.concat(point)
|
||||
})
|
||||
|
||||
return notchPolygon
|
||||
}
|
||||
get rect() {
|
||||
let points = [this.getPoint(1), this.getPoint(2), this.getPoint(3), this.getPoint(4), this.getPoint(1)]
|
||||
|
||||
let rectPolygon = []
|
||||
points.forEach(point => {
|
||||
rectPolygon = rectPolygon.concat(point)
|
||||
})
|
||||
return rectPolygon
|
||||
}
|
||||
|
||||
placeText(text, padding) {
|
||||
text.position = Points.add(this.originOffset, { x: 0, y: this.notchSize.y })
|
||||
padding = Points.multiply(padding, this.type)
|
||||
text.position = Points.add(text.position, padding)
|
||||
|
||||
if (this.type.y == -1) text.position.y -= text.height
|
||||
|
||||
if (this.type.x == -1) text.position.x -= text.width
|
||||
}
|
||||
}
|
||||
|
||||
export class Flag extends PIXI.Graphics {
|
||||
constructor(
|
||||
{
|
||||
type = FlagType.bottomLeft,
|
||||
width = 100,
|
||||
height = 30,
|
||||
notchSize = 10,
|
||||
notchWidth = null,
|
||||
notchHeight = null,
|
||||
originOffset = { x: 0, y: 0 }
|
||||
} = {},
|
||||
nativeLines = false
|
||||
) {
|
||||
super(nativeLines)
|
||||
|
||||
this.flagPolygon = new FlagPolygon({
|
||||
type,
|
||||
width,
|
||||
height,
|
||||
notchSize,
|
||||
notchWidth,
|
||||
notchHeight,
|
||||
originOffset
|
||||
})
|
||||
|
||||
this.draw()
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.drawPolygon(this.flagPolygon)
|
||||
}
|
||||
|
||||
get typeName() {
|
||||
return FlagType.toString(this.type)
|
||||
}
|
||||
}
|
||||
|
||||
export class Label extends PIXI.Graphics {
|
||||
constructor(text, textStyle = new PIXI.TextStyle(), nativeLines = false) {
|
||||
super(nativeLines)
|
||||
this._text = new PIXI.Text(text, textStyle)
|
||||
this.addChild(this._text)
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this._text
|
||||
}
|
||||
}
|
||||
|
||||
export class FlagLabel extends Label {
|
||||
constructor(text, textStyle, flagStyle, nativeLines = false) {
|
||||
super(text, textStyle, nativeLines)
|
||||
this.flagPolygon = new FlagPolygon(flagStyle)
|
||||
this.draw()
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.drawPolygon(this.flagPolygon)
|
||||
}
|
||||
}
|
287
lib/pixi/maps/geographics.html
Normal file
@ -0,0 +1,287 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>GeoGraphics</title>
|
||||
|
||||
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../../../css/doctest.css'>
|
||||
|
||||
<script src="../../../dist/iwmlib.3rdparty.js"></script>
|
||||
<script src="../../../dist/iwmlib.js"></script>
|
||||
<script src="../../../dist/iwmlib.pixi.js"></script>
|
||||
|
||||
|
||||
<style>
|
||||
.inline-showcase {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.map-example {
|
||||
display: inline-block;
|
||||
width: 256px;
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<script>
|
||||
let capitals = {
|
||||
abidjan: { x: 5.349470, y: -4.006472 },
|
||||
berlin: { x: 52.525430, y: 13.385291 },
|
||||
canberra: { x: -35.282025, y: 149.128648 },
|
||||
capetown: { x: -33.925448, y: 18.416962 },
|
||||
moscow: { x: 55.750892, y: 37.622799 },
|
||||
washington: { x: 38.895650, y: -77.031407 },
|
||||
rio: { x: -22.871400, y: -43.280490 },
|
||||
tokio: { x: 35.696278, y: 139.731366 }
|
||||
}
|
||||
|
||||
function createApp(view) {
|
||||
let app = new MapApp({
|
||||
view,
|
||||
focus: { x: 0, y: 0 },
|
||||
zoom: 1,
|
||||
width: 512,
|
||||
height: 512,
|
||||
coordsLogging: true
|
||||
})
|
||||
|
||||
const osmworld = "../assets/maps/osm/0/0/0.png"
|
||||
const wikimedia = "../assets/maps/wikimedia-world-robinson/2000px-BlankMap-World.png"
|
||||
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject("Creating app timed out.")
|
||||
}, 3000)
|
||||
|
||||
app.loadSprites([
|
||||
osmworld,
|
||||
wikimedia
|
||||
], (sprites) => {
|
||||
|
||||
let osmMap = new ImageMap(sprites.get(osmworld), new MapData(new Projection.Mercator()), { cover: false })
|
||||
|
||||
let wikimediaMap = new ImageMap(sprites.get(wikimedia), new MapData(new Projection.Robinson(10)), {
|
||||
baseZoomHeight: sprites.get(osmworld).texture.height,
|
||||
cover: false
|
||||
})
|
||||
|
||||
console.log(wikimediaMap)
|
||||
|
||||
app.addMaps({
|
||||
"osm": osmMap, "wiki": wikimediaMap
|
||||
})
|
||||
app.selectMap("osm")
|
||||
app.setup().run()
|
||||
resolve(app)
|
||||
}, { resolutionDependent: false })
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
function enableSwitch(button, app) {
|
||||
button.addEventListener("click", () => {
|
||||
let next = (app.activeMapKey == "osm") ? "wiki" : "osm"
|
||||
console.log("CLICKED")
|
||||
|
||||
app.selectMap(next)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<body onload='Doctest.run()'>
|
||||
<h1>GeoGraphics</h1>
|
||||
<p> GeoGraphics are graphical objects, that does not store the graphics information
|
||||
in screen space, but in geographical coordinates. Therefore GeoGraphics must be
|
||||
placed on GeoLayers to work properly. </p>
|
||||
<p><i>
|
||||
Note: As GeoLayers are always children of a map layer. When the map is changed
|
||||
all GeoLayers are notified via the 'adaptTo(map)' method.</i></p>
|
||||
<p>
|
||||
The geolayers forward this 'adaptTo' to all children that are GeoGraphics.
|
||||
Which adjust their so called 'point' data to the new map.</p>
|
||||
</ul>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#geopoint">Gep Point</a></li>
|
||||
<li><a href="#geoline">Geo Line</a></li>
|
||||
<li><a href="#geoshape">Geo Shape</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<!--GeoPoint -->
|
||||
<section id="geopoint">
|
||||
<h2>GeoPoint</h2>
|
||||
<p>GeoPoint is a single coordinate in the map.</p>
|
||||
<canvas id="geopoint_canvas"></canvas>
|
||||
<div class=" controls">
|
||||
<button id="geopoint_switch">Change Map</button>
|
||||
</div>
|
||||
|
||||
|
||||
<script class="doctest">
|
||||
|
||||
; (function () {
|
||||
createApp(geopoint_canvas).then(app => {
|
||||
let capitalLayer = new GeoLayer({ name: "Capital Overlay" })
|
||||
for (key in capitals) {
|
||||
let capitalPoint = new GeoPoint(capitals[key], {
|
||||
|
||||
/**
|
||||
* To style GeoGraphics, the fill has to be set in the
|
||||
* onDraw callback.
|
||||
*
|
||||
* Note: GeoPoints are the only GeoGraphic with no
|
||||
* initial geometry attached. These have to be drawn to
|
||||
* the graphics object manually.
|
||||
*/
|
||||
onDraw: function () {
|
||||
this.graphics.beginFill(0xFF0000)
|
||||
this.graphics.drawCircle(0, 0, 5)
|
||||
this.graphics.endFill()
|
||||
}
|
||||
})
|
||||
capitalLayer.place(capitalPoint)
|
||||
}
|
||||
|
||||
enableSwitch(geopoint_switch, app)
|
||||
|
||||
app.mapLayer.place(capitalLayer)
|
||||
}).catch(console.error)
|
||||
})()
|
||||
|
||||
</script>
|
||||
</section>
|
||||
<section id="geoline">
|
||||
<h2>GeoLine</h2>
|
||||
<p>Geo line is a set of points, that are rendered as a line and can be updated individually.</p>
|
||||
<canvas id="geoline_canvas"></canvas>
|
||||
<div class="controls">
|
||||
<button id="geoline_switch">Change Map</button>
|
||||
<label>closed
|
||||
<input type="checkbox" id="geoline_close_toggle">
|
||||
</label>
|
||||
</div>
|
||||
<script class="doctest">
|
||||
|
||||
; (function () {
|
||||
createApp(geoline_canvas).then(app => {
|
||||
let overlay = new GeoLayer()
|
||||
|
||||
//// You can initialize the GeoLine with a set of coordinates.
|
||||
let newYorkRooseveltAirField = { x: 40.738473, y: -73.613131 }
|
||||
let aeroport_de_Paris_le_bourget = { x: 48.960373, y: 2.436891 }
|
||||
|
||||
// Instantiate the geo line.
|
||||
let lindberghTransatlanticFlight = new GeoLine([
|
||||
newYorkRooseveltAirField,
|
||||
aeroport_de_Paris_le_bourget
|
||||
], {
|
||||
// Define how the line should be drawn.
|
||||
// Note: This is called everytime the GeoGraphic
|
||||
// has to adapt.
|
||||
onDraw: function () {
|
||||
this.graphics.lineStyle(3, 0xf8baea)
|
||||
}
|
||||
})
|
||||
|
||||
// Just place it on a GeoLayer.
|
||||
overlay.place(lindberghTransatlanticFlight)
|
||||
|
||||
|
||||
// Another option is to add points with the *addPoint()* method.
|
||||
// This is useful for dynamic lines.
|
||||
let geoline = new GeoLine([], {
|
||||
onDraw: function () {
|
||||
this.graphics.lineStyle(3, 0x00cc54, 0.5)
|
||||
}
|
||||
})
|
||||
overlay.place(geoline)
|
||||
for (let [name, coordinates] of Object.entries(capitals)) {
|
||||
geoline.addPoint(coordinates)
|
||||
}
|
||||
|
||||
// Don't forget to add the geolayer to the maplayer.
|
||||
app.mapLayer.place(overlay)
|
||||
|
||||
enableSwitch(geoline_switch, app)
|
||||
|
||||
geoline_close_toggle.addEventListener("input", () => {
|
||||
|
||||
geoline.closed = geoline_close_toggle.checked
|
||||
})
|
||||
|
||||
}).catch(console.error)
|
||||
})()
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</section>
|
||||
|
||||
<section id="geoshape">
|
||||
|
||||
<h2>Geoshape</h2>
|
||||
<p>GeoGraphics represent Polygons, that are drawn on a map. If the map change, the graphic adapts to the new map
|
||||
and represents
|
||||
the same geographical shape.</p>
|
||||
<div class="inline-showcase">
|
||||
<div class="map-example">
|
||||
<canvas id="geoshape_canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<button id="geoshape_switch">Change Map</button>
|
||||
|
||||
<script class='doctest'>
|
||||
|
||||
|
||||
(function () {
|
||||
createApp(geoshape_canvas).then((app) => {
|
||||
|
||||
let triangle = [
|
||||
[capitals.berlin,
|
||||
capitals.moscow,
|
||||
capitals.tokio,
|
||||
capitals.canberra,
|
||||
capitals.capetown]
|
||||
]
|
||||
|
||||
let countryLayer = new GeoLayer({ name: "Country Layer" })
|
||||
|
||||
|
||||
let shape = new GeoShape(triangle, {
|
||||
onDraw: function () {
|
||||
this.graphics.beginFill(0xFF0000)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* The following click handler should trigger when either the
|
||||
* shape or a hole is clicked. This is the default and intended
|
||||
* behaviour, as holes are considered to be contained by the
|
||||
* desired geometry and should not interefere with any userinteraction.
|
||||
*/
|
||||
shape.graphics.interactive = true
|
||||
shape.graphics.on("pointerdown", () => {
|
||||
app.showNotification("Shape was clicked!")
|
||||
})
|
||||
|
||||
countryLayer.place(shape)
|
||||
app.mapLayer.place(countryLayer)
|
||||
|
||||
enableSwitch(geoshape_switch, app)
|
||||
|
||||
|
||||
|
||||
}).catch(console.error)
|
||||
})()
|
||||
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
737
lib/pixi/maps/geographics.js
Normal file
@ -0,0 +1,737 @@
|
||||
import { Points } from '../../utils.js'
|
||||
import { EventHandler } from './utils.js'
|
||||
import { FlagPolygon } from '../graphics/label.js'
|
||||
import { GeoLayer, MapLayer } from './geolayer.js'
|
||||
import { DeepZoomMap } from './map.js'
|
||||
|
||||
/**
|
||||
* GeoGraphics are graphical objects, that does not store the graphics information
|
||||
* in screen space, but in geographical coordinates. Therefore GeoGraphics must be
|
||||
* placed on GeoLayers to work properly.
|
||||
*
|
||||
* (Note: As GeoLayers are always children of a map layer. When the map is changed
|
||||
* all GeoLayers are notified via the 'adaptTo(map)' method.)
|
||||
*
|
||||
* The geolayers forward this 'adaptTo' to all children that are GeoGraphics.
|
||||
* Which adjust their so called 'point' data to the new map.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export class GeoGraphics {
|
||||
constructor(coordinates, { scale = 1, onDraw = null, onDrawEnd = null, debug = false } = {}) {
|
||||
this.coordinates = coordinates
|
||||
this.debug = debug
|
||||
this.graphics = new PIXI.Graphics()
|
||||
this.scale = scale
|
||||
this.drawHandler = new EventHandler('onDraw', { listeners: onDraw })
|
||||
this.drawEndHandler = new EventHandler('onDrawEnd', { listeners: onDrawEnd })
|
||||
this._points = null
|
||||
this._position = null
|
||||
}
|
||||
|
||||
clone() {
|
||||
console.error(`Call of abstract method clone(). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
_cloneOptions() {
|
||||
return {
|
||||
debug: this.debug,
|
||||
scale: this.scale
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The _adaptCoordinates is called first by the adaptTo Method.
|
||||
* Here all coordinates are transformed into point coordinates.
|
||||
* This must be overloaded in subclass.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
_adaptCoordinates(map) {
|
||||
console.error(`Call of abstract method _adaptCoordinates(map). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all screen points in a single array.
|
||||
*
|
||||
* @abstract
|
||||
* @returns {array} - Array of all points in the GeoGraphic.
|
||||
*/
|
||||
_getPoints() {
|
||||
console.error(`Call of abstract method _getPoints(func). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulates all points depending on a function.
|
||||
* Mainly used to transform points to local space.
|
||||
*
|
||||
* @abstract
|
||||
* @param {function} func
|
||||
* @memberof GeoGraphics
|
||||
*/
|
||||
_manipulatePoints(func) {
|
||||
console.error(`Call of abstract method _manipulatePoints(func). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* The _draw method is called last on adaptation. It creates the GraphicData
|
||||
* of the specified subclass. To manipulate the style of the graphic, hook an onDraw listener
|
||||
* to the GeoGraphics object. It is called before the _draw and lets the user modify color and
|
||||
* lineStyle of the drawn object.
|
||||
*
|
||||
* Note: It could also be used for more radical manipulations on the graphics object.
|
||||
* But this should be used with care.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
_draw() {
|
||||
console.error(`Call of abstract method _draw(). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the containing geo layer, when the map changes.
|
||||
*/
|
||||
adaptTo(map) {
|
||||
this._points = this._adaptCoordinates(map)
|
||||
this._updatePosition()
|
||||
this.draw()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Redraws the graphics.
|
||||
*
|
||||
* This should be only called if you require an redraw independent of an adapt.
|
||||
*
|
||||
* @memberof GeoGraphics
|
||||
*/
|
||||
draw() {
|
||||
this._prepareDraw()
|
||||
this.drawHandler.call(this, this.graphics)
|
||||
this._draw()
|
||||
this.drawEndHandler.call(this, this.graphics)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the point data.
|
||||
* Note: This data changes on adaptation.
|
||||
*/
|
||||
get points() {
|
||||
return this._points
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen position of the GeoGraphics.
|
||||
*/
|
||||
get position() {
|
||||
return this._position
|
||||
}
|
||||
|
||||
get map() {
|
||||
if (
|
||||
this.graphics.layer &&
|
||||
(this.graphics.layer instanceof GeoLayer || this.graphics.layer instanceof MapLayer)
|
||||
) {
|
||||
return this.graphics.layer.map
|
||||
} else return null
|
||||
}
|
||||
|
||||
get mapLayer() {
|
||||
if (
|
||||
this.graphics.layer &&
|
||||
(this.graphics.layer instanceof GeoLayer || this.graphics.layer instanceof MapLayer)
|
||||
) {
|
||||
return this.graphics.layer.mapLayer
|
||||
} else return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare draw is a private function, that prepares the graphics
|
||||
* for the next draw call. It also fires the drawHandler.
|
||||
*
|
||||
* @private
|
||||
* @memberof GeoGraphics
|
||||
*/
|
||||
_prepareDraw() {
|
||||
this.graphics.clear()
|
||||
/*
|
||||
Set a fillcolor and a stroke style for
|
||||
debugging. Can be overloaded using the onDraw
|
||||
event function.
|
||||
*/
|
||||
if (this.debug) this.graphics.beginFill(0xff00ff)
|
||||
}
|
||||
|
||||
_updatePosition() {
|
||||
let points = this._getPoints()
|
||||
this._position = GeoGraphics.calculateCenterOfMass(points)
|
||||
this._manipulatePoints(point => {
|
||||
point = Points.subtract(point, this._position)
|
||||
return point
|
||||
})
|
||||
this.graphics.position = this._position
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the center of Mass for a set of points.
|
||||
*
|
||||
* @static
|
||||
* @param {Array} points - Array of points in the format {x: a, y:b}
|
||||
* @returns {object} - Returns a point containing the center of mass of the polygon.
|
||||
* @memberof GeoGraphics
|
||||
*/
|
||||
static calculateCenterOfMass(points) {
|
||||
let com = new PIXI.Point()
|
||||
points.forEach(p => {
|
||||
let point = new PIXI.Point(p.x, p.y)
|
||||
com = Points.add(com, point)
|
||||
})
|
||||
return Points.multiplyScalar(com, 1 / points.length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single point on the Map.
|
||||
*
|
||||
* This GeoGraphics does not provide any visual representation.
|
||||
* Draw the desired shape in the onDraw callback.
|
||||
*/
|
||||
export class GeoPoint extends GeoGraphics {
|
||||
clone() {
|
||||
return new GeoPoint(this.coordinates, this._cloneOptions())
|
||||
}
|
||||
|
||||
_adaptCoordinates(map) {
|
||||
let scale = 1
|
||||
|
||||
if (this.mapLayer.map instanceof DeepZoomMap) {
|
||||
scale = this.mapLayer.map.image.scale.x
|
||||
}
|
||||
|
||||
scale = scale / 4
|
||||
|
||||
return map.coordinatesToPoint(this.coordinates)
|
||||
}
|
||||
|
||||
_getPoints() {
|
||||
return [this.points]
|
||||
}
|
||||
|
||||
_manipulatePoints(func) {
|
||||
this._points = func(this._points)
|
||||
}
|
||||
|
||||
_draw() {}
|
||||
}
|
||||
|
||||
export class GeoLine extends GeoGraphics {
|
||||
/**
|
||||
* @param {object} opts - Optional values
|
||||
* @param {array} [opts.points=[]] - Initial points of the geo shape.
|
||||
* @param {boolean} [closed=false] - Defines if the
|
||||
*/
|
||||
constructor(coordinates, { closed = false, size = 1, onDraw = null } = {}) {
|
||||
super(coordinates, {
|
||||
size,
|
||||
onDraw
|
||||
})
|
||||
|
||||
this._closed = closed
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new GeoLine(this.coordinates, this._cloneOptions)
|
||||
}
|
||||
|
||||
_cloneOptions() {
|
||||
let options = super._cloneOptions()
|
||||
Object.assign(options, {
|
||||
closed: this.closed
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a point to the geo line.
|
||||
*/
|
||||
addPoint(coordinate) {
|
||||
this.coordinates.push(coordinate)
|
||||
}
|
||||
|
||||
_manipulatePoints(func) {
|
||||
this.points.forEach((point, idx, array) => {
|
||||
array[idx] = func(point)
|
||||
})
|
||||
}
|
||||
|
||||
_getPoints() {
|
||||
return this.points
|
||||
}
|
||||
|
||||
_adaptCoordinates(map) {
|
||||
let points = []
|
||||
this.coordinates.forEach(point => {
|
||||
points.push(map.coordinatesToPoint(point))
|
||||
})
|
||||
return points
|
||||
}
|
||||
|
||||
_prepareDraw() {
|
||||
this.graphics.clear()
|
||||
if (this.debug) this.graphics.lineStyle(0.5, 0xff00ff)
|
||||
this.drawHandler.call(this)
|
||||
}
|
||||
|
||||
_draw() {
|
||||
/**
|
||||
* This resets the fill.
|
||||
*
|
||||
* DISCUSS: SO: "I'm not sure how the line should be defined.
|
||||
* On the one hand. The line is clearly intended to
|
||||
* represent a line and not an area. On the other hand,
|
||||
* why should the user be prevented from using a fill for the
|
||||
* area within the line. But if he want's a fill, why don't take
|
||||
* a Polygon in the first place?
|
||||
*
|
||||
* (But if it's a predefined GeoJSON object obtained through e.g. elasticsearch,
|
||||
* then the user is not in full control of the object type and it may be a good
|
||||
* addition to grant the user this additional design choice.)
|
||||
*
|
||||
* The opportunity to do so would result in additional conditions, when creating the
|
||||
* GeoGraphics of an overlay."
|
||||
* */
|
||||
this.graphics.beginFill(0, 0)
|
||||
|
||||
if (this.points.length > 0) {
|
||||
this.graphics.moveTo(this.points[0].x, this.points[0].y)
|
||||
|
||||
for (let i = 1; i < this.points.length; i++) {
|
||||
this.graphics.lineTo(this.points[i].x, this.points[i].y)
|
||||
}
|
||||
|
||||
if (this.closed) {
|
||||
this.graphics.lineTo(this.points[0].x, this.points[0].y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get closed() {
|
||||
return this._closed
|
||||
}
|
||||
|
||||
set closed(val) {
|
||||
if (val != this._closed) {
|
||||
this._closed = val
|
||||
this.draw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class GeoShape extends GeoGraphics {
|
||||
clone() {
|
||||
return new GeoShape(this.coordinates, this._cloneOptions)
|
||||
}
|
||||
|
||||
_manipulatePoints(func) {
|
||||
this.constructor._manipulatePoints(this.points, func)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mets the requirements for the _manipulatePointsMethod().
|
||||
* Primarily used in subclasses to get the processing steps from
|
||||
* their superclass.
|
||||
*
|
||||
* @static
|
||||
* @protected
|
||||
* @param {Array.<PIXI.Points>} points - The points array that shold be manipulated.
|
||||
* @param {function} func - The function that changes the single point value. Has to return a new point.
|
||||
* @memberof GeoShape
|
||||
*/
|
||||
static _manipulatePoints(points, func) {
|
||||
points.forEach((pointArray, arrIdx) => {
|
||||
pointArray.forEach((point, idx) => {
|
||||
points[arrIdx][idx] = func(point)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getPoints() {
|
||||
return this.constructor._getPointsFrom(this.points)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all points of a polygon array.
|
||||
* Useful for when getting sub polygons in child class.
|
||||
*/
|
||||
static _getPointsFrom(shape) {
|
||||
let concatArray = []
|
||||
shape.forEach(array => {
|
||||
concatArray = concatArray.concat(array)
|
||||
})
|
||||
|
||||
return concatArray
|
||||
}
|
||||
|
||||
_adaptCoordinates(map) {
|
||||
let val = this.constructor._adaptPoint(this.coordinates, map)
|
||||
return val
|
||||
}
|
||||
|
||||
static _adaptPoint(coordinates, map) {
|
||||
return coordinates.map(array => {
|
||||
return array.map(point => {
|
||||
return map.coordinatesToPoint(point)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_draw() {
|
||||
this._drawFrom(this.points)
|
||||
this.graphics.position = this.position
|
||||
}
|
||||
|
||||
_drawFrom(shape) {
|
||||
const { polygon, hole } = this.constructor._pointsToShape(shape)
|
||||
this._drawShape(polygon, hole)
|
||||
}
|
||||
|
||||
/**
|
||||
* Separates the points array into it's two parts:
|
||||
* - the solid polygon
|
||||
* - a hole that is cut into the polygon (optional)
|
||||
*
|
||||
* @private
|
||||
* @returns {object} - Returns an object containing the polygon and the hole in the form of {polygon: [...PIXI.Point], hole: [...PIXI.Point]}
|
||||
*/
|
||||
static _pointsToShape(points) {
|
||||
let polygon = GeoShape._transformToPIXI(points[0])
|
||||
let hole = points[1] ? GeoShape._transformToPIXI(points[1]) : []
|
||||
return { polygon, hole }
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an array of poins into an array of PIXI.Points.
|
||||
* Note: When creating PIXI.Polygons, for some reason the points
|
||||
* need to be a PIXI.Points object (at least the first one).
|
||||
*
|
||||
* @param {array} points - Points in the form of {x:a,y:b}
|
||||
* @returns An array of PIXI.Points
|
||||
* @memberof GeoPolygon
|
||||
*/
|
||||
static _transformToPIXI(points = []) {
|
||||
let polygon = []
|
||||
points.forEach(point => {
|
||||
polygon.push(new PIXI.Point(point.x, point.y))
|
||||
})
|
||||
|
||||
return polygon
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a single shape onto the graphics object.
|
||||
* Useful when being called from subclass.
|
||||
*
|
||||
* @protected
|
||||
* @param {Array.<PIXI.Point>} polygon - An array of PIXI.Points for drawing a polygon.
|
||||
* @param {Array.<PIXI.Point>} [hole=[]] - An Array of PIXI.Points for cutting a hole into the polygon
|
||||
* @memberof GeoShape
|
||||
*/
|
||||
_drawShape(polygon, hole = []) {
|
||||
// We save the fill specified in the onDraw event handler.
|
||||
//
|
||||
// Consider: Maybe it would be a good idea to add a 'onHoleDraw'
|
||||
// callback to make the hole customizable. Maybe you want
|
||||
// to fill it with a different color or an mediocre alpha value.
|
||||
// then feel free to implement it.
|
||||
let { fill, alpha } = PIXIUtils.saveFill(this.graphics)
|
||||
|
||||
/**
|
||||
* This may seem redundant, but it's required
|
||||
* to make the hole clickable.
|
||||
*
|
||||
* It was a bit confusing, so I made a CodePen
|
||||
* for it: https://codepen.io/Ukmasmu/pen/WJEaoK
|
||||
*/
|
||||
if (hole.length > 0) {
|
||||
this.graphics.beginFill(0x0000ff, 0.0000001)
|
||||
this.graphics.drawPolygon(hole)
|
||||
}
|
||||
|
||||
this.graphics.beginFill(fill, alpha)
|
||||
this.graphics.drawPolygon(polygon)
|
||||
|
||||
if (hole.length > 0) {
|
||||
this.graphics.beginHole()
|
||||
this.graphics.drawPolygon(hole)
|
||||
this.graphics.endHole()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The MultiGraphics makes use of the other GeoGraphics to
|
||||
* create a single graphics element out of multiple different
|
||||
* GeoGraphics.
|
||||
*/
|
||||
class GeoMultiGraphics extends GeoGraphics {
|
||||
/**
|
||||
* The _adaptCoordinates is called first by the adaptTo Method.
|
||||
* Here all coordinates are transformed into point coordinates.
|
||||
* This must be overloaded in subclass.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
_adaptCoordinates(map) {}
|
||||
|
||||
/**
|
||||
* Gets all screen points in a single array.
|
||||
*
|
||||
* @abstract
|
||||
* @returns {array} - Array of all points in the GeoGraphic.
|
||||
*/
|
||||
_getPoints() {
|
||||
console.error(`Call of abstract method _getPoints(func). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulates all points depending on a function.
|
||||
* Mainly used to transform points to local space.
|
||||
*
|
||||
* @abstract
|
||||
* @param {function} func
|
||||
* @memberof GeoGraphics
|
||||
*/
|
||||
_manipulatePoints(func) {
|
||||
console.error(`Call of abstract method _manipulatePoints(func). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* The _draw method is called last on adaptation. It creates the GraphicData
|
||||
* of the specified subclass. To manipulate the style of the graphic, hook an onDraw listener
|
||||
* to the GeoGraphics object. It is called before the _draw and lets the user modify color and
|
||||
* lineStyle of the drawn object.
|
||||
*
|
||||
* Note: It could also be used for more radical manipulations on the graphics object.
|
||||
* But this should be used with care.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
_draw() {
|
||||
console.error(`Call of abstract method _draw(). Overwrite in subclass.`, this)
|
||||
}
|
||||
}
|
||||
|
||||
export class GeoText extends GeoPoint {
|
||||
constructor(coordinates, text, opts) {
|
||||
super(coordinates, opts)
|
||||
this.align = opts.align
|
||||
this.textStyle = Object.assign(new PIXI.TextStyle(), opts.textStyle)
|
||||
this._text = new PIXI.Text(text, this.textStyle)
|
||||
|
||||
//TODO: Make this more generic:
|
||||
// We have 8 layers (12-20) for each map so this temporarily works fine.
|
||||
// Outsource it to the map class.
|
||||
//let textScale = Math.pow(2, 7)
|
||||
|
||||
// let textScale = 5
|
||||
// this.text.scale.set(1 / textScale, 1 / textScale)
|
||||
|
||||
switch (this.align) {
|
||||
case 'left':
|
||||
break
|
||||
case 'center':
|
||||
this.text.position.x -= this.text.width / 2
|
||||
break
|
||||
case 'right':
|
||||
this.text.position.x -= this.text.width
|
||||
break
|
||||
default:
|
||||
console.error('Align type is not implemented: ' + this.align + ". Use 'left', 'right' or 'center'.")
|
||||
}
|
||||
this.text.position.y -= this.text.height / 2
|
||||
this.graphics.addChild(this.text)
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this._text
|
||||
}
|
||||
|
||||
_draw() {
|
||||
super._draw()
|
||||
}
|
||||
|
||||
adaptTo(map) {
|
||||
super.adaptTo(map)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A geotext with a notch at a certain Position.
|
||||
*/
|
||||
export class GeoFlagLabel extends GeoText {
|
||||
constructor(coordinates, text, opts) {
|
||||
super(
|
||||
coordinates,
|
||||
text,
|
||||
Object.assign(opts, {
|
||||
align: 'left'
|
||||
})
|
||||
)
|
||||
|
||||
if (!opts.flag) {
|
||||
opts.flag = {}
|
||||
}
|
||||
|
||||
const {
|
||||
alpha = 1,
|
||||
color = 0xffffff,
|
||||
borderAlpha = 1,
|
||||
borderColor = 0x000000,
|
||||
borderThickness = 0,
|
||||
fillNotch = false
|
||||
} = opts
|
||||
|
||||
Object.assign(this, {
|
||||
alpha,
|
||||
color,
|
||||
fillNotch,
|
||||
borderAlpha,
|
||||
borderColor,
|
||||
borderThickness
|
||||
})
|
||||
|
||||
this.flagOptions = Object.assign(
|
||||
{
|
||||
notchSize: 10,
|
||||
notchWidth: null,
|
||||
notchHeight: null,
|
||||
padding: { x: 0, y: 0 },
|
||||
originOffset: { x: 0, y: 0 }
|
||||
},
|
||||
opts.flag
|
||||
)
|
||||
|
||||
this.border = this.borderThickness > 0
|
||||
|
||||
Object.assign(this.flagOptions, {
|
||||
width: this.text.width + this.flagOptions.padding.x * 2,
|
||||
height: this.text.height + this.flagOptions.padding.y * 2
|
||||
})
|
||||
|
||||
this.flagPolygon = new FlagPolygon(this.flagOptions)
|
||||
|
||||
// hover effect
|
||||
//--------------------
|
||||
const duration = 0.2
|
||||
this.graphics.interactive = true
|
||||
this.graphics.on('pointerover', event => {
|
||||
TweenLite.to(this.graphics, duration, { alpha: 0.6 })
|
||||
})
|
||||
this.graphics.on('pointerout', event => {
|
||||
TweenLite.to(this.graphics, duration, { alpha: 1 })
|
||||
})
|
||||
this.graphics.on('pointerupoutside', event => {
|
||||
TweenLite.to(this.graphics, duration, { alpha: 1 })
|
||||
})
|
||||
this.graphics.on('pointercancel', event => {
|
||||
TweenLite.to(this.graphics, duration, { alpha: 1 })
|
||||
})
|
||||
this.graphics.on('pointertap', event => {
|
||||
TweenLite.to(this.graphics, duration, { alpha: 1 })
|
||||
})
|
||||
// this.graphics.on('pointerdown', event => {
|
||||
// TweenLite.to(this.graphics, duration, { alpha: 0.45 })
|
||||
// })
|
||||
// this.graphics.on('pointerup', event => {
|
||||
// TweenLite.to(this.graphics, duration, { alpha: 0.6 })
|
||||
// })
|
||||
}
|
||||
|
||||
_draw() {
|
||||
super._draw()
|
||||
|
||||
this.graphics.beginFill(this.color, this.alpha)
|
||||
this.graphics.drawPolygon(this.flagPolygon)
|
||||
this.flagPolygon.placeText(this.text, this.flagOptions.padding)
|
||||
|
||||
if (this.border) {
|
||||
const lineStyle = [this.borderThickness, this.borderColor, this.borderAlpha]
|
||||
if (this.fillNotch) {
|
||||
this.graphics.beginFill(this.borderColor)
|
||||
this.graphics.drawPolygon(this.flagPolygon.notch)
|
||||
this.graphics.endFill()
|
||||
|
||||
this.graphics.lineStyle(...lineStyle)
|
||||
this.graphics.drawPolygon(this.flagPolygon.rect)
|
||||
this.graphics.drawPolygon(this.flagPolygon.notch)
|
||||
} else {
|
||||
this.graphics.lineStyle(...lineStyle)
|
||||
this.graphics.drawPolygon(this.flagPolygon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class GeoMultiShape extends GeoShape {
|
||||
static _manipulatePoints(points, func) {
|
||||
points.forEach(shape => {
|
||||
GeoShape._manipulatePoints(shape, func)
|
||||
})
|
||||
}
|
||||
|
||||
static _getPointsFrom(multiShapeArray) {
|
||||
let points = []
|
||||
multiShapeArray.forEach(shape => {
|
||||
points = points.concat(GeoShape._getPointsFrom(shape))
|
||||
})
|
||||
return points
|
||||
}
|
||||
|
||||
static _adaptPoint(coordinates, map) {
|
||||
let points = []
|
||||
coordinates.forEach(shape => {
|
||||
let adaptedPoint = GeoShape._adaptPoint(shape, map)
|
||||
points.push(adaptedPoint)
|
||||
})
|
||||
return points
|
||||
}
|
||||
|
||||
calculateLocation() {
|
||||
let coms = []
|
||||
this.coordinates.forEach(polygon => {
|
||||
coms.push(GeoGraphics.calculateCenterOfMass(polygon))
|
||||
})
|
||||
|
||||
return GeoGraphics.calculateCenterOfMass(coms)
|
||||
}
|
||||
|
||||
_drawFrom(multiShape) {
|
||||
multiShape.forEach(shape => {
|
||||
super._drawFrom(shape)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class PIXIUtils {
|
||||
/*
|
||||
* Transform a pixi text to it's actual screensize,
|
||||
* ignoring it's local transforms
|
||||
*/
|
||||
static toScreenFontSize(pixiText, fontSize = null) {
|
||||
pixiText._recursivePostUpdateTransform()
|
||||
|
||||
let normalizedScale = {
|
||||
x: pixiText.scale.x / pixiText.transform.worldTransform.a,
|
||||
y: pixiText.scale.x / pixiText.transform.worldTransform.d
|
||||
}
|
||||
|
||||
pixiText.scale = { x: normalizedScale.x, y: normalizedScale.y }
|
||||
if (fontSize) pixiText.style.fontSize = fontSize
|
||||
}
|
||||
|
||||
static saveFill(graphics) {
|
||||
return {
|
||||
fill: graphics.fill.color,
|
||||
alpha: graphics.fill.alpha
|
||||
}
|
||||
}
|
||||
}
|
380
lib/pixi/maps/geojson.html
Normal file
@ -0,0 +1,380 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>GeoJson</title>
|
||||
<link rel='stylesheet' href='../../iwmlib/lib/3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../../iwmlib/lib/../css/doctest.css'>
|
||||
<script src='../../iwmlib/lib/3rdparty/highlight/highlight.pack.js'></script>
|
||||
<script src='../../iwmlib/lib/3rdparty/all.js'></script>
|
||||
<script src='../../iwmlib/lib/all.js'></script>
|
||||
<script src='../../iwmlib/lib/pixi/all.js'></script>
|
||||
<script src="../all.js"></script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Material Icons";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.eot);
|
||||
/* For IE6-8 */
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.woff2) format('woff2'),
|
||||
url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.woff) format('woff'),
|
||||
url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.inline-showcase {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.map-example {
|
||||
display: inline-block;
|
||||
width: 256px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 2px 3px rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
|
||||
background: #333;
|
||||
padding: 40px;
|
||||
margin: 20px auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
thead td {
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
span.implemented {
|
||||
content: "";
|
||||
color: greenyellow
|
||||
}
|
||||
|
||||
span:before {
|
||||
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
span.implemented:before {
|
||||
font-family: "Material Icons";
|
||||
content: 'check';
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
span.not-implemented {
|
||||
color: tomato;
|
||||
}
|
||||
|
||||
span.not-implemented:before {
|
||||
font-family: "Material Icons";
|
||||
content: 'close';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload='Doctest.run()'>
|
||||
<h1>GeoJson</h1>
|
||||
<p>GeoJson is a standardized format of how to display geometry in a geographical context, using latitude/longitude
|
||||
pairs
|
||||
to display one (or multiple) Point, Line or Polygon.
|
||||
</p>
|
||||
<p> This implementation is roughly based on
|
||||
<a class="external" href="https://tools.ietf.org/html/rfc7946">RFC7946</a>.
|
||||
</p>
|
||||
<p>GeoJson objects consist of two objects:
|
||||
<ul>
|
||||
<li>type: The type of the object. All listed in the table below.</li>
|
||||
<li>coordinates: The coordinate point(s)* of which the object is composed.</li>
|
||||
</ul>
|
||||
The points are an array of exactly two values in the format [longitude, latitude]. Here the implementation
|
||||
breaks with the
|
||||
standardized format, as multiple point formats can be used for convenience (see
|
||||
<a href="#point">Point</a> section).
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Implemented</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Point</td>
|
||||
<td>
|
||||
<span class="implemented"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Line</td>
|
||||
<td>
|
||||
<span class="implemented"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Polygon</td>
|
||||
<td>
|
||||
<span class="implemented"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MultiPoint</td>
|
||||
<td>
|
||||
<span class="not-implemented"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MultiLine</td>
|
||||
<td>
|
||||
<span class="not-implemented"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MultiPolygon</td>
|
||||
<td>
|
||||
<span class="implemented"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</ul>
|
||||
<canvas id="canv_0"></canvas>
|
||||
<script>
|
||||
|
||||
// As map an image of europe is used.
|
||||
let europe = "../../../../var/examples/maps/europe/europe.jpg"
|
||||
let europeData = new MapData(MERCATOR, {
|
||||
clip: {
|
||||
min: { x: 32.863294, y: -18.58 },
|
||||
max: { x: 57.467973, y: 44.277158 }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let app = window.app = new MapApp({
|
||||
debug: true,
|
||||
view: canv_0,
|
||||
focus: { x: 46.053113042094864, y: 13.150212801583365 },
|
||||
zoom: 0.25,
|
||||
width: 880,
|
||||
height: 512,
|
||||
coordsLogging: true
|
||||
})
|
||||
|
||||
|
||||
// The sprites of image maps should be loaded by the
|
||||
// apps resources loader.
|
||||
app.loadTextures([europe], (textures) => ready(textures), { resolutionDependent: false })
|
||||
|
||||
window.readyHandler = new EventHandler("onReady")
|
||||
|
||||
function ready(textures) {
|
||||
let europeMap = new ImageMap(new PIXI.Sprite(textures.get(europe)), europeData)
|
||||
app.setMap("germany", europeMap)
|
||||
app.setup().run()
|
||||
|
||||
readyHandler.call()
|
||||
}
|
||||
</script>
|
||||
<h2 id="point">Point</h2>
|
||||
<p>Points represent a single point on the map. In the following all valid coordinate types are shown</p>
|
||||
<script class="doctest">
|
||||
let geoJsonPoint = [
|
||||
{
|
||||
//Madrid - only valid GeoJson format.
|
||||
"type": "Point",
|
||||
"coordinates": [-3.701517, 40.417485]
|
||||
}, {
|
||||
//London - most clean version.
|
||||
"type": "Point",
|
||||
"coordinates": {
|
||||
"latitude": 51.508271,
|
||||
"longitude": -0.128505
|
||||
}
|
||||
},
|
||||
{
|
||||
//Wien - short version.
|
||||
"type": "Point",
|
||||
"coordinates": {
|
||||
"lat": 48.212156,
|
||||
"lng": 16.377807
|
||||
}
|
||||
},
|
||||
{
|
||||
//Athens - PIXI point in form {x: latitude, y: longitude}.
|
||||
"type": "Point",
|
||||
"coordinates": {
|
||||
"x": 37.971782,
|
||||
"y": 23.726510
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
</script>
|
||||
<h2>Line</h2>
|
||||
<p>The line can be used to show a path or connection between multiple points.</p>
|
||||
<script class="doctest">
|
||||
const places = {
|
||||
"Madrid": [-3.701517, 40.417485],
|
||||
"London": [-0.128505, 51.508271],
|
||||
"Wien": [16.377807, 48.212156],
|
||||
"Athens": [23.726510, 37.971782],
|
||||
}
|
||||
|
||||
let geoJsonLine = {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
places.Madrid,
|
||||
places.London,
|
||||
places.Wien,
|
||||
places.Athens
|
||||
]
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<h2>Polygon</h2>
|
||||
<p>Polygons are used to represent shapes. They are an array of two pointarrays. The first represent the shape, the
|
||||
optional
|
||||
second one a hole that can be cut into the shape.</p>
|
||||
<script class="doctest">
|
||||
let geoJsonPolygon = {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[14.220297, 53.948800],
|
||||
[14.098276, 52.869102],
|
||||
[14.792306, 50.835383],
|
||||
[12.267321, 50.160485],
|
||||
[13.864702, 48.732998],
|
||||
[12.957140, 47.509061],
|
||||
[10.222090, 47.331850],
|
||||
[7.615329, 47.578565],
|
||||
[8.240349, 48.981837],
|
||||
[6.431923, 49.474461],
|
||||
[6.521907, 49.810873],
|
||||
[5.998715, 51.837875],
|
||||
[7.012748, 52.396167],
|
||||
[7.294660, 53.673047],
|
||||
[8.979846, 53.896205],
|
||||
[8.644994, 54.895603],
|
||||
[9.917260, 54.808699],
|
||||
[11.042638, 53.970400],
|
||||
[13.226022, 54.471929]
|
||||
],
|
||||
[
|
||||
[12.364498326583373, 49.348267001730406],
|
||||
[12.364498326583373, 48.653439624053],
|
||||
[6.8644970015833735, 49.734852799255926,],
|
||||
[6.929973207833399, 50.49886609496484,]
|
||||
]
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<h2>MultiPolygon</h2>
|
||||
<p>Multipolygons are an array of polygons.</p>
|
||||
<script class="doctest">
|
||||
let geoJsonMultiPolygon = {
|
||||
"type": "MultiPolygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
[2.4534564721784875, 39.602018178422234],
|
||||
[3.2148986405775872, 39.944805466067685],
|
||||
[3.589576532964429, 39.73370002861697],
|
||||
[3.1967690651395344, 39.30481714119756],
|
||||
[2.9066958581303197, 39.389861165290846],
|
||||
[2.870436707254214, 39.55963858264072]
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
[1.4261138640210334, 39.02533795687074],
|
||||
[1.6497119610906168, 39.12020148440332],
|
||||
[1.7645326055317128, 39.02293427288866],
|
||||
[1.5651072757129327, 38.877400666886004],
|
||||
[1.3898547131448993, 38.867887468083474],
|
||||
[1.3838115213322055, 38.972462606739654],
|
||||
[1.456329823084502, 38.9629621453345]
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
[3.9340384662878307, 40.04714135355371],
|
||||
[3.970297617163993, 39.93948536023678],
|
||||
[4.109291028855864, 39.93011595102749],
|
||||
[4.399364235865022, 39.817583017567095],
|
||||
[4.405407427677716, 39.92543076543482],
|
||||
[4.254327632360457, 40.06584681024191],
|
||||
[3.9642544253512995, 40.06117092743255]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<h2>Applying GeoJson Objects to the Map</h2>
|
||||
<p>The GeoJson data is used to create a
|
||||
<a href="./geographics.html" target="_blank">GeoGraphics</a> object.</p>
|
||||
<script class="doctest">
|
||||
|
||||
readyHandler.add(() => {
|
||||
|
||||
// For this demo, we bundle all GeoJson types.
|
||||
let geographics = [].concat(geoJsonMultiPolygon, geoJsonPolygon, geoJsonLine, geoJsonPoint)
|
||||
|
||||
// We need a geoLayer for the GeoGraphics to live on.
|
||||
// (Currently not working with the MapLayer itself. SO: 13-06-2018)
|
||||
let geoJsonLayer = new GeoLayer("GeoJson Layer")
|
||||
app.mapLayer.place(geoJsonLayer)
|
||||
|
||||
// Add all items individually.
|
||||
geographics.forEach(geoJson => {
|
||||
const type = geoJson.type
|
||||
const geometry = geoJson.coordinates
|
||||
|
||||
console.log(type, geometry)
|
||||
// Converts all points to PIXI format.
|
||||
let coordinates = GeoJson.validateAndConvert(type, geometry)
|
||||
console.log("Converted: ", type, geometry)
|
||||
|
||||
// Use a Utils function to get an appropriate GeoGraphics for every type.
|
||||
let graphics = GeoUtils.fromGeoJsonToGeoGraphics(type, coordinates, {
|
||||
//The GeoGraphics shoul
|
||||
onDraw: GeoJson.isLineType(type) ? function () {
|
||||
this.graphics.lineStyle(10, 0x00FFFF)
|
||||
} : function () {
|
||||
|
||||
|
||||
this.graphics.beginFill(0xFF0000, 0.8)
|
||||
if (type == "MultiPolygon") {
|
||||
this.graphics.drawCircle(0, 0, 200)
|
||||
} else if (type == "Point") {
|
||||
this.graphics.drawCircle(0, 0, 50)
|
||||
}
|
||||
this.graphics.beginFill(0x00FF00)
|
||||
}
|
||||
})
|
||||
|
||||
// Finally place the GeoGraphics element.
|
||||
geoJsonLayer.place(graphics)
|
||||
})
|
||||
|
||||
geoJsonLayer.adapt()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
329
lib/pixi/maps/geojson.js
Normal file
@ -0,0 +1,329 @@
|
||||
import { GeoGraphics, GeoShape, GeoMultiShape, GeoLine, GeoPoint } from './geographics.js'
|
||||
|
||||
/**
|
||||
* Helper class for handling GeoJson data.
|
||||
* As specified by [RFC7946](https://tools.ietf.org/html/rfc7946).
|
||||
*
|
||||
* @static
|
||||
* @export
|
||||
* @class GeoJsonGraphics
|
||||
* @extends {GeoGraphics}
|
||||
*/
|
||||
export default class GeoJson {
|
||||
static isLineType(type) {
|
||||
return type == 'LineString' || type == 'MultiLineString'
|
||||
}
|
||||
|
||||
static _getFormatStringOfType(type) {
|
||||
let description = ', where p represents a coordinate point'
|
||||
let format = ''
|
||||
switch (type) {
|
||||
case 'Point':
|
||||
format = 'p'
|
||||
break
|
||||
case 'LineString':
|
||||
format = '[p1,p2,p3,...,pn]'
|
||||
break
|
||||
case 'Polygon':
|
||||
format = '[ [p1,p2,...,pn], [h1,h2,...,hn] ]'
|
||||
description += ' and h also represents a coordinate point, but it creates a hole.'
|
||||
break
|
||||
default:
|
||||
format = type
|
||||
description = " is either not valid or not yet implemented in method '_getFormatStringOfType(type)'."
|
||||
}
|
||||
|
||||
return format + ' - ' + description
|
||||
}
|
||||
|
||||
static get types() {
|
||||
return ['Point', 'LineString', 'Polygon', 'MultiPoint', 'MultiLineString', 'MultiPolygon']
|
||||
}
|
||||
|
||||
static unwrapFeatureCollection(featureCollection) {
|
||||
if (featureCollection.features == null) {
|
||||
console.error(
|
||||
'Error at GeoJson.unrwapFeatureCollection(collection): Provided object was no valid FeatureCollection.',
|
||||
featureCollection
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let list = []
|
||||
|
||||
featureCollection.features.forEach(feature => {
|
||||
let { type, coordinates } = feature
|
||||
|
||||
if (feature.type.toLowerCase() == 'feature') {
|
||||
;({ type, coordinates } = feature.geometry)
|
||||
}
|
||||
|
||||
list.push({ type, coordinates })
|
||||
// console.log({type, coordinates})
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
static validateAndConvert(type, coordinates) {
|
||||
if (!GeoJson.validateType(type)) throw new GeoJson.InvalidTypeError(type)
|
||||
else {
|
||||
if (GeoJson.validateCoordinates(type, coordinates)) {
|
||||
let converted = GeoJson.convert(type, coordinates)
|
||||
return converted
|
||||
} else {
|
||||
console.error(
|
||||
`Coordinates are invalid. They must be in format of type '${type} - ${GeoJson._getFormatStringOfType(
|
||||
type
|
||||
)}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static validateType(type) {
|
||||
return GeoJson.types.indexOf(type) != -1
|
||||
}
|
||||
|
||||
static validateCoordinates(type, coordinates) {
|
||||
let valid = false
|
||||
|
||||
switch (type) {
|
||||
case 'Point':
|
||||
valid = !!GeoJson.validateAndConvertPoint(coordinates)
|
||||
break
|
||||
case 'LineString':
|
||||
valid = GeoJson.validateLineString(coordinates)
|
||||
break
|
||||
case 'Polygon':
|
||||
valid = GeoJson.validatePolygon(coordinates)
|
||||
break
|
||||
case 'MultiPolygon':
|
||||
valid = true
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
if (!GeoJson.validatePolygon(coordinates[i])) {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'MultiPoint':
|
||||
case 'MultiLineString':
|
||||
default:
|
||||
console.error('Type was not yet implemented: ', type)
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a point if it's an valid coordinate.
|
||||
*
|
||||
* NOTE: Here we are not following the GeoJSON standard.
|
||||
* For convenience multiple forms of representing a coordinate are
|
||||
* considered valid. A complete list is provided in the GeoUtils.
|
||||
*
|
||||
* @param {object} point - The point that is tested for validity.
|
||||
* @returns
|
||||
* @memberof GeoJson
|
||||
*/
|
||||
static validateAndConvertPoint(point) {
|
||||
return GeoUtils.validateCoordinate(point)
|
||||
}
|
||||
|
||||
/**
|
||||
*Validates if the given points represent a 'LineString'.
|
||||
*
|
||||
* @param {array} points - A list of coordinates that represent a line.
|
||||
* @returns {boolean} - Returns true, if the array is in formes as: [x1,x2,x3,...,xn]. Where x# represent a valid coordinate.
|
||||
* @memberof GeoJson
|
||||
*/
|
||||
static validateLineString(points) {
|
||||
let valid = false
|
||||
if (Array.isArray(points)) valid = points.every(GeoJson.validateAndConvertPoint)
|
||||
return valid
|
||||
}
|
||||
|
||||
static validatePolygon(points) {
|
||||
let valid = false
|
||||
if ((Array.isArray(points) && points.length >= 1) || points.length <= 2)
|
||||
valid = points.every(this.validateLineString)
|
||||
return valid
|
||||
}
|
||||
|
||||
static convert(type, coordinates) {
|
||||
let converted = null
|
||||
switch (type) {
|
||||
case 'Point':
|
||||
converted = GeoJson.validateAndConvertPoint(coordinates)
|
||||
break
|
||||
case 'LineString':
|
||||
converted = GeoJson._convertLineString(coordinates)
|
||||
break
|
||||
case 'Polygon':
|
||||
converted = GeoJson._convertPolygon(coordinates)
|
||||
break
|
||||
case 'MultiPolygon':
|
||||
converted = GeoJson._convertMultiPolygon(coordinates)
|
||||
break
|
||||
default:
|
||||
throw new GeoJson.InvalidTypeError(type)
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
static _convertLineString(coordinates) {
|
||||
return coordinates.map(point => {
|
||||
return GeoJson.validateAndConvertPoint(point)
|
||||
})
|
||||
}
|
||||
|
||||
static _convertPolygon(coordinates) {
|
||||
return coordinates.map(shape => {
|
||||
return GeoJson._convertLineString(shape)
|
||||
})
|
||||
}
|
||||
static _convertMultiPolygon(coordinates) {
|
||||
return coordinates.map(polygon => {
|
||||
return GeoJson._convertPolygon(polygon)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
GeoJson.InvalidTypeError = class extends Error {
|
||||
constructor(type) {
|
||||
super(`The requested Type was not implemented: ${type}.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoUtils contains a collection of useful functions when working with maps.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
export class GeoUtils {
|
||||
static transformToGeoGraphics(list) {
|
||||
let geographicsList = []
|
||||
list.forEach(item => {
|
||||
if (item.type == 'FeatureCollection') {
|
||||
item.features.forEach(feature => {
|
||||
let { type, coordinates } = feature
|
||||
|
||||
if (type.toLowerCase() == 'feature') ({ type, coordinates } = feature.geometry)
|
||||
|
||||
coordinates = GeoJson.validateAndConvert(type, coordinates)
|
||||
|
||||
let geographics = this.fromGeoJsonToGeoGraphics(type, coordinates)
|
||||
if (geographics) geographicsList.push(geographics)
|
||||
})
|
||||
} else {
|
||||
let geo = fromGeoJsonToGeoGraphics(item.type, item.geometry)
|
||||
if (geo) geographicsList.push(geo)
|
||||
}
|
||||
})
|
||||
return geographicsList
|
||||
}
|
||||
|
||||
static resolveFeatureCollection(collection) {
|
||||
if (!collection.features) {
|
||||
console.error(
|
||||
'Error in GeoUtils.resolveFeatureCollection(colelction): Passed parameter was no feature collection.',
|
||||
collection
|
||||
)
|
||||
return
|
||||
}
|
||||
let geojson = []
|
||||
collection.features.forEach(feature => {
|
||||
let { type, coordinates } = feature
|
||||
|
||||
if (feature.type == feature) {
|
||||
coordinates = feature.geometry.coordinates
|
||||
type = feature.geometry.type
|
||||
}
|
||||
|
||||
geojson.push({ type, coordinates })
|
||||
})
|
||||
|
||||
return geojson
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the appropriate GeoGraphics object from a GeoJson type.
|
||||
* The coordinates need to be alread in the appropriate PIXI format.
|
||||
* If not already - this can be achieved by calling 'GeoJson.validateAndConvert(type, points)'
|
||||
* beforehand.
|
||||
*
|
||||
* @static
|
||||
* @param {string} type - Any of the GeoJson types ('Point', 'LineString', 'Polygon', 'MultiPoint', 'MultiLineString', 'MultiPolygon').
|
||||
* @param {array} coordinates - Array of coordinates that fit the type. The positions within these must be in PIXI format: {x:a, y:b}.
|
||||
* @returns {GeoGraphics} - Returns a GeoGraphics object. If the conversion fails, it returns null.
|
||||
* @memberof GeoGraphics
|
||||
*/
|
||||
static fromGeoJsonToGeoGraphics(type, coordinates, opts = {}) {
|
||||
let geographics = null
|
||||
|
||||
/**
|
||||
* TODO: REMOVE
|
||||
* Just for initial debugging purposes
|
||||
*/
|
||||
Object.assign(opts, {
|
||||
debug: true
|
||||
})
|
||||
|
||||
switch (type) {
|
||||
case 'Polygon':
|
||||
geographics = new GeoShape(coordinates, opts)
|
||||
break
|
||||
|
||||
case 'MultiPolygon':
|
||||
geographics = new GeoMultiShape(coordinates, opts)
|
||||
break
|
||||
case 'LineString':
|
||||
geographics = new GeoLine(coordinates, opts)
|
||||
break
|
||||
case 'Point':
|
||||
geographics = new GeoPoint(coordinates, opts)
|
||||
break
|
||||
default:
|
||||
console.log('Could not create Geographics for type: ' + type + '. This was not implemented yet.')
|
||||
//Nothing
|
||||
}
|
||||
|
||||
return geographics
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an object, if it's an coordinate object.
|
||||
* Coordinate objects can be in the following forms:
|
||||
*
|
||||
* [lng, lat] - GeoJSON array format. !Attention lng and lat are swapped compared to the x,y format.
|
||||
* {x: lat, y: lng} - 'correct' PIXI.format
|
||||
* {lat, lng}
|
||||
* {latitude: lat, longitude: lng}
|
||||
*
|
||||
* @static
|
||||
* @param {object / array} coordinate - Coordinate to be tested, if it is an valid coordinate.
|
||||
* @returns - Returns the coordinate properly transformed. If transformation was not possible, it returns null.
|
||||
* @memberof GeoGraphics
|
||||
*/
|
||||
static validateCoordinate(coordinate) {
|
||||
if (Array.isArray(coordinate)) {
|
||||
if (coordinate.length == 2 && typeof coordinate[0] == 'number' && typeof coordinate[1] == 'number')
|
||||
return new PIXI.Point(coordinate[1], coordinate[0])
|
||||
else return false
|
||||
} else {
|
||||
const latvalues = ['x', 'lat', 'latitude']
|
||||
const lngvalues = ['y', 'lng', 'longitude']
|
||||
|
||||
let result = {}
|
||||
for (let key of Object.keys(coordinate)) {
|
||||
let target = key.toLowerCase()
|
||||
if (latvalues.indexOf(target) !== -1) result.x = coordinate[key]
|
||||
else if (lngvalues.indexOf(target) !== -1) result.y = coordinate[key]
|
||||
}
|
||||
|
||||
if (result.hasOwnProperty('x') && result.hasOwnProperty('y')) return new PIXI.Point(result.x, result.y)
|
||||
else return false
|
||||
}
|
||||
}
|
||||
}
|
302
lib/pixi/maps/geolayer.js
Normal file
@ -0,0 +1,302 @@
|
||||
import { GeoMap } from './map.js'
|
||||
import MapView from './mapview.js'
|
||||
import { EventHandler } from './utils.js'
|
||||
|
||||
import { PIXILayer } from '../../../../src/layers/js/layer.js'
|
||||
import Logging from '../../logging.js'
|
||||
//import { GeoGraphics } from "../pixi/geographics.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 extends PIXILayer {
|
||||
constructor(opts = {}) {
|
||||
super(opts)
|
||||
|
||||
/**
|
||||
* When setting the map and mapLayer with the options paramter.
|
||||
* The GeoLayer becomes a RootLayer, when the root layer should not be a MapLayer.
|
||||
*/
|
||||
if (opts.map) this._map = opts.map
|
||||
if (opts.map) this._mapLayer = opts.mapLayer
|
||||
|
||||
this.geographics = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
} else console.error('There was no map specified.', this)
|
||||
}
|
||||
|
||||
place(geographic) {
|
||||
if (geographic.constructor.name.startsWith('Geo') && geographic.graphics) {
|
||||
// Fix to remove the rollupjs circular dependency
|
||||
//if (geographic instanceof GeoGraphics) {
|
||||
this.geographics.push(geographic)
|
||||
super.place(geographic.graphics)
|
||||
} else super.place(geographic)
|
||||
}
|
||||
|
||||
placeLayer(layer) {
|
||||
if (layer instanceof GeoLayer || layer instanceof MapLayer) {
|
||||
super.placeLayer(layer)
|
||||
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)
|
||||
}
|
||||
|
||||
//GeoLayers have to be children of a map layer,
|
||||
// therefore we can recursively get the map.
|
||||
get map() {
|
||||
return this._map ? this._map : this.parent.map
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: It may be a good idea to inherit maplayer from geo layer.
|
||||
*/
|
||||
export class MapLayer extends PIXILayer {
|
||||
constructor(active, maps, scatterContainer, opts = {}) {
|
||||
super(
|
||||
Object.assign(
|
||||
{
|
||||
container: new PIXI.Container()
|
||||
},
|
||||
opts
|
||||
)
|
||||
)
|
||||
|
||||
this.opts = opts
|
||||
|
||||
this.transformHandler = new EventHandler('onTransform', {
|
||||
listeners: () => {
|
||||
this.labelVisibility()
|
||||
}
|
||||
})
|
||||
|
||||
this.scatterContainer = scatterContainer
|
||||
|
||||
if (!maps[active]) console.error('No map was set!')
|
||||
else opts.map = maps[active]
|
||||
|
||||
this.mapview = new MapView(opts)
|
||||
this.changeHandler = new EventHandler('onChange', {
|
||||
listeners: opts.onChange
|
||||
})
|
||||
|
||||
//TODO Implement error handling here.
|
||||
this.maps = maps
|
||||
this.changeMap(active)
|
||||
if (opts.map) this.placeMap(opts.map)
|
||||
this.dynamicElements = new Map()
|
||||
}
|
||||
|
||||
labelVisibility() {
|
||||
const visibility = this.childrenVisibility
|
||||
|
||||
if (visibility) {
|
||||
const zoom = this.mapview.zoom
|
||||
|
||||
const min = visibility.min || 0
|
||||
const max = visibility.max || Number.MAX_VALUE
|
||||
|
||||
if (zoom > min && zoom < max) {
|
||||
this.elements.forEach(it => (it.visible = true))
|
||||
|
||||
this.elements.forEach(it => {
|
||||
const scale = 1 / it.parent.scale.x
|
||||
|
||||
// it.children are poi groups
|
||||
// it.children[0] is the poi group of the tübingen poi
|
||||
// it.children[0].children are the text containers (not PIXI.Text), about 20 pieces
|
||||
|
||||
if (it.children.length > 0) {
|
||||
it.children[0].children.forEach(poi => {
|
||||
if (poi.children.length === 1) {
|
||||
poi.scale.set(scale, scale)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.elements.forEach(it => (it.visible = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapt() {
|
||||
this.layers.forEach(layer => {
|
||||
if (layer.adapt) layer.adapt(this.map)
|
||||
})
|
||||
}
|
||||
|
||||
placeLayer(layer) {
|
||||
super.placeLayer(layer)
|
||||
if (layer instanceof GeoLayer && this.map) {
|
||||
layer.adapt(this.map)
|
||||
}
|
||||
}
|
||||
|
||||
placeMap(map) {
|
||||
if (map instanceof GeoMap) {
|
||||
this.scatterContainer.addChild(map.image)
|
||||
this.map.onTransform.add(this.transformed.bind(this))
|
||||
this.mapview.setMap(this.map)
|
||||
|
||||
this.map.image.addChild(this.container)
|
||||
|
||||
this.adapt()
|
||||
} else {
|
||||
console.error("Could not set map, it's not of type GeoMap.", map)
|
||||
}
|
||||
}
|
||||
|
||||
placeElement(elem) {
|
||||
if (elem instanceof PIXI.DisplayObject) {
|
||||
this.map.image.addChild(elem)
|
||||
this.elements.push(elem)
|
||||
} else {
|
||||
console.error('Element need to be of type PIXI.DisplayObject.', elem)
|
||||
}
|
||||
}
|
||||
|
||||
transformed(e) {
|
||||
this.mapview.transformed(e)
|
||||
this.transformHandler.call(this)
|
||||
}
|
||||
|
||||
clone(container = null) {
|
||||
let clone = {}
|
||||
for (let name of Object.keys(this.maps)) {
|
||||
//console.info(this.maps[name])
|
||||
clone[name] = this.maps[name].clone(container)
|
||||
}
|
||||
|
||||
//console.info(this.active)
|
||||
let mapLayerClone = new MapLayer(this.active, clone, container, {
|
||||
name: MapLayer.idx++,
|
||||
viewport: this.mapview.viewport,
|
||||
focus: this.mapview.focus,
|
||||
zoom: this.mapview.zoom
|
||||
})
|
||||
//mapLayerClone._map = clone['luftbild']
|
||||
mapLayerClone.childrenVisibility = this.childrenVisibility
|
||||
return mapLayerClone
|
||||
}
|
||||
|
||||
changeMap(
|
||||
key,
|
||||
{
|
||||
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.
|
||||
} = {}
|
||||
) {
|
||||
Logging.log(`Map change: ${key}`)
|
||||
|
||||
if (this.active !== key) {
|
||||
if (this.maps.hasOwnProperty(key)) {
|
||||
let old = this.map ? this.map : null
|
||||
this._map = this.maps[key]
|
||||
this._map.name = key
|
||||
this.active = key
|
||||
|
||||
let container = useScatterAsContainer ? this.scatterContainer : this.container
|
||||
|
||||
this.map.load(container)
|
||||
|
||||
// Copies all layers.
|
||||
this.layers.forEach(layer => {
|
||||
if (old) this.map.image.addChild(layer.container)
|
||||
})
|
||||
|
||||
this.placeMap(this.map)
|
||||
|
||||
/**
|
||||
* TODO: Improve
|
||||
*
|
||||
* I'm quite sure if I made a design mistake here.
|
||||
* In an earlier version I did not need to migrate the
|
||||
* layers manually from the map to the next map.
|
||||
*
|
||||
* I believe the old version was a container next to the
|
||||
* map, which got updated on transform.
|
||||
*
|
||||
* -SO
|
||||
*/
|
||||
if (old) old.unload()
|
||||
|
||||
this.changeHandler.call(this, old)
|
||||
} else {
|
||||
let keys = Object.keys(this.maps)
|
||||
|
||||
if (keys.length == 0) console.error('There is no map set for the map layer!')
|
||||
else {
|
||||
let fallbackMap = keys[0]
|
||||
console.error(
|
||||
`A map with the key (${key}) does not exists within the mapapp. Fallback to map: ${fallbackMap}.`
|
||||
)
|
||||
this.changeMap(fallbackMap, {
|
||||
useScatterAsContainer
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get map() {
|
||||
return this._map
|
||||
}
|
||||
|
||||
/**
|
||||
* This is required for the consistency of georelated layers.
|
||||
* The request traverses up to the mapLayer where it then returns
|
||||
* the responsible map layer.
|
||||
*/
|
||||
get mapLayer() {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
MapLayer.idx = 0
|
914
lib/pixi/maps/map.js
Normal file
@ -0,0 +1,914 @@
|
||||
import { MapObjectScatter } from './scatter.js'
|
||||
import { DeepZoomImage, DeepZoomInfo } from '../deepzoom/image.js'
|
||||
import { MapData, DeepZoomMapData } from './mapdata.js'
|
||||
import { Points } from '../../utils.js'
|
||||
import { EventHandler } from './utils.js'
|
||||
import Mercator from './projections/mercator.js'
|
||||
|
||||
/**
|
||||
* The GeoMap class displays a map, that it gets from MapData object.
|
||||
* It handles the current location on the map, the zoom factor, interactions and
|
||||
* the viewport, the area of the map, the user can see and navigate to.
|
||||
*
|
||||
* You should not instantiate GeoMap on it's own. Use the implemented child classes
|
||||
* or derive a new one from it.
|
||||
*
|
||||
* Note: The name Map is already in use. Therefore GeoMap was used instead.
|
||||
*
|
||||
|
||||
* @abstract
|
||||
* @class
|
||||
* @see {@link maps.html}
|
||||
*/
|
||||
|
||||
export class GeoMap {
|
||||
/**
|
||||
* Creates instance of GeoMap
|
||||
*
|
||||
* @constructor
|
||||
* @param {MapData}[mapdata={}] - The mapdata describes how the map has to be interpreted by the Map class. E.g. what are the boundaries of the map?
|
||||
* @param {object}[opts={}] - With the opts, the created MapObjectScatter can be adjusted.
|
||||
* @param {boolean}[opts.cover=false] - Enables covering behaviour of a map object. Normally maps should cover the whole app.
|
||||
*/
|
||||
constructor(
|
||||
mapdata = {},
|
||||
{
|
||||
debug = true,
|
||||
cover = true,
|
||||
// Scatter Options
|
||||
alpha = 1,
|
||||
startScale = 1,
|
||||
minScale = 0,
|
||||
maxScale = Infinity,
|
||||
translatable = true,
|
||||
scalable = true,
|
||||
rotatable = false, // Many functionalities are not supported when rotating the map. Mainly the cover mechanism.
|
||||
viewport = mapdata.maxViewport,
|
||||
// Events
|
||||
onLoad = null,
|
||||
onTransform = null
|
||||
} = {}
|
||||
) {
|
||||
this._id = GeoMap.counter++
|
||||
this.onLoad = new EventHandler('loaded', { listeners: onLoad })
|
||||
this.onTransform = new EventHandler('transform', { listeners: onTransform })
|
||||
|
||||
this.alpha = alpha
|
||||
this.cover = cover
|
||||
this.debug = debug
|
||||
|
||||
//TODO discuss if this is required here.
|
||||
// Those are just scatter options and the information
|
||||
// is redundant in the map class and the scatter.
|
||||
this.startScale = startScale
|
||||
this.minScale = minScale
|
||||
this.maxScale = maxScale
|
||||
this.rotatable = rotatable
|
||||
this.translatable = translatable
|
||||
this.scalable = scalable
|
||||
this.viewport = viewport
|
||||
|
||||
this.mapdata = mapdata
|
||||
this.overlays = {}
|
||||
|
||||
/**
|
||||
* Adjust the viewport depending on the mapdata clipping.
|
||||
*/
|
||||
if (this.mapdata.clip) {
|
||||
const vp = this.viewport
|
||||
const cp = this.mapdata.clip
|
||||
let bounds = {
|
||||
min: {
|
||||
x: vp.min.x > cp.min.x ? vp.min.x : cp.min.x,
|
||||
y: vp.min.y > cp.min.y ? vp.min.y : cp.min.y
|
||||
},
|
||||
max: {
|
||||
x: vp.max.x < cp.max.x ? vp.max.x : cp.max.x,
|
||||
y: vp.max.y < cp.max.y ? vp.max.y : cp.max.y
|
||||
}
|
||||
}
|
||||
|
||||
this.viewport = bounds
|
||||
}
|
||||
}
|
||||
|
||||
flushHandlers() {
|
||||
// this.onLoaded
|
||||
this.onTransform.empty()
|
||||
}
|
||||
|
||||
lock({ rotatable = false, translatable = false, movableX = false, movableY = false, scalable = false } = {}) {
|
||||
if (this.image && this.image.scatter) {
|
||||
this.image.scatter.translatable = rotatable
|
||||
this.image.scatter.movableX = translatable
|
||||
this.image.scatter.movableY = movableX
|
||||
this.image.scatter.rotatable = movableY
|
||||
this.image.scatter.scalable = scalable
|
||||
}
|
||||
|
||||
// Issue #001: This causes the map to not be displayed at the correct position on
|
||||
// map change.
|
||||
// // Rotation does not yet work with the cover mechanism.
|
||||
// //this.rotatable = false
|
||||
// this.translatable = false
|
||||
// this.scalable = false
|
||||
}
|
||||
|
||||
unlock({ rotatable = true, translatable = true, movableX = true, movableY = true, scalable = true } = {}) {
|
||||
if (this.image && this.image.scatter) {
|
||||
this.image.scatter.translatable = translatable
|
||||
this.image.scatter.movableX = movableX
|
||||
this.image.scatter.movableY = movableY
|
||||
this.image.scatter.rotatable = rotatable
|
||||
this.image.scatter.scalable = scalable
|
||||
}
|
||||
// Issue #001
|
||||
// // Rotation does not yet work with the cover mechanism.
|
||||
// //this.rotatable = true
|
||||
// this.translatable = true
|
||||
// this.scalable = true
|
||||
}
|
||||
|
||||
unload() {
|
||||
this.image.parent.removeChild(this.image)
|
||||
this.image.scatter = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Is called when the scatter object is transformed.
|
||||
*
|
||||
* @private
|
||||
* @param {ScatterEvent}[e] - Contains informations on how the element was transformed.
|
||||
*/
|
||||
transformed(e) {
|
||||
this.onTransform.call(this, e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapps the display object around a scatter object.
|
||||
*
|
||||
* @private
|
||||
* @param {DisplayObject} displayObject - Defines the display object that will be wrapped inside the scatter object.
|
||||
* @param {PIXI.Container} container - Defines the frame in which the map will be displayed.
|
||||
*/
|
||||
load(image, renderer, frame = null, scatter = null) {
|
||||
if (this.debug) console.log('Load image: ', image, frame)
|
||||
|
||||
this.image = image
|
||||
if (frame) this.setFrame(frame)
|
||||
|
||||
let min = this.mapdata.toPixel(this.viewport.min)
|
||||
let max = this.mapdata.toPixel(this.viewport.max)
|
||||
let boundaries = {
|
||||
min: { x: min.x, y: max.y },
|
||||
max: { x: max.x, y: min.y }
|
||||
}
|
||||
|
||||
let scatterOpts = Object.assign({
|
||||
cover: this.cover,
|
||||
scaleable: this.scaleable,
|
||||
translatable: this.translatable,
|
||||
rotatable: this.rotatable,
|
||||
debug: this.debug,
|
||||
startScale: this.startScale,
|
||||
minScale: this.minScale,
|
||||
maxScale: this.maxScale,
|
||||
boundaries,
|
||||
onTransform: this.transformed.bind(this)
|
||||
})
|
||||
|
||||
this.scatter = new MapObjectScatter(image, renderer, scatterOpts)
|
||||
this.image.scatter = scatter == null ? this.scatter : scatter
|
||||
|
||||
this.onLoad.call(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identical copy of the current map.
|
||||
*/
|
||||
clone() {
|
||||
console.error('Method clone() is abstract. Overload it in subclass', this)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Moves the 'camera' to another location on the map.
|
||||
*
|
||||
* @abstract
|
||||
* @param {object} coordinates - Target coordinates in form {x: latitude, y: longitude}
|
||||
* @param {number} scale - Target scale.
|
||||
*/
|
||||
moveTo(coordinates, zoom = null, { animate = false } = {}) {
|
||||
console.error(this, 'The method moveTo(coordinates, zoom, opts) has not been defined in child class.')
|
||||
}
|
||||
|
||||
zoomTo(zoomFactor) {
|
||||
console.error('Method zoomTo() is abstract. Overload it in subclass', this, zoomFactor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the textures of the image, when it was unloaded and is required again.
|
||||
*
|
||||
* @memberof GeoMap
|
||||
*/
|
||||
reloadImage() {
|
||||
console.error(`Call of abstract method reloadImage(). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads the textures of the image, when not in use.
|
||||
*
|
||||
* @memberof GeoMap
|
||||
*/
|
||||
unloadImage() {
|
||||
console.error(`Call of abstract method unloadImage(). Overwrite in subclass.`, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a point at pixel position {x,y}
|
||||
* to a coordinate with latitude and longitude.
|
||||
*
|
||||
*
|
||||
* @param {object} point - Point in form of {x: x_val, y: y_val}.
|
||||
* @returns {object} - Coordinates on the map in form of {x: latitude, y: longitude}.
|
||||
*/
|
||||
coordinatesFromPoint(point) {
|
||||
let coords = this.mapdata.toCoordinates(this.toRelativePosition(point))
|
||||
return coords
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform coordinates in the map into pixel positions on the deep zoom image.
|
||||
*
|
||||
* @param {object} coords - Coordinates of a map position in form {x: latitude, y: longitude}.
|
||||
* @return - Returns a image position in form of {x: x_val, y: y_val}.
|
||||
*/
|
||||
coordinatesToPoint(coordinates) {
|
||||
return this.toAbsolutePixelCoordinates(this.mapdata.toPixel(coordinates))
|
||||
}
|
||||
|
||||
toRelativePosition(point) {
|
||||
let _point = new PIXI.Point()
|
||||
|
||||
_point.x = point.x / this.width
|
||||
_point.y = point.y / this.height
|
||||
|
||||
return _point
|
||||
}
|
||||
|
||||
toAbsolutePixelCoordinates(point) {
|
||||
let _point = new PIXI.Point()
|
||||
_point.x = point.x * this.width
|
||||
_point.y = point.y * this.height
|
||||
|
||||
return _point
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Appends the object to a PIXI container. This is important,
|
||||
// * to notify the map, that it's parent has changed.
|
||||
// *
|
||||
// * If you want to use PIXI's addChild, make sure you call
|
||||
// * appended right afterwards.
|
||||
// *
|
||||
// * @param {PIXI.Container} container
|
||||
// * @returns Returns the map object to allow chaining.
|
||||
// */
|
||||
// appendTo(container) {
|
||||
// container.addChild(this.image)
|
||||
// return this.appended(container)
|
||||
// }
|
||||
|
||||
get width() {
|
||||
return this.image.scatter.width / this.image.scatter.scale
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.image.scatter.height / this.image.scatter.scale
|
||||
}
|
||||
|
||||
get distance() {
|
||||
console.error('Overload get distance in subclass.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a screen point for a coordinate.
|
||||
*/
|
||||
positionInWindow(coordinates) {
|
||||
let pos = this.coordinatesToPoint(coordinates)
|
||||
|
||||
let framePos = {
|
||||
x: this.image.position.x + pos.x * this.image.scale.x,
|
||||
y: this.image.position.y + pos.y * this.image.scale.y
|
||||
}
|
||||
|
||||
return framePos
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coordinates for a screen point.
|
||||
*/
|
||||
coordinatesInWindow(point) {
|
||||
let mapPos = {
|
||||
x: (point.x - this.image.position.x) / this.image.scale.x,
|
||||
y: (point.y - this.image.position.y) / this.image.scale.y
|
||||
}
|
||||
|
||||
let coords = this.coordinatesFromPoint(mapPos)
|
||||
return coords
|
||||
}
|
||||
|
||||
setFrame(frame) {
|
||||
if (this.debug) console.log('Set Frame: ', frame)
|
||||
this.frame = frame
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame is the display in which the map is shown.
|
||||
* Normally it's the app, but it can be another element,
|
||||
* for example when in a submap.
|
||||
*/
|
||||
getFrame() {
|
||||
let frame = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
|
||||
if (this.frame) {
|
||||
//The app does not have an x and y attribute.
|
||||
//Therefore we need the additional check.
|
||||
frame = {
|
||||
x: this.frame.x ? this.frame.x : 0,
|
||||
y: this.frame.y ? this.frame.y : 0,
|
||||
width: this.frame.width,
|
||||
height: this.frame.height
|
||||
}
|
||||
} else if (this.image.parent) {
|
||||
let parent = this.image.parent
|
||||
|
||||
for (let key of Object.keys(frame)) {
|
||||
frame[key] = parent[key]
|
||||
}
|
||||
}
|
||||
|
||||
frame.localCenter = {
|
||||
x: frame.width / 2,
|
||||
y: frame.height / 2
|
||||
}
|
||||
|
||||
console.log(frame)
|
||||
frame.center = {
|
||||
x: frame.x + frame.localCenter.x,
|
||||
y: frame.y + frame.localCenter.y
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all maps from a json object.
|
||||
*
|
||||
* @static
|
||||
* @param {object} json - The parsed map data object.
|
||||
* @returns {object} - Returns an object with the names as keys and the GeoMaps as value.
|
||||
* @memberof GeoMap
|
||||
*/
|
||||
static allFromJson(json) {
|
||||
let error = { message: '' }
|
||||
let maps = {}
|
||||
if (GeoMap._validateJson(json, error)) {
|
||||
for (let [mapname, data] of Object.entries(json)) {
|
||||
maps[mapname] = GeoMap._createMap(data)
|
||||
}
|
||||
} else console.error('Could not validate JSON: ' + error.message)
|
||||
|
||||
return maps
|
||||
}
|
||||
|
||||
/**
|
||||
*Extracts a single map from a JSON map file.
|
||||
*
|
||||
* @static
|
||||
* @param {string} map - Name of the map.
|
||||
* @param {object} json - Parsed json object.
|
||||
* @returns {GeoMap} - Returns a geomap, if the map was in the data and valid. Otherwise false.
|
||||
* @memberof GeoMap
|
||||
*/
|
||||
static mapFromJson(map, json) {
|
||||
if (json[map]) {
|
||||
const data = json[map]
|
||||
if (this._validJsonMap(data)) return GeoMap._createMap(data)
|
||||
else console.error('Map was not in a valid format.')
|
||||
} else console.error('Map was not in data.')
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validates if the map data contains valid data
|
||||
* for creating the maps.
|
||||
*
|
||||
* @static
|
||||
* @param {object} json - The object containing multiple map data sets.
|
||||
* @param {error-object} error - An object that contains an parameter message: {message = ""}. This is faking a call by reference.
|
||||
* @returns {boolean} - True if all sets were valid. False otherwise.
|
||||
* @memberof GeoMap
|
||||
*/
|
||||
static _validateJson(json, error) {
|
||||
let isValid = true
|
||||
|
||||
if (Object.keys(json).length == 0) {
|
||||
isValid = false
|
||||
error.message += 'The provided JSON object did not contain any items.'
|
||||
}
|
||||
for (let [name, data] of Object.entries(json)) {
|
||||
if (!GeoMap._validJsonMap(data)) {
|
||||
error.message += `${name} was not valid. `
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (error.message != '') error.message = 'Maps could not be validated: ' + error.message
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
static changePathsInJson(json, path) {
|
||||
for (let key of Object.keys(json)) {
|
||||
json[key].icon = path + json[key].icon
|
||||
json[key].tiles.path = path + json[key].tiles.path
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*Validates of a single data set contains the valid data for creating a map.
|
||||
*
|
||||
* @static
|
||||
* @param {object} json - The object containing a single set of map data.
|
||||
* @returns {boolean} - True if valid, otherwise false.
|
||||
* @memberof GeoMap
|
||||
*/
|
||||
static _validJsonMap(json) {
|
||||
let isJsonValid = true
|
||||
const required = ['projection', 'type', 'icon']
|
||||
for (const requirement of Object.values(required)) {
|
||||
if (!json.hasOwnProperty(requirement)) {
|
||||
isJsonValid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (isJsonValid && json.type.toLowerCase() == 'deepzoom') {
|
||||
if (!json.hasOwnProperty('tiles')) isJsonValid = false
|
||||
}
|
||||
|
||||
return isJsonValid
|
||||
}
|
||||
|
||||
static _createMap(data) {
|
||||
switch (data.type.toLowerCase()) {
|
||||
case 'deepzoom':
|
||||
return GeoMap._createDeepZoomMap(data)
|
||||
default:
|
||||
console.error(`Datatype is invalid or not implemented yet: ${data.type}`)
|
||||
}
|
||||
}
|
||||
|
||||
static _createDeepZoomMap(data) {
|
||||
const projection = GeoMap._getProjectionByName(data.projection)
|
||||
const tilesConfig = data.tiles
|
||||
const options = data.options
|
||||
|
||||
const mapdata = new DeepZoomMapData(projection, tilesConfig)
|
||||
return new DeepZoomMap(mapdata, tilesConfig, options)
|
||||
}
|
||||
|
||||
static _getProjectionByName(projection) {
|
||||
switch (projection.toLowerCase()) {
|
||||
case 'mercator':
|
||||
return new Mercator()
|
||||
default:
|
||||
console.error(`Projection is invalid or not implemented yet ${projection}.`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GeoMap.counter = 0
|
||||
|
||||
/**
|
||||
* The DeepZoomMap class extends the GeoMap to create
|
||||
* maps as deepzoom images from maptiles.
|
||||
*
|
||||
* @extends GeoMap
|
||||
* @class
|
||||
* @see {@link maps.html}
|
||||
*/
|
||||
|
||||
export class DeepZoomMap extends GeoMap {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} tilesConfig - The tiles config object, that defines at what path and in which format the tiles are.
|
||||
* @param {MapData} mapdata - A MapData object, that contains informations of how the given map has to be interpreted.
|
||||
* @param {object} opts - Additional options to specify the behaviour of the deep zoom image.
|
||||
*/
|
||||
constructor(mapdata, tilesConfig, opts = {}) {
|
||||
if (!tilesConfig.app) console.error('App was not set in the tilesConfig.')
|
||||
opts = Object.assign(
|
||||
{
|
||||
maxScale: Math.min(tilesConfig.width, tilesConfig.height) / tilesConfig.tileSize,
|
||||
minScale: mapdata.getMinScale,
|
||||
highResolution: true,
|
||||
debug: false
|
||||
},
|
||||
opts
|
||||
)
|
||||
super(mapdata, opts)
|
||||
|
||||
this.tilesConfig = tilesConfig
|
||||
this._verifyMapdata()
|
||||
}
|
||||
|
||||
_verifyMapdata() {
|
||||
if (!(this.mapdata instanceof MapData)) {
|
||||
console.error('Use the MapData object for creating maps!')
|
||||
} else {
|
||||
if (this.mapdata instanceof DeepZoomMapData) {
|
||||
if (!this.mapdata.app) console.error('No app was set on the mapdata!')
|
||||
} else {
|
||||
console.error('Use the DeepZoomMapData object.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the DeepZoomImage.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
load(container = null, scatter = null) {
|
||||
this.info = new DeepZoomInfo(this.tilesConfig)
|
||||
console.log('APP:', this.mapdata.app)
|
||||
let image = new DeepZoomImage(this.info, {
|
||||
app: this.mapdata.app,
|
||||
alpha: this.alpha,
|
||||
debug: this.debug,
|
||||
world: scatter == null ? scatter : scatter.getWorldScatter()
|
||||
})
|
||||
|
||||
super.load(image, container, scatter)
|
||||
|
||||
if (this.debug) console.log('Loaded image: ', image, 'With options: ', this.info)
|
||||
}
|
||||
|
||||
unloadImage() {
|
||||
if (this.image) {
|
||||
this.image.deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
reloadImage() {
|
||||
this.image.activate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Event function that is invoked by the deep zoom image, when it has been transformed.
|
||||
*
|
||||
* @private
|
||||
* @param {ScatterEvent} e
|
||||
*/
|
||||
transformed(e) {
|
||||
this.image.transformed(e)
|
||||
super.transformed(e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Jumps to a certain point on the map and adjusts the size of the
|
||||
* map depending on the distance (zoom).
|
||||
*
|
||||
*
|
||||
* @param {*} coordinates
|
||||
* @param {*} [zoom=null]
|
||||
* @param {*} [{ animate = 0 }={}]
|
||||
* @memberof DeepZoomMap
|
||||
*/
|
||||
moveTo(coordinates, zoom = null, { animate = 0 } = {}) {
|
||||
if (this.image.scatter == null) {
|
||||
return
|
||||
}
|
||||
//TODO: This is jaggy on submaps. Make it more smooth. Severin Opel 08-05-2018
|
||||
if (zoom) {
|
||||
/**
|
||||
* Zoom's the view to a specified distance in DZI-Levels.
|
||||
* The input level is clipped to allowed levels.
|
||||
* */
|
||||
let targetScale = this.scaleForFloatingLevel(zoom)
|
||||
|
||||
// Don't scale beyond boundaries.
|
||||
if (this.maxScale) targetScale = Math.min(this.maxScale, targetScale)
|
||||
|
||||
if (this.minScale) targetScale = Math.max(targetScale, this.minScale)
|
||||
|
||||
this.image.scatter.requestScale(targetScale)
|
||||
}
|
||||
|
||||
if (coordinates) {
|
||||
//Get pixel position of lat lang values
|
||||
let point = this.coordinatesToPoint(coordinates)
|
||||
|
||||
let containerCenter
|
||||
if (this.frame) {
|
||||
containerCenter = this.getFrame().localCenter
|
||||
//console.log("HAS FRAME: ", containerCenter)
|
||||
} else {
|
||||
containerCenter = {
|
||||
x: this.image.parent.width / 2,
|
||||
y: this.image.parent.height / 2
|
||||
}
|
||||
|
||||
//console.log("GET CONTAINER CENTER")
|
||||
}
|
||||
|
||||
//Calculate the delta depending on image position and center the
|
||||
// desired position in the frame.
|
||||
let delta = {
|
||||
x: containerCenter.x - point.x * this.image.scatter.scale - this.scatter.position.x,
|
||||
y: containerCenter.y - point.y * this.image.scatter.scale - this.scatter.position.y
|
||||
}
|
||||
// console.log("MOVE TO: " , this.image.scatter.translatable,point, delta)
|
||||
|
||||
if (animate > 0) {
|
||||
let last = 0
|
||||
TweenMax.to({ x: 0, y: 0 }, animate, {
|
||||
ease: Power0.easeNone,
|
||||
x: delta.x,
|
||||
y: delta.y,
|
||||
onUpdateParams: ['{self}'],
|
||||
onUpdate: tween => {
|
||||
let step = tween.ratio - last
|
||||
last = tween.ratio
|
||||
this.image.scatter.transform(Points.multiplyScalar(delta, step), /*ratio*/ 1, 0, { x: 0, y: 0 })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.image.scatter.transform(delta, /*ratio*/ 1, 0, { x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
// return this.image.scatter.position
|
||||
}
|
||||
|
||||
scaleForFloatingLevel(level) {
|
||||
level = Math.max(0, level - this.image.info.baseLevel)
|
||||
return Math.pow(2, level) / this.image.resolution
|
||||
}
|
||||
|
||||
floatingLevelForScale(scale) {
|
||||
let level = Math.log2(scale * this.image.resolution)
|
||||
let newLevel = this.image.info.baseLevel + Math.max(level, 0)
|
||||
return Math.min(newLevel, this.image.info.maxLoadableLevel)
|
||||
}
|
||||
|
||||
get distance() {
|
||||
return this.floatingLevelForScale(this.scatter.scale)
|
||||
}
|
||||
|
||||
clone(container, scatter = null) {
|
||||
const map = new DeepZoomMap(this.mapdata, this.tilesConfig, {
|
||||
alpha: this.alpha,
|
||||
cover: this.cover,
|
||||
debug: this.debug,
|
||||
startScale: this.startScale,
|
||||
minScale: this.minScale,
|
||||
maxScale: this.maxScale,
|
||||
rotatable: this.rotatable,
|
||||
translatable: this.translatable,
|
||||
scalable: this.scalable,
|
||||
viewport: this.viewport
|
||||
})
|
||||
|
||||
//TODO: If frame is not set, the map does not work anymore.
|
||||
|
||||
//map.load(container, scatter)
|
||||
|
||||
// this.image.parent.children.forEach(child=>{
|
||||
// if(child instanceof PIXI.DisplayObject){
|
||||
|
||||
// if(child instanceof PIXI.Graphics){
|
||||
// // let clone = child.clone()
|
||||
// // const position = child.position
|
||||
// // clone.position.set(position.x, position.y)
|
||||
// // map.addChild(clone)
|
||||
// // console.log(map.mapLayer)
|
||||
|
||||
// }else console.log("DisplayObject is not yet implemented for cloning.", child)
|
||||
|
||||
// }else console.error("Not implemented object to clone: ", child)
|
||||
// })
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
tint() {
|
||||
let color = DeepZoomMap.tintcolors[DeepZoomMap.tintcolor++ % DeepZoomMap.tintcolors.length]
|
||||
this.image.children[0].children.forEach(tiles => {
|
||||
tiles.children.forEach(tile => {
|
||||
tile.tint = color
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// /** Returns the tile layer level that corresponds to the given scale.
|
||||
// * @param {number} scale - the scale factor
|
||||
// **/
|
||||
// levelForScale(scale) {
|
||||
// let level = Math.round(Math.log2(scale * this.resolution)) // Math.floor(Math.log2(event.scale))+1
|
||||
// let newLevel = this.info.baseLevel + Math.max(level, 0)
|
||||
// return Math.min(newLevel, this.info.maxLoadableLevel)
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Returns the scale factor that correspond to a given level.
|
||||
// * @param {number} level - the requested level
|
||||
// * @returns {number} - Returns the scale level for the given level.
|
||||
// */
|
||||
// scaleForLevel(level){
|
||||
// level = Math.max(0, level - this.info.baseLevel)
|
||||
// return Math.pow(2, level) / this.resolution
|
||||
// }
|
||||
}
|
||||
|
||||
DeepZoomMap.tintcolors = [0xff0000, 0xff00ff, 0xffff00, 0x00ff00, 0x00ffff, 0x0000ff]
|
||||
DeepZoomMap.tintcolor = 0
|
||||
|
||||
/**
|
||||
*
|
||||
* ImageMap extends GeoMap to display simple images
|
||||
* as maps.
|
||||
*/
|
||||
|
||||
export class ImageMap extends GeoMap {
|
||||
constructor(sprite, mapdata, opts = {}) {
|
||||
super(mapdata, opts)
|
||||
|
||||
this.sprite = sprite
|
||||
|
||||
this.baseZoomHeight = opts.baseZoomHeight ? opts.baseZoomHeight : this.sprite.texture.height
|
||||
}
|
||||
|
||||
load(container = null, scatter = null) {
|
||||
super.load(this.sprite, container, scatter)
|
||||
this.image.alpha = this.alpha
|
||||
this.image.interactive = true
|
||||
}
|
||||
|
||||
clone(container = null, scatter = null) {
|
||||
const map = new ImageMap(new PIXI.Sprite(this.sprite.texture), this.mapdata, {
|
||||
alpha: this.alpha,
|
||||
cover: this.cover,
|
||||
debug: this.debug,
|
||||
startScale: this.startScale,
|
||||
minScale: this.minScale,
|
||||
maxScale: this.maxScale,
|
||||
rotatable: this.rotatable,
|
||||
translatable: this.translatable,
|
||||
scalable: this.scalable,
|
||||
viewport: this.viewport
|
||||
})
|
||||
map.load(container, scatter)
|
||||
|
||||
console.log(map.image)
|
||||
return map
|
||||
}
|
||||
|
||||
// transformed(e) {
|
||||
// for (let key in this.overlays) {
|
||||
// this.overlays[key].forEach((overlay) => {
|
||||
// overlay.sprite.scale.x = overlay.scale / e.scale
|
||||
// overlay.sprite.scale.y = overlay.scale / e.scale
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} coordinates
|
||||
* @param {*} [zoom=null]
|
||||
* @param {*} [{ animate = 0 }={}]
|
||||
* @memberof ImageMap
|
||||
*/
|
||||
moveTo(coordinates, zoom = null, { animate = 0 } = {}) {
|
||||
// console.log('Coordinates: ', coordinates)
|
||||
|
||||
// if (zoom) {
|
||||
// this.image.scatter.requestScale(zoom)
|
||||
// }
|
||||
|
||||
// if (coordinates) {
|
||||
// //Get pixel position of lat lang values
|
||||
// let offset = Points.multiplyScalar(this.coordinatesToPoint(coordinates), this.scatter.scale)
|
||||
|
||||
// let frame = this.getFrame()
|
||||
// console.log(frame)
|
||||
|
||||
// //Calculate the delta depending on image position and center the
|
||||
// // desired position in the frame.
|
||||
// let delta = {
|
||||
// x: frame.center.x - offset.x - this.scatter.position.x,
|
||||
// y: frame.center.y - offset.y - this.scatter.position.y
|
||||
// }
|
||||
|
||||
// console.log('Move to: ', offset, frame, delta)
|
||||
|
||||
// if (this.image) {
|
||||
// if (animate > 0) {
|
||||
// console.log(animate)
|
||||
// //TODO Replace this with a more robus manipulation method (RequestMovement + animation)
|
||||
// this.image.scatter.move(delta, { animate })
|
||||
// //Requesting the transform dhoul be okay. Maybe also replace with an request mehtod.
|
||||
// } else this.image.scatter.transform(delta, /*ratio*/ 1, 0, { x: 0, y: 0 })
|
||||
// } else console.error('Imge was not loaded yet.')
|
||||
// }
|
||||
|
||||
if (this.image.scatter == null) {
|
||||
return
|
||||
}
|
||||
//TODO: This is jaggy on submaps. Make it more smooth. Severin Opel 08-05-2018
|
||||
if (zoom) {
|
||||
/**
|
||||
* Zoom's the view to a specified distance in DZI-Levels.
|
||||
* The input level is clipped to allowed levels.
|
||||
* */
|
||||
|
||||
let targetScale = zoom * this.zoomFactor
|
||||
|
||||
// Don't scale beyond boundaries.
|
||||
if (this.maxScale) targetScale = Math.min(this.maxScale, targetScale)
|
||||
|
||||
if (this.minScale) targetScale = Math.max(targetScale, this.minScale)
|
||||
|
||||
this.image.scatter.requestScale(targetScale)
|
||||
}
|
||||
|
||||
//coordinates = null
|
||||
if (coordinates) {
|
||||
//Get pixel position of lat lang values
|
||||
let point = this.coordinatesToPoint(coordinates)
|
||||
|
||||
let containerCenter
|
||||
if (this.frame) {
|
||||
containerCenter = this.getFrame().localCenter
|
||||
//console.log("HAS FRAME: ", containerCenter)
|
||||
} else {
|
||||
containerCenter = {
|
||||
x: this.image.parent.width / 2,
|
||||
y: this.image.parent.height / 2
|
||||
}
|
||||
|
||||
//console.log("GET CONTAINER CENTER")
|
||||
}
|
||||
|
||||
//Calculate the delta depending on image position and center the
|
||||
// desired position in the frame.
|
||||
|
||||
let delta = {
|
||||
x: containerCenter.x - point.x * this.image.scatter.scale - this.scatter.position.x,
|
||||
y: containerCenter.y - point.y * this.image.scatter.scale - this.scatter.position.y
|
||||
}
|
||||
// console.log("MOVE TO: " , this.image.scatter.translatable,point, delta)
|
||||
|
||||
if (animate > 0) {
|
||||
let last = 0
|
||||
TweenMax.to({ x: 0, y: 0 }, animate, {
|
||||
ease: Power0.easeNone,
|
||||
x: delta.x,
|
||||
y: delta.y,
|
||||
onUpdateParams: ['{self}'],
|
||||
onUpdate: tween => {
|
||||
let step = tween.ratio - last
|
||||
last = tween.ratio
|
||||
this.image.scatter.transform(Points.multiplyScalar(delta, step), /*ratio*/ 1, 0, { x: 0, y: 0 })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.image.scatter.transform(delta, /*ratio*/ 1, 0, { x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get zoom() {
|
||||
return this.scatter.scale / this.zoomFactor
|
||||
}
|
||||
|
||||
get zoomFactor() {
|
||||
let factor = this.baseZoomHeight / this.sprite.texture.height
|
||||
console.log(factor)
|
||||
return factor
|
||||
}
|
||||
}
|
105
lib/pixi/maps/mapapp.html
Normal file
@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>MapApp</title>
|
||||
|
||||
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../../../css/doctest.css'>
|
||||
|
||||
<script src="../../../dist/iwmlib.3rdparty.js"></script>
|
||||
<script src="../../../dist/iwmlib.js"></script>
|
||||
<script src="../../../dist/iwmlib.pixi.js"></script>
|
||||
|
||||
|
||||
<style>
|
||||
.controls {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="Doctest.run()">
|
||||
<h1>MapApp</h1>
|
||||
<p>
|
||||
This class extends the PIXIApp to simplify the process of rendering
|
||||
Maps in the canvas. For that reason, it contains useful functions
|
||||
for an easier handling of maps in the canvas.
|
||||
</p>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="cityControl" class="controls"></div>
|
||||
<p><strong>WHAT TO SEE:</strong> The map should focus Paris.</p>
|
||||
<script class="doctest">
|
||||
let capitals = {
|
||||
london: { x: 51.5, y: -0.083333 },
|
||||
rome: { x: 41.9, y: 12.483333 },
|
||||
madrid: { x: 40.4, y: -3.683333 },
|
||||
paris: { x: 48.833986, y: 2.346989 }
|
||||
}
|
||||
|
||||
// You may define a focus point ...
|
||||
let focus = capitals.paris
|
||||
|
||||
// ... and a zoom level.
|
||||
let zoom = 1
|
||||
|
||||
// Name has to be app (like all other PIXIApps).
|
||||
const app = (window.app = new MapApp({
|
||||
focus,
|
||||
zoom,
|
||||
view: canvas,
|
||||
coordsLogging: true,
|
||||
width: 512,
|
||||
height: 512
|
||||
}))
|
||||
|
||||
// As map an image of europe is used.
|
||||
let europe = '../assets/maps/pixabay/europe.jpg'
|
||||
|
||||
//Preload all required sprites for the image map.
|
||||
app.loadSprites([europe], sprites => ready(sprites), {
|
||||
resolutionDependent: false
|
||||
})
|
||||
|
||||
// The mapdata object contains informations,
|
||||
// how the displayed map has to be interpreted.
|
||||
// e.g. which projection is used or how the
|
||||
// image is clipped.
|
||||
let europeData = new MapData(new Projection.Mercator(), {
|
||||
clip: {
|
||||
min: { x: 32.863294, y: -18.58 },
|
||||
max: { x: 57.467973, y: 44.277158 }
|
||||
}
|
||||
})
|
||||
|
||||
function ready(sprites) {
|
||||
// When resources are loaded, the ImageMap can be instantiated.
|
||||
let imageMap = new ImageMap(sprites.get(europe), europeData, {
|
||||
coordsLogging: true,
|
||||
maxScale: 1,
|
||||
cover: false
|
||||
})
|
||||
|
||||
// Finally apply the map to the MapApp
|
||||
app.setMap('europe', imageMap)
|
||||
console.error("IMAGE MAP SET!")
|
||||
|
||||
|
||||
// The app requires a map before beeing able to run.
|
||||
// So start the app here.
|
||||
app.setup().run()
|
||||
}
|
||||
|
||||
for (let [key, val] of Object.entries(capitals)) {
|
||||
let cityBtn = document.createElement("button")
|
||||
cityBtn.innerText = key
|
||||
cityBtn.addEventListener("click", () => {
|
||||
app.mapLayer.map.moveTo(val)
|
||||
})
|
||||
cityControl.appendChild(cityBtn)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
548
lib/pixi/maps/mapapp.js
Normal file
@ -0,0 +1,548 @@
|
||||
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'
|
||||
|
||||
/**
|
||||
* 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,
|
||||
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.startmap = opts.startmap
|
||||
this.overlays = opts.overlays
|
||||
this.maps = opts.maps
|
||||
this.focus = opts.focus
|
||||
this.zoom = opts.zoom
|
||||
|
||||
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._setupKeyboardUtils()
|
||||
|
||||
Logging.log('Application start')
|
||||
}
|
||||
|
||||
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.domLayer = new DOMLayer({
|
||||
name: 'DOM Root',
|
||||
container: document.body
|
||||
})
|
||||
|
||||
if (!this.startmap) {
|
||||
let firstMap = Object.keys(this.maps)[0]
|
||||
if (firstMap != null) this.startmap = firstMap
|
||||
else {
|
||||
console.error('No map was set. Set a map first, before running the setup command!')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//console.log('startup', this.startmap, this.maps)
|
||||
|
||||
this.mapLayer = new MapLayer(this.startmap, this.maps, this.scene, {
|
||||
name: 'Map Layer',
|
||||
focus: this.focus,
|
||||
zoom: this.zoom
|
||||
})
|
||||
|
||||
this.mapLayer.changeHandler.add(this._mapChanged.bind(this))
|
||||
this.pixiLayer.place(this.mapLayer)
|
||||
this._mapChanged(null)
|
||||
|
||||
this.pixiUiLayer = new PIXILayer({ name: 'Pixi UI' })
|
||||
this.pixiLayer.placeLayer(this.pixiUiLayer)
|
||||
|
||||
this.domUiLayer = new DOMLayer({ name: 'DOM UI' })
|
||||
this.domLayer.placeLayer(this.domUiLayer)
|
||||
|
||||
if (this.fpsLogging && fpsDisplay) this.pixiUiLayer.place(fpsDisplay)
|
||||
|
||||
if (this.coordsLogging) {
|
||||
this.coordsDisplay = new CoordinateDisplay(this)
|
||||
this.pixiUiLayer.place(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
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
addMaps(maps) {
|
||||
for (let key in maps) {
|
||||
this.addMap(key, maps[key])
|
||||
}
|
||||
}
|
||||
|
||||
selectMap(key) {
|
||||
if (this.maps[key]) {
|
||||
if (this.mapLayer) this.mapLayer.changeMap(key)
|
||||
} else {
|
||||
console.error(`Selected map ("${key}") was not (yet) added to the mapapp.`)
|
||||
}
|
||||
}
|
||||
|
||||
setMap(key, map) {
|
||||
this.addMap(key, map)
|
||||
this.selectMap(key)
|
||||
}
|
||||
|
||||
addMap(key, map) {
|
||||
this.maps[key] = map
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
_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.mapLayer.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)
|
||||
}
|
||||
}
|
201
lib/pixi/maps/mapdata.html
Normal file
@ -0,0 +1,201 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>MapData</title>
|
||||
|
||||
|
||||
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../../../css/doctest.css'>
|
||||
|
||||
<script src="../../../dist/iwmlib.3rdparty.js"></script>
|
||||
<script src="../../../dist/iwmlib.js"></script>
|
||||
<script src="../../../dist/iwmlib.pixi.js"></script>
|
||||
|
||||
<style>
|
||||
.inline-showcase {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.map-example {
|
||||
display: inline-block;
|
||||
width: 256px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.map-marker {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -75%) scale(0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload='Doctest.run()'>
|
||||
<h1>MapData</h1>
|
||||
<p>Mapdata calculates is responsible for transforming map coordinates to pixel coordinates and backwards.</p>
|
||||
|
||||
<h2>Static Squared World Map</h2>
|
||||
<p>The most simple example is a squared world map, thats projected with mercator transformation. Ranging from
|
||||
Longitude
|
||||
-180 to 180 and Latitude from -90 to 90 (Exclusive, because mercator gets infinite at +/-90). </p>
|
||||
<p>Coordinates:
|
||||
<strong id="map_coords_0"></strong>
|
||||
</p>
|
||||
<div id="mapContainerGeneral" class="map-container">
|
||||
<img src="../assets/maps/osm/0/0/0.png" id="map_image_0" alt="Image is missing." width="512">
|
||||
</div>
|
||||
<script>
|
||||
|
||||
let capitals = {
|
||||
abidjan: { x: 5.349470, y: -4.006472 },
|
||||
berlin: { x: 52.525430, y: 13.385291 },
|
||||
canberra: { x: -35.282025, y: 149.128648 },
|
||||
capetown: { x: -33.925448, y: 18.416962 },
|
||||
moscow: { x: 55.750892, y: 37.622799 },
|
||||
washington: { x: 38.895650, y: -77.031407 },
|
||||
rio: { x: -22.871400, y: -43.280490 },
|
||||
tokio: { x: 35.696278, y: 139.731366 }
|
||||
}
|
||||
|
||||
const europeanCapitalCities = {
|
||||
berlin: { x: 52.505949, y: 13.379400 },
|
||||
london: { x: 51.5, y: -0.083333 },
|
||||
rome: { x: 41.9, y: 12.483333 },
|
||||
madrid: { x: 40.4, y: -3.683333 },
|
||||
paris: { x: 48.833986, y: 2.346989 }
|
||||
}
|
||||
|
||||
|
||||
//Helper function to print the coordinates to a dom element.
|
||||
function printCoordinates(element, mapdata, out_dom) {
|
||||
element.addEventListener("mousemove", (event) => {
|
||||
let coords = mapdata.toCoordinates({ x: event.offsetX / event.target.width, y: event.offsetY / event.target.height })
|
||||
out_dom.innerHTML = "Lat: " + coords.x.toFixed(4) + " Lng: " + coords.y.toFixed(4)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function placeMarker(parent, position) {
|
||||
const markerIconPath = "../../../assets/icons/room.png"
|
||||
|
||||
let marker = document.createElement("img")
|
||||
marker.src = markerIconPath
|
||||
marker.className = "map-marker"
|
||||
|
||||
Object.assign(marker.style, {
|
||||
left: (position.x * 100) + "%",
|
||||
top: (position.y * 100) + "%"
|
||||
})
|
||||
parent.appendChild(marker)
|
||||
console.log(marker)
|
||||
}
|
||||
|
||||
</script>
|
||||
<script class="doctest">
|
||||
|
||||
// Instantiate our mapdata.
|
||||
let squared_world = new MapData(new Projection.Mercator());
|
||||
|
||||
// Define a position, we are interested in in {x: latitude, y: longitude}
|
||||
let iceland = { x: 64.514979, y: -19.020796 }
|
||||
|
||||
// Gets the normalized positition of the coordinates
|
||||
let pos_0 = squared_world.toPixel(iceland)
|
||||
|
||||
// Helper function to set the dom marker.
|
||||
placeMarker(mapContainerGeneral, pos_0)
|
||||
|
||||
</script>
|
||||
<script>
|
||||
printCoordinates(map_image_0, squared_world, map_coords_0)
|
||||
</script>
|
||||
<h2 id="germany">Clipped Map</h2>
|
||||
<p>Often we don't use the whole map, or our map is a subsection of the world. MapData can clip those cases, using
|
||||
the a
|
||||
bounding box of min and max coordinates.</p>
|
||||
<p>Coordinates:
|
||||
<strong id="map_coords_1"></strong>
|
||||
</p>
|
||||
<div class="map-container" id="mapContainerClipped">
|
||||
<img src="../assets/maps/pixabay/europe.jpg" id="map_image_1" alt="Image is missing." width="512">
|
||||
</div>
|
||||
<script class="doctest">
|
||||
|
||||
//Same procedure as above, just add a clipping parameter to the MapData object.
|
||||
let europe = new MapData(new Projection.Mercator(), {
|
||||
clip: {
|
||||
min: { x: 32.863294, y: -18.58 },
|
||||
max: { x: 57.467973, y: 44.277158 }
|
||||
}
|
||||
})
|
||||
|
||||
//Same as above
|
||||
for (let coordinates of Object.values(europeanCapitalCities)) {
|
||||
let pixelPosition = europe.toPixel(coordinates)
|
||||
placeMarker(mapContainerClipped, pixelPosition)
|
||||
}
|
||||
|
||||
</script>
|
||||
<script>
|
||||
printCoordinates(map_image_1, germany, map_coords_1)
|
||||
</script>
|
||||
<h2>Clipped And Translated Map</h2>
|
||||
<p>Maps can be also translated, if the whole world is shown and clipping is not an option.</p>
|
||||
<p>Coordinates:
|
||||
<strong id="map_coords_2"></strong>
|
||||
</p>
|
||||
<div id="map_image_2" class="map-container"">
|
||||
</div>
|
||||
<script class=" doctest">
|
||||
|
||||
|
||||
|
||||
const size = 256
|
||||
|
||||
|
||||
// Fake offset by using the old mapdata.
|
||||
let translation = squared_world.toPixel({ x: 0, y: -140 })
|
||||
console.log(translation)
|
||||
|
||||
|
||||
//This map is clipped at the bottom
|
||||
// And also translated in hoizontal direction.
|
||||
// The translate option corrects that.
|
||||
let translated_world = new MapData(new Projection.Mercator(), {
|
||||
translate: { x: 0, y: 40 },
|
||||
|
||||
})
|
||||
|
||||
|
||||
console.log(translation)
|
||||
|
||||
Object.assign(map_image_2.style, {
|
||||
width: size + "px",
|
||||
height: size + "px",
|
||||
backgroundImage: "url(../assets/maps/osm/0/0/0.png)",
|
||||
backgroundColor: "red",
|
||||
backgroundPosition: translation.x * size + "px 0"
|
||||
})
|
||||
|
||||
//Same as above
|
||||
for (let coordinates of Object.values(capitals)) {
|
||||
let position = translated_world.toPixel(coordinates)
|
||||
placeMarker(map_image_2, position)
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
map_image_2.addEventListener("mousemove", (event) => {
|
||||
let coords = translated_world.toCoordinates({ x: event.offsetX / event.target.width, y: event.offsetY / event.target.height })
|
||||
map_coords_2.innerHTML = "Lat: " + coords.x.toFixed(4) + " Lng: " + coords.y.toFixed(4)
|
||||
})
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
155
lib/pixi/maps/mapdata.js
Normal file
@ -0,0 +1,155 @@
|
||||
/**
|
||||
* MapData contains the informations about how
|
||||
* a Map has to be interpreted. What are the bounds of the
|
||||
* map and how to translate coordinates into
|
||||
* image positions.
|
||||
*
|
||||
* @class
|
||||
* @see {@link mapdata.html}
|
||||
*/
|
||||
export class MapData {
|
||||
/**
|
||||
* Creates instance of MapData
|
||||
*
|
||||
* @constructor
|
||||
* @param {Projection}[projection] - Specifies the projection of the map (e.g. Mercator Projection).
|
||||
* @param {object}[opts] - Addiditonal options.
|
||||
* @param {[[minLat, minLng],[maxLat, maxLng]]}[opts.bounds] - Describes the minimum and maximum coordinates on the map
|
||||
* @param {Point}[opts.translate] - Defines a translation, when clipping is not an option (e.g. when the whole world is shown, but translated.)
|
||||
*/
|
||||
constructor(projection, opts = {}) {
|
||||
this.opts = Object.assign(
|
||||
{
|
||||
translate: { x: 0, y: 0 }
|
||||
},
|
||||
opts
|
||||
)
|
||||
|
||||
this.projection = projection
|
||||
|
||||
if (this.clip) {
|
||||
let _cmin = this.projection.forward(this.opts.clip.min)
|
||||
let _cmax = this.projection.forward(this.opts.clip.max)
|
||||
|
||||
// Swaps the y values, Mercator has it's origin bottom right,
|
||||
// browser coordinates start top right.
|
||||
let cmin = { x: _cmin.x, y: _cmax.y }
|
||||
let cmax = { x: _cmax.x, y: _cmin.y }
|
||||
|
||||
this.clipExt = {
|
||||
coords: {
|
||||
min: this.opts.clip.min,
|
||||
max: this.opts.clip.max
|
||||
},
|
||||
point: {
|
||||
min: cmin,
|
||||
max: cmax
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toCoordinates(point) {
|
||||
if (this.clip) {
|
||||
let min = this.clipExt.point.min
|
||||
let max = this.clipExt.point.max
|
||||
|
||||
let width = max.x - min.x
|
||||
let height = max.y - min.y
|
||||
|
||||
point.x *= width
|
||||
point.y *= height
|
||||
|
||||
point.x += min.x
|
||||
point.y += min.y
|
||||
}
|
||||
|
||||
let coordinates = this.projection.backward(point)
|
||||
|
||||
if (this.opts.translate) {
|
||||
coordinates.x -= this.opts.translate.x
|
||||
coordinates.y -= this.opts.translate.y
|
||||
}
|
||||
|
||||
return coordinates
|
||||
}
|
||||
|
||||
toPixel(coordinates) {
|
||||
let coords = { x: coordinates.x, y: coordinates.y }
|
||||
if (this.opts.translate) {
|
||||
coords.x += this.opts.translate.x
|
||||
coords.y += this.opts.translate.y
|
||||
}
|
||||
|
||||
let point = this.projection.forward(coords)
|
||||
|
||||
if (this.opts.clip) {
|
||||
let min = this.clipExt.point.min
|
||||
let max = this.clipExt.point.max
|
||||
|
||||
let clippedPoint = {
|
||||
x: point.x - min.x,
|
||||
y: point.y - min.y
|
||||
}
|
||||
|
||||
let width = max.x - min.x
|
||||
let height = max.y - min.y
|
||||
|
||||
point.x = clippedPoint.x / width
|
||||
point.y = clippedPoint.y / height
|
||||
}
|
||||
|
||||
return point
|
||||
}
|
||||
|
||||
get clip() {
|
||||
return this.opts.clip
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds to pixel transforms some bounds in form of {min:{x:minLat, y:minLng},max:{x:maxLat, y:maxLng}}
|
||||
* to pixel coordinates.
|
||||
*
|
||||
* @param {*} bounds
|
||||
*/
|
||||
boundsToPixel(bounds) {
|
||||
let min = this.toPixel(bounds.min)
|
||||
let max = this.toPixel(bounds.max)
|
||||
|
||||
// Y values needs to be swapped, as PIXI has it's origin
|
||||
// in the top-left corner and a regular map in the bottom-left corner.
|
||||
let boundaries = {
|
||||
min: { x: min.x, y: max.y },
|
||||
max: { x: max.x, y: min.y }
|
||||
}
|
||||
|
||||
return boundaries
|
||||
}
|
||||
|
||||
get maxViewport() {
|
||||
return this.opts.clip ? this.opts.clip : this.projection.maxViewport
|
||||
}
|
||||
}
|
||||
|
||||
export class DeepZoomMapData extends MapData {
|
||||
constructor(projection, tilesConfig, opts = {}) {
|
||||
if (!opts.app) console.error('Deepzoom Mapdata needs an app set in the options.')
|
||||
if (tilesConfig.clip) {
|
||||
opts.clip = {
|
||||
min: {
|
||||
x: tilesConfig.clip.bounds.min[0],
|
||||
y: tilesConfig.clip.bounds.min[1]
|
||||
},
|
||||
max: {
|
||||
x: tilesConfig.clip.bounds.max[0],
|
||||
y: tilesConfig.clip.bounds.max[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(tilesConfig, opts)
|
||||
|
||||
super(projection, opts)
|
||||
this.app = opts.app
|
||||
}
|
||||
}
|
340
lib/pixi/maps/maps.html
Normal file
@ -0,0 +1,340 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>Maps</title>
|
||||
|
||||
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../../../css/doctest.css'>
|
||||
|
||||
<script src="../../../dist/iwmlib.3rdparty.js"></script>
|
||||
<script src="../../../dist/iwmlib.js"></script>
|
||||
<script src="../../../dist/iwmlib.pixi.js"></script>
|
||||
|
||||
|
||||
<style>
|
||||
.inline-showcase {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.map-example {
|
||||
display: inline-block;
|
||||
width: 256px;
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload='Doctest.run()'>
|
||||
<h1>Maps</h1>
|
||||
<p>Maps represent a geographical image in a PIXI.Application. Preferably in a MapApp to have more convenient methods to
|
||||
handle the maps.</p>
|
||||
|
||||
<h2>Cover Test</h2>
|
||||
<p>Usually maps should cover the whole app, or a window inside the app. Therefore they support a cover option.
|
||||
This option allows the
|
||||
</p>
|
||||
<canvas id="big_map">
|
||||
|
||||
</canvas>
|
||||
<script>
|
||||
|
||||
// let iwm = { x: 48.52454, y: 9.05468 }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// let luftbildMap = new DeepZoomMap(luftbildData, luftbildConfig, { alpha: 0.8, cover: true, onTransform: transformed.bind(this) })
|
||||
|
||||
|
||||
|
||||
// const app = window.app = new MapApp({
|
||||
// transparent: false,
|
||||
// backgroundColor: 0x00ffff,
|
||||
// view: big_map,
|
||||
// root: "../",
|
||||
// width: 512,
|
||||
// height: 512,
|
||||
// coordsLogging: true,
|
||||
// focus: iwm,
|
||||
// zoom: 18
|
||||
// })
|
||||
// app.setup().run()
|
||||
|
||||
// let markerLayer = new GeoLayer({ name: "Marker Layer" })
|
||||
// let iwmMarker = new GeoPoint(iwm)
|
||||
|
||||
// let marker = new PIXI.Graphics()
|
||||
// marker.beginFill(0xFF6900)
|
||||
// marker.drawCircle(0, 0, 1)
|
||||
// marker.endFill()
|
||||
// iwmMarker.graphics.addChild(marker)
|
||||
|
||||
// markerLayer.place(iwmMarker)
|
||||
// app.mapLayer.place(markerLayer)
|
||||
|
||||
// app.setMap("luftbild", luftbildMap)
|
||||
|
||||
// function transformed(e) {
|
||||
// markerLayer.adapt(app.map)
|
||||
// }
|
||||
</script>
|
||||
<h2>Map Types</h2>
|
||||
<div class="inline-showcase">
|
||||
<div class="map-example">
|
||||
<canvas id="imagemap_canvas"></canvas>
|
||||
|
||||
<h3>Image Map</h3>
|
||||
<p>A map, that is represented by a single image.</p>
|
||||
</div>
|
||||
<div class="map-example">
|
||||
<canvas id="dzi_canvas"></canvas>
|
||||
<h3>Deep Zoom Map</h3>
|
||||
<p>The map representation is a DeepZoomImage, that can be scaled much more deeply, without losing quality by increasing
|
||||
the amount of tiles, that represent the image.</p>
|
||||
<p>
|
||||
You should see a map of the world with a set of cities highlighted with dots. When pressing the button all views should jump
|
||||
to the same point.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script class='doctest'>
|
||||
|
||||
let osmConfig = {
|
||||
"projection": "mercator",
|
||||
"type": "deepzoom",
|
||||
"tiles": {
|
||||
"tileSize": 256,
|
||||
"format": "png",
|
||||
"overlap": 0,
|
||||
"type": "map",
|
||||
"height": 1024,
|
||||
"width": 1024,
|
||||
"path": "../../examples/osm",
|
||||
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Berlin = { x: 52.514961, y: 13.401366 }
|
||||
|
||||
const deepZoomExampleApp = new MapApp({
|
||||
view: dzi_canvas,
|
||||
root: "../",
|
||||
width: 256,
|
||||
height: 256,
|
||||
coordsLogging: true,
|
||||
focus: Berlin
|
||||
})
|
||||
|
||||
let osmMapData = new DeepZoomMapData(new Projection.Mercator(), osmConfig.tiles,{
|
||||
app:deepZoomExampleApp
|
||||
})
|
||||
|
||||
let deepZoomMap = new DeepZoomMap(osmMapData, Object.assign({},osmConfig.tiles,{app:deepZoomExampleApp}), { cover: false })
|
||||
|
||||
deepZoomExampleApp.setMap("deepzoom", deepZoomMap)
|
||||
|
||||
|
||||
deepZoomExampleApp.setup().run()
|
||||
console.log(deepZoomMap)
|
||||
|
||||
|
||||
// let imageApp = new MapApp({
|
||||
// view: imagemap_canvas,
|
||||
// width: 256,
|
||||
// height: 256,
|
||||
// focus: constanceRhineBridge,
|
||||
// coordsLogging: true,
|
||||
// })
|
||||
|
||||
// imageApp.setup().run()
|
||||
|
||||
// let germany = "./examples/germany.jpg"
|
||||
|
||||
// imageApp.loadSprites([
|
||||
// germany
|
||||
// ], (sprites) => ready(sprites), { resolutionDependent: false })
|
||||
|
||||
// let ready = (sprites) => {
|
||||
// let imageMap = new ImageMap(sprites.get(germany), germanyData,{
|
||||
// cover: false
|
||||
// })
|
||||
// imageApp.setMap("germany", imageMap)
|
||||
// }
|
||||
|
||||
|
||||
// TODO Fix this when 'stadtplan' is available again.
|
||||
// let stadtplanConfig = tilesConfig["luftbild_2017"]
|
||||
// stadtplanConfig.path = "../" + stadtplanConfig.path
|
||||
// let dziData = new MapData(MERCATOR, {
|
||||
// clip: {
|
||||
// "min": { x: 48.464967, y: 8.979732 },
|
||||
// "max": { x: 48.5674, y: 9.2817 }
|
||||
// }
|
||||
// })
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<h1>Movement</h1>
|
||||
<p>
|
||||
Its crucial to focus certain points in a map application. The following tests the behavioud of maps inside a mapapp and maps
|
||||
inside of windows.
|
||||
</p>
|
||||
|
||||
</ul>
|
||||
<h2>Map Frame</h2>
|
||||
<p>
|
||||
The map's frame specifies the stage of the map. When moving to the focus point normally, the map focuses in the center, the
|
||||
frame defines the new area, the map will be centered in. This is important when trying to mask a map.
|
||||
</p>
|
||||
<canvas id="canv_0"></canvas>
|
||||
<div class="controls">
|
||||
<button id="swapBtn_0">Change Map</button>
|
||||
<button id="nextCapital">Next Capital</button>
|
||||
<input type="number" name="0" id="animationTime" min="0" value="0.35">
|
||||
<span id="msg"></span>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
let visiting = 0
|
||||
|
||||
capitals = {
|
||||
abidjan: { x: 5.349470, y: -4.006472 },
|
||||
berlin: { x: 52.525430, y: 13.385291 },
|
||||
canberra: { x: -35.282025, y: 149.128648 },
|
||||
capetown: { x: -33.925448, y: 18.416962 },
|
||||
moscow: { x: 55.750892, y: 37.622799 },
|
||||
washington: { x: 38.895650, y: -77.031407 },
|
||||
rio: { x: -22.871400, y: -43.280490 },
|
||||
tokio: { x: 35.696278, y: 139.731366 }
|
||||
}
|
||||
|
||||
let pointApp = window.app = new MapApp({
|
||||
view: canv_0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
coordsLogging: true
|
||||
})
|
||||
|
||||
let movementMap = new DeepZoomMap(osmMapData, Object.assign({},osmConfig.tiles,{app:pointApp}), { cover: false })
|
||||
|
||||
pointApp.setMap("deepzoom", movementMap)
|
||||
pointApp.setup().run()
|
||||
|
||||
// var osmworld = "examples/world_square.png"
|
||||
// var world2 = "examples/world.jpg"
|
||||
|
||||
// let worlOSMdData = new MapData(MERCATOR)
|
||||
|
||||
// let world2Data = new MapData(MERCATOR, {
|
||||
// translate: { x: 0, y: -11.035417 },
|
||||
// clip: {
|
||||
// min: { x: -69.870531, y: -180 },
|
||||
// max: { x: 85, y: 180 }
|
||||
// }
|
||||
// })
|
||||
|
||||
// // The sprites of image maps should be loaded by the
|
||||
// // apps resources loader.
|
||||
// pointApp.loadSprites([
|
||||
// osmworld,
|
||||
// world2
|
||||
// ], (sprites) => point_ready(sprites), { resolutionDependent: false })
|
||||
|
||||
|
||||
let frame = new PIXI.Graphics()
|
||||
frame.beginFill(0xFFFFFF, 0.5)
|
||||
const border = 4
|
||||
frame.lineStyle(border, 0xff0000, 0.8)
|
||||
frame.drawRect(border / 2, border / 2, pointApp.width / 2, pointApp.height / 2)
|
||||
const focusWidth = 4
|
||||
frame.endFill()
|
||||
frame.drawCircle(pointApp.width / 4 + border / 2, pointApp.height / 4 + border / 2, 10)
|
||||
|
||||
frame.interactive = true
|
||||
|
||||
new DisplayObjectScatter(frame)
|
||||
frame.scatter.position = { x: 50, y: 50 }
|
||||
|
||||
function placeCapitals(maplayer){
|
||||
let capitalLayer = new GeoLayer({ name: "Capital Overlay" })
|
||||
|
||||
for (key in capitals) {
|
||||
let capitalPoint = new GeoPoint(capitals[key], {
|
||||
|
||||
/**
|
||||
* To style GeoGraphics, the fill has to be set in the
|
||||
* onDraw callback.
|
||||
*
|
||||
* Note: GeoPoints are the only GeoGraphic with no
|
||||
* initial geometry attached. These have to be drawn to
|
||||
* the graphics object manually.
|
||||
*/
|
||||
onDraw: function () {
|
||||
this.graphics.beginFill(0xFF0000)
|
||||
this.graphics.drawCircle(0, 0, 5)
|
||||
this.graphics.endFill()
|
||||
}
|
||||
})
|
||||
capitalLayer.place(capitalPoint)
|
||||
}
|
||||
|
||||
maplayer.place(capitalLayer)
|
||||
capitalLayer.adapt(pointApp.map)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script class="doctest">
|
||||
// function point_ready(sprites) {
|
||||
// let maps_0 = ["world", "world2"]
|
||||
|
||||
// let sprite = sprites.get(osmworld)
|
||||
|
||||
// let worldOSMMap = new ImageMap(sprite, worlOSMdData, {
|
||||
// cover: false,
|
||||
// onLoaded: () => {
|
||||
// worldOSMMap.setFrame(frame)
|
||||
// }
|
||||
// })
|
||||
// pointApp.setMap(maps_0[0], worldOSMMap)
|
||||
|
||||
// let world2Map = new ImageMap(sprites.get(world2), world2Data, {
|
||||
// cover: false,
|
||||
// startScale: 1,
|
||||
// onLoaded: function () {
|
||||
// this.setFrame(frame)
|
||||
// }
|
||||
// })
|
||||
|
||||
// pointApp.addMap(maps_0[1], world2Map)
|
||||
// pointApp.pixiLayer.place(frame)
|
||||
|
||||
|
||||
placeCapitals(pointApp.mapLayer)
|
||||
|
||||
let active_0 = 0
|
||||
swapBtn_0.addEventListener("click", () => {
|
||||
active_0++
|
||||
pointApp.selectMap(maps_0[active_0 % maps_0.length])
|
||||
})
|
||||
|
||||
nextCapital.addEventListener("click", () => {
|
||||
let keys = Object.keys(capitals)
|
||||
visiting = (++visiting) % keys.length
|
||||
let key = keys[visiting]
|
||||
console.log(key)
|
||||
|
||||
pointApp.map.moveTo(capitals[key], null, { animate: parseFloat(animationTime.value) })
|
||||
msg.innerHTML = "Visiting " + key + "."
|
||||
})
|
||||
// }
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
123
lib/pixi/maps/mapview.js
Normal file
@ -0,0 +1,123 @@
|
||||
import { DeepZoomMap } from './map.js'
|
||||
|
||||
/**
|
||||
* The MapView class is responsible for a consistent map view.
|
||||
* It is aware of the current viewposition, the scale and viewport.
|
||||
* It ensures, that maps can be changed, without the user noticing it.
|
||||
*
|
||||
*/
|
||||
export default class MapView {
|
||||
/**
|
||||
*
|
||||
* @param {object} [focus = {x:0, y:0}] - Defines the startup focuspoint of the app.
|
||||
* @param {number} [zoom = 0] - Defines the startup zoom of the app. Note that this is just a request.
|
||||
* The MapView will prioritize a full scale app, than displaying the demanded zoom factor
|
||||
*/
|
||||
constructor({
|
||||
map = null,
|
||||
focus = null,
|
||||
zoom = null,
|
||||
viewport = { min: { x: -85, y: -180 }, max: { x: 85, y: 180 } }
|
||||
} = {}) {
|
||||
this.viewport = viewport
|
||||
this._focus = focus
|
||||
this._zoom = zoom
|
||||
this._map = map
|
||||
this.referenceHeight = 256
|
||||
}
|
||||
|
||||
get focus() {
|
||||
return this._focus
|
||||
}
|
||||
get zoom() {
|
||||
return this._zoom
|
||||
}
|
||||
|
||||
get map() {
|
||||
return this._map
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the map to the given focuspoint and zoom factor.
|
||||
*/
|
||||
setMap(map) {
|
||||
this._map = map
|
||||
this.update()
|
||||
return true
|
||||
}
|
||||
|
||||
update() {
|
||||
this.map.moveTo(this._focus, this._zoom)
|
||||
}
|
||||
|
||||
transformed(e) {
|
||||
this.updateZoom()
|
||||
this.updateFocusPoint()
|
||||
}
|
||||
|
||||
applyCameraPosition() {
|
||||
this.updateFocusPoint()
|
||||
this.updateZoom()
|
||||
}
|
||||
|
||||
updateFocusPoint() {
|
||||
const frame = this.map.getFrame()
|
||||
this._focus = this.coordinatesFromWindowPoint(frame.localCenter)
|
||||
}
|
||||
|
||||
updateZoom() {
|
||||
/**
|
||||
* TODO: This relies on the fact, that all maps have the same tileSize,
|
||||
* if a set would have a smaller tileSize. Improve that.
|
||||
*/
|
||||
if (this.map instanceof DeepZoomMap) this._zoom = this.map.floatingLevelForScale(this.map.image.scatter.scale)
|
||||
else {
|
||||
this._zoom = this.map.zoom
|
||||
console.warn('Zoom is not yet correctly implemented in this Map type: ' + this.map)
|
||||
}
|
||||
}
|
||||
|
||||
mapPointToWindowPoint(point) {
|
||||
let container = this.map.image.parent
|
||||
|
||||
let _point = new PIXI.Point(
|
||||
this.map.scatter.position.x + this.map.scatter.scale * point.x,
|
||||
this.map.scatter.position.y + this.map.scatter.scale * point.y
|
||||
)
|
||||
|
||||
return container.toGlobal(_point)
|
||||
}
|
||||
|
||||
windowPointToMapPoint(point) {
|
||||
let offset = this.map.image.parent.toGlobal({ x: 0, y: 0 })
|
||||
let _point = new PIXI.Point(
|
||||
(point.x - this.map.scatter.position.x - offset.x) / this.map.scatter.scale,
|
||||
(point.y - this.map.scatter.position.y - offset.y) / this.map.scatter.scale
|
||||
)
|
||||
|
||||
return _point
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the coordinates of a specific point in the viewport.
|
||||
*
|
||||
* @param {PIXI.Point | {x,y}} point - Pixel position in the viewport.
|
||||
* @returns {{x,y}} Coordinates on the map of the provided position.
|
||||
* @memberof MapView
|
||||
*/
|
||||
coordinatesFromWindowPoint(point) {
|
||||
let position = {
|
||||
x: point.x - this.map.scatter.position.x,
|
||||
y: point.y - this.map.scatter.position.y
|
||||
}
|
||||
|
||||
let normalized = {
|
||||
x: position.x / (this.map.width * this.map.scatter.scale),
|
||||
y: position.y / (this.map.height * this.map.scatter.scale)
|
||||
}
|
||||
|
||||
let coordinates = this.map.mapdata.toCoordinates(normalized)
|
||||
|
||||
return coordinates
|
||||
}
|
||||
}
|
261
lib/pixi/maps/overlay.html
Normal file
@ -0,0 +1,261 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Overlay</title>
|
||||
|
||||
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../../../css/doctest.css'>
|
||||
|
||||
<script src="../../../dist/iwmlib.3rdparty.js"></script>
|
||||
<script src="../../../dist/iwmlib.js"></script>
|
||||
<script src="../../../dist/iwmlib.pixi.js"></script>
|
||||
|
||||
|
||||
<style>
|
||||
.inline-showcase {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.map-example {
|
||||
display: inline-block;
|
||||
width: 256px;
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="Doctest.run()">
|
||||
<h1>Overlay</h1>
|
||||
<p>
|
||||
The overlayclass creates a convenient way to create and design
|
||||
complex map overlays.
|
||||
</p>
|
||||
<canvas id="view"> </canvas>
|
||||
<script>
|
||||
window.pointApp = window.app = new MapApp({
|
||||
view,
|
||||
width: 512,
|
||||
height: 512,
|
||||
coordsLogging: true
|
||||
})
|
||||
var osmworld = '../assets/maps/osm/0/0/0.png'
|
||||
let worlOSMData = new MapData(new Projection.Mercator())
|
||||
</script>
|
||||
<script class="doctest">
|
||||
|
||||
/**
|
||||
* To create an overlay you just need to create a JSON file.
|
||||
* The styles defined in the styles are cascading, meaning that
|
||||
* the styles can be defined on the top level, or directly at the
|
||||
* single items. If the style is not defined in the item, the top-level style will be used.
|
||||
* If the top-level style is not defined either, the default style is applied.
|
||||
*
|
||||
* The structure of an overlay file is as follows:
|
||||
*
|
||||
* {
|
||||
* //COMMENT: Top-Level
|
||||
*
|
||||
* //COMMENT: Frequently used properties are:
|
||||
* size: 500,
|
||||
* color: 0xFF00FF,
|
||||
* fillAlpha:0.5
|
||||
*
|
||||
* icon: 'path/to/icon.png'
|
||||
* iconColor: 0xFFFFFF,
|
||||
* "items": [
|
||||
* //COMMENT: Items are defined in the items array.
|
||||
* {
|
||||
* location: {
|
||||
* //COMMENT: Location must be defined.
|
||||
* x: LAT_VALUE,
|
||||
* y: LNG_VALUE
|
||||
* }
|
||||
* },{
|
||||
* color: 0xFF0000,
|
||||
* location: {
|
||||
* //COMMENT: Location must be defined.
|
||||
* x: LAT_VALUE,
|
||||
* y: LNG_VALUE
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
|
||||
let exampleOverlayJSON = {
|
||||
icon: '../../../assets/icons/place.png',
|
||||
iconColor: '0x000000',
|
||||
iconAnchor: { x: 0.5, y: 1 },
|
||||
scale: 0.2,
|
||||
color: '0xFF00FF',
|
||||
fillAlpha: 0,
|
||||
items: [
|
||||
{
|
||||
name: 'Abidjan',
|
||||
fontWeight: "bold",
|
||||
icon: '../../../assets/icons//beenhere.png',
|
||||
iconColor: 0x00ff00,
|
||||
labelVerticalAlignment: "underneath",
|
||||
label: 'Abidjan',
|
||||
location: {
|
||||
x: 5.34947,
|
||||
y: -4.006472
|
||||
},
|
||||
information:
|
||||
'... der größte städtische Ballungsraum der Elfenbeinküste.'
|
||||
},
|
||||
{
|
||||
name: 'Berlin',
|
||||
|
||||
location: {
|
||||
x: 52.52543,
|
||||
y: 13.385291
|
||||
},
|
||||
information:
|
||||
'... ist die Bundeshauptstadt der Bundesrepublik Deutschland.'
|
||||
},
|
||||
{
|
||||
name: 'Canberra',
|
||||
|
||||
location: {
|
||||
x: -35.282025,
|
||||
y: 149.128648
|
||||
},
|
||||
information:
|
||||
'... ist die Hauptstadt und achtgrößte Stadt Australiens.'
|
||||
},
|
||||
{
|
||||
name: 'Kapstadt',
|
||||
|
||||
location: {
|
||||
x: -33.925448,
|
||||
y: 18.416962
|
||||
},
|
||||
information:
|
||||
'... ist eine Hafenstadt an der Südwestküste Südafrikas auf einer Halbinsel, die vom beeindruckenden Tafelberg dominiert wird.'
|
||||
},
|
||||
{
|
||||
name: 'Moskau',
|
||||
|
||||
location: {
|
||||
x: 55.750892,
|
||||
y: 37.622799
|
||||
},
|
||||
information:
|
||||
'... die kosmopolitische Hauptstadt Russlands, liegt im Westen des Landes und wird von der Moskwa durchflossen.'
|
||||
},
|
||||
{
|
||||
name: 'Washington, D.C.',
|
||||
|
||||
location: {
|
||||
x: 38.89565,
|
||||
y: -77.031407
|
||||
},
|
||||
information:
|
||||
'... ist die Hauptstadt der USA und eine kompakte Stadt am Potomac River, die an die Bundesstaaten Maryland und Virginia grenzt.'
|
||||
},
|
||||
{
|
||||
name: 'Rio de Janeiro',
|
||||
|
||||
location: {
|
||||
x: -22.8714,
|
||||
y: -43.28049
|
||||
},
|
||||
information:
|
||||
'... ist eine ausgedehnte brasilianische Küstenmetropole. '
|
||||
},
|
||||
{
|
||||
name: 'Tokio',
|
||||
|
||||
location: {
|
||||
x: 35.696278,
|
||||
y: 139.731366
|
||||
},
|
||||
information:
|
||||
'... ist eine Global City in der Kantō-Region im Osten der japanischen Hauptinsel Honshū.'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let overlay = new Overlay(exampleOverlayJSON)
|
||||
|
||||
/**
|
||||
* Textures should be loaded using the app's texture loader.
|
||||
* With the findTexture method all required Textures within the
|
||||
* overlays are loaded.
|
||||
*/
|
||||
let overlayTextures = overlay.findAllTextures()
|
||||
|
||||
let list = [osmworld].concat(overlayTextures)
|
||||
pointApp.loadTextures(list, textures => texturesLoaded(textures), {
|
||||
resolutionDependent: false
|
||||
})
|
||||
|
||||
function texturesLoaded(textures) {
|
||||
|
||||
/** When all textures are loaded .... */
|
||||
|
||||
// Create the map!
|
||||
let worldOSMMap = new ImageMap(
|
||||
new PIXI.Sprite(textures.get(osmworld)),
|
||||
worlOSMData,
|
||||
{
|
||||
cover: false,
|
||||
onLoaded: () => {
|
||||
worldOSMMap.setFrame(frame)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Setup the map in the mapapp.
|
||||
pointApp.setMap('osm', worldOSMMap)
|
||||
pointApp.setup().run()
|
||||
|
||||
//Retrieve all overlay textures.
|
||||
overlay.selectTextures(textures)
|
||||
|
||||
// Just some Helpers for the Popups.
|
||||
let popup = null
|
||||
let cleaner = null
|
||||
const vanishingTime = 1000
|
||||
|
||||
/**
|
||||
*
|
||||
* The actual PIXI elements are created when the overlay.create() is called.
|
||||
* This returns an GeoLayer which can be directly put onto the map's Maplayer (or any other GeoLayer).
|
||||
*
|
||||
* INFO: MapLayer and GeoLayer are specialized container for PIXI. They are meant to
|
||||
* place PIXI Elements according to their world-coordinates (lat/lng position), instead
|
||||
* of actual pixel values.
|
||||
*
|
||||
*/
|
||||
|
||||
let exampleOverlayGeoLayer = overlay.create()
|
||||
|
||||
// When placed on the mapLayer, the PIXI Graphic elements, that
|
||||
// reside inside GeoGraphic Objects are placed automatically at the
|
||||
// correct coordinates of the map.
|
||||
pointApp.mapLayer.place(exampleOverlayGeoLayer)
|
||||
|
||||
|
||||
// Just a helper function that clears the popups and removes
|
||||
// a remaining cleaner timeout.
|
||||
function clearPopup() {
|
||||
if (popup) {
|
||||
popup.parent.removeChild(popup)
|
||||
popup = null
|
||||
}
|
||||
|
||||
if (cleaner != null) {
|
||||
clearTimeout(cleaner)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
571
lib/pixi/maps/overlay.js
Normal file
@ -0,0 +1,571 @@
|
||||
import { GeoLayer } from './geolayer.js'
|
||||
import { GeoPoint, GeoMultiShape } from './geographics.js'
|
||||
import GeoJson from './geojson.js'
|
||||
|
||||
import Popup from '../popup.js'
|
||||
|
||||
/**
|
||||
* The Overlay-Class shall provide and easy to use frameworkd
|
||||
* of marking points of interest on a map, using shapes, geometries,
|
||||
* items or text.
|
||||
*
|
||||
* It's highly configurable and adaptable to a variety of scenarios.
|
||||
*
|
||||
*/
|
||||
|
||||
export default class Overlay {
|
||||
/**
|
||||
*Creates an instance of Overlay.
|
||||
* @param {*} [{
|
||||
* name = "unnamed", {string}
|
||||
* connector = false,
|
||||
* connectorType = "line",
|
||||
* location = null,
|
||||
* label = false,
|
||||
* labelType = null,
|
||||
* labelLocation = null,
|
||||
* labelAlignment = "center",
|
||||
* labelVerticalAlign = "underneath",
|
||||
* labelTarget = null,
|
||||
* shape = "geometry", {string} - Defines the shape to be used: geometry, circle, rectangle
|
||||
* geometry = null,
|
||||
* geometryType = "point",
|
||||
* geometryFallback = "circle",
|
||||
* informationPopup = "infocard",
|
||||
*
|
||||
* }={}]
|
||||
* @memberof Overlay
|
||||
*/
|
||||
constructor(opts = {}) {
|
||||
let defaultTextStyle = new PIXI.TextStyle()
|
||||
|
||||
Object.assign(
|
||||
this,
|
||||
{
|
||||
type: 'auto',
|
||||
name: 'unnamed',
|
||||
|
||||
borderColor: 0x000000,
|
||||
borderThickness: 5,
|
||||
|
||||
disabledAlpha: 1,
|
||||
disabledColor: 0xaa1111,
|
||||
disabledBorderColor: 0x000000,
|
||||
enabled: true,
|
||||
labelScale: 1,
|
||||
location: null,
|
||||
label: false,
|
||||
labelType: null,
|
||||
labelLocation: null,
|
||||
labelAlignment: 'center',
|
||||
labelVerticalAlign: 'underneath',
|
||||
labelSpacing: 10,
|
||||
labelTarget: null,
|
||||
geometry: null,
|
||||
information: '',
|
||||
informationPath: null,
|
||||
informationType: 'popup',
|
||||
icon: null,
|
||||
iconAlpha: 1,
|
||||
iconAnchor: { x: 0.5, y: 0.5 },
|
||||
iconColor: 0xffffff,
|
||||
iconScale: 1,
|
||||
iconOffset: { x: 0, y: 0 },
|
||||
items: [],
|
||||
color: 0xff00ff,
|
||||
fillAlpha: 1,
|
||||
size: 50,
|
||||
scale: 1,
|
||||
rescale: false,
|
||||
popoverOffset: { x: 0, y: 0 },
|
||||
|
||||
/**
|
||||
* The following Attributes are taken from the TextStyle class
|
||||
* of PIXI. All can be overwritten in the overlay's JSON file.
|
||||
*/
|
||||
labelMultiLineAlign: defaultTextStyle.align,
|
||||
labelBreakWords: defaultTextStyle.breakWords,
|
||||
labelDropShadow: defaultTextStyle.dropShadow,
|
||||
labelDropShadowAlpha: defaultTextStyle.dropShadowAlpha,
|
||||
labelDropShadowAngle: defaultTextStyle.dropShadowAngle,
|
||||
labelDropShadowBlur: defaultTextStyle.dropShadowBlur,
|
||||
labelDropShadowColor: defaultTextStyle.dropShadowColor,
|
||||
labelDropShadowDistance: defaultTextStyle.dropShadowDistance,
|
||||
fontColor: 0x000000,
|
||||
fontFamily: defaultTextStyle.fontFamily,
|
||||
fontSize: defaultTextStyle.fontSize,
|
||||
fontStyle: defaultTextStyle.fontStyle,
|
||||
fontWeight: defaultTextStyle.fontWeight,
|
||||
labelStroke: defaultTextStyle.stroke,
|
||||
labelStrokeThickness: defaultTextStyle.strokeThickness,
|
||||
wordWrap: defaultTextStyle.wordWrap,
|
||||
wordWrapWidth: defaultTextStyle.wordWrapWidth,
|
||||
labelFill: defaultTextStyle.fill,
|
||||
labelFillGradientType: defaultTextStyle.fillGradientType,
|
||||
labelFillGradientStops: defaultTextStyle.fillGradientStops,
|
||||
fontVariant: defaultTextStyle.fontVariant,
|
||||
labelLeading: defaultTextStyle.leading,
|
||||
letterSpacing: defaultTextStyle.letterSpacing,
|
||||
labelLineHeight: defaultTextStyle.lineHeight,
|
||||
labelLineJoin: defaultTextStyle.lineJoin,
|
||||
labelMiterLimit: defaultTextStyle.miterLimit,
|
||||
labelPadding: defaultTextStyle.padding,
|
||||
labelTrim: defaultTextStyle.trim,
|
||||
textBaseline: defaultTextStyle.textBaseline,
|
||||
labelWhiteSpace: defaultTextStyle.whiteSpace
|
||||
},
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
get textureParameters() {
|
||||
return ['icon']
|
||||
}
|
||||
|
||||
setTexture(parameter, key, path) {
|
||||
let obj = (this[parameter] = {})
|
||||
obj._key = key
|
||||
obj._path = path
|
||||
obj._texture = null
|
||||
|
||||
obj.initialize = function(texture) {
|
||||
if (!texture) console.error('Could not initialize texture. Invalid value provided: ', texture)
|
||||
this._texture = texture
|
||||
}.bind(obj)
|
||||
|
||||
obj.__defineGetter__('texture', function() {
|
||||
if (!obj._texture) {
|
||||
console.error('Texture was not yet initialized! ', obj)
|
||||
} else {
|
||||
return obj._texture
|
||||
}
|
||||
})
|
||||
|
||||
obj.__defineGetter__('key', () => {
|
||||
return obj._key
|
||||
})
|
||||
obj.__defineGetter__('path', () => {
|
||||
return obj._path
|
||||
})
|
||||
}
|
||||
|
||||
apply(parameter, func, obj = null) {
|
||||
if (obj == null) return
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach(child => {
|
||||
if (typeof child == 'object') this.apply(parameter, func, child)
|
||||
})
|
||||
} else {
|
||||
for (let [key, val] of Object.entries(obj)) {
|
||||
if (val == null) continue
|
||||
if (key == parameter) {
|
||||
func.call(obj, val)
|
||||
}
|
||||
|
||||
if (typeof val == 'object') {
|
||||
this.apply(parameter, func, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an overlay from the current Object.
|
||||
*
|
||||
* @param {settings} object - Object to configure the creation process.
|
||||
* @param {settings.excludeItems} function - You may provide a function, that excludes elements selectively. The function must return true for items to exclude them.
|
||||
* @param {settings.adjustItems} function - You may adjust the created PIXI items, using the adjustItems function.
|
||||
* @returns {GeoLayer} - Returns the created GeoLayer.
|
||||
* @memberof Overlay
|
||||
*/
|
||||
create({
|
||||
excludeItems = item => {
|
||||
return false
|
||||
},
|
||||
informationCallback = null,
|
||||
adjustItems = null
|
||||
} = {}) {
|
||||
const name = this.name[0].toUpperCase() + this.name.slice(1).toLowerCase() + ' Overlay'
|
||||
let geoLayer = new GeoLayer({ name })
|
||||
if (this.rescale) geoLayer.rescale = this.rescale
|
||||
this.items.forEach(item => {
|
||||
if (!excludeItems(item)) {
|
||||
if (adjustItems) {
|
||||
adjustItems(item)
|
||||
}
|
||||
|
||||
item.overlay = this
|
||||
let graphics = this.createItem(item, informationCallback)
|
||||
geoLayer.place(graphics)
|
||||
}
|
||||
})
|
||||
return geoLayer
|
||||
}
|
||||
|
||||
getItemProperty(item, property) {
|
||||
let propertyValue = null
|
||||
const propertyExistsOnItem = item[property] !== undefined
|
||||
const propertyExistsOnOverlay = this[property] !== undefined
|
||||
|
||||
if (propertyExistsOnItem) propertyValue = item[property]
|
||||
else if (propertyExistsOnOverlay) propertyValue = this[property]
|
||||
else {
|
||||
console.error(`Invalid property: ${property} on`, item)
|
||||
}
|
||||
|
||||
propertyValue = this._resolveReference(item, propertyValue)
|
||||
|
||||
return propertyValue
|
||||
}
|
||||
|
||||
_isReference(property) {
|
||||
if (property && typeof property == 'object' && property.hasOwnProperty('@property')) return true
|
||||
else return false
|
||||
}
|
||||
|
||||
_resolveReference(item, property) {
|
||||
if (this._isReference(property)) {
|
||||
let referencedProperty = property['@property']
|
||||
return this.getItemProperty(item, referencedProperty)
|
||||
} else {
|
||||
return property
|
||||
}
|
||||
}
|
||||
|
||||
findAllTextures() {
|
||||
let textures = []
|
||||
|
||||
this.textureParameters.forEach(parameter => {
|
||||
if (this.hasOwnProperty(parameter) && this[parameter] != null) {
|
||||
textures.push(this[parameter])
|
||||
}
|
||||
|
||||
this.items.forEach(item => {
|
||||
if (item.hasOwnProperty(parameter) && item[parameter] != null) {
|
||||
textures.push(item[parameter])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return textures
|
||||
}
|
||||
|
||||
selectTextures(textures) {
|
||||
this.textureParameters.forEach(parameter => {
|
||||
if (this.hasOwnProperty(parameter) && this[parameter]) {
|
||||
let path = this[parameter]
|
||||
const texture = textures.get(path)
|
||||
if (!texture) console.error('Texture could not be selected: ', path)
|
||||
this[parameter] = {
|
||||
texture,
|
||||
path
|
||||
}
|
||||
}
|
||||
this.items.forEach((item, idx) => {
|
||||
if (this.items[idx].hasOwnProperty(parameter) && this.items[idx][parameter]) {
|
||||
const path = this.items[idx][parameter]
|
||||
const texture = textures.get(path)
|
||||
|
||||
this.items[idx][parameter] = {
|
||||
path,
|
||||
texture
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createItem(item, informationCallback = null) {
|
||||
let geographics
|
||||
|
||||
/**
|
||||
* Use a factory to draw the items, if a type is specified.
|
||||
*/
|
||||
if (this.type != 'auto') {
|
||||
let geographicsFactory = Overlay.requestFactory(this.type)
|
||||
if (!geographicsFactory)
|
||||
console.error(
|
||||
'Invalid Overlay Mode: ' +
|
||||
this.type +
|
||||
'. Fallback to auto mode. Overlaymodes must be registeres beforehand. Valid modes are: ' +
|
||||
Overlay.listFactories().join(',') +
|
||||
'.'
|
||||
)
|
||||
else {
|
||||
let actualItem = Object.assign({}, this, item)
|
||||
geographics = geographicsFactory(actualItem)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Otherwise draw the default geographics.
|
||||
*/
|
||||
if (!geographics) {
|
||||
if (item.geometry == null || item.geometry == undefined) {
|
||||
if (item.location) {
|
||||
geographics = this._drawPoint(item, informationCallback)
|
||||
const scale = this.getItemProperty(item, 'scale')
|
||||
geographics.graphics.scale.set(scale, scale)
|
||||
} else console.warn("Item could not be drawn. Specify a 'location' or a 'geometry'.", item)
|
||||
} else {
|
||||
geographics = this._drawGeometry(item)
|
||||
this._drawRoutine(geographics, item, informationCallback)
|
||||
}
|
||||
|
||||
this._drawLabel(item, geographics)
|
||||
}
|
||||
|
||||
return geographics
|
||||
}
|
||||
|
||||
_drawLabel(item, geographics) {
|
||||
let label = this.getItemProperty(item, 'label')
|
||||
if (label) {
|
||||
let textStyle = this._gatherFontStyle(item)
|
||||
let text = new PIXI.Text(label, textStyle)
|
||||
|
||||
let labelScale = this.getItemProperty(item, 'labelScale')
|
||||
text.scale.set(labelScale, labelScale)
|
||||
|
||||
let position = new PIXI.Point()
|
||||
|
||||
let align = this.getItemProperty(item, 'labelAlignment')
|
||||
if (align == 'left');
|
||||
else if (align == 'center') position.set(text.position.x - text.width / 2, text.position.y)
|
||||
else if (align == 'right') position.set(text.position.x - text.width, text.position.y)
|
||||
else this._logPropertyNotImplemented('labelAlignment', align)
|
||||
|
||||
let verticalAlign = this.getItemProperty(item, 'labelVerticalAlignment')
|
||||
if (verticalAlign == 'underneath') {
|
||||
let size = this.getItemProperty(item, 'size')
|
||||
let scale = this.getItemProperty(item, 'scale')
|
||||
|
||||
let labelSpacing = this.getItemProperty(item, 'labelSpacing')
|
||||
|
||||
position.y += size * scale + labelSpacing
|
||||
} else if (verticalAlign == 'above') {
|
||||
let size = this.getItemProperty(item, 'size')
|
||||
let scale = this.getItemProperty(item, 'scale')
|
||||
|
||||
let labelSpacing = this.getItemProperty(item, 'labelSpacing')
|
||||
|
||||
position.y -= size * scale + text.height + labelSpacing
|
||||
} else this._logPropertyNotImplemented('labelVerticalAlignment', verticalAlign)
|
||||
|
||||
text.position.set(position.x, position.y)
|
||||
|
||||
geographics.graphics.addChild(text)
|
||||
}
|
||||
}
|
||||
|
||||
_gatherFontStyle(item) {
|
||||
return {
|
||||
align: this.getItemProperty(item, 'labelMultiLineAlign'),
|
||||
breakWords: this.getItemProperty(item, 'labelBreakWords'),
|
||||
dropShadow: this.getItemProperty(item, 'labelDropShadow'),
|
||||
dropShadowAlpha: this.getItemProperty(item, 'labelDropShadowAlpha'),
|
||||
dropShadowAngle: this.getItemProperty(item, 'labelDropShadowAngle'),
|
||||
dropShadowBlur: this.getItemProperty(item, 'labelDropShadowBlur'),
|
||||
dropShadowColor: this.getItemProperty(item, 'labelDropShadowColor'),
|
||||
dropShadowDistance: this.getItemProperty(item, 'labelDropShadowDistance'),
|
||||
fontFamily: this.getItemProperty(item, 'fontFamily'),
|
||||
fontSize: this.getItemProperty(item, 'fontSize'),
|
||||
fontStyle: this.getItemProperty(item, 'fontStyle'),
|
||||
fontWeight: this.getItemProperty(item, 'fontWeight'),
|
||||
stroke: this.getItemProperty(item, 'labelStroke'),
|
||||
strokeThickness: this.getItemProperty(item, 'labelStrokeThickness'),
|
||||
wordWrap: this.getItemProperty(item, 'wordWrap'),
|
||||
wordWrapWidth: this.getItemProperty(item, 'wordWrapWidth'),
|
||||
|
||||
fill: this.getItemProperty(item, 'labelFill'),
|
||||
fillGradientType: this.getItemProperty(item, 'labelFillGradientType'),
|
||||
fillGradientStops: this.getItemProperty(item, 'labelFillGradientStops'),
|
||||
fontVariant: this.getItemProperty(item, 'fontVariant'),
|
||||
leading: this.getItemProperty(item, 'labelLeading'),
|
||||
letterSpacing: this.getItemProperty(item, 'letterSpacing'),
|
||||
lineHeight: this.getItemProperty(item, 'labelLineHeight'),
|
||||
lineJoin: this.getItemProperty(item, 'labelLineJoin'),
|
||||
miterLimit: this.getItemProperty(item, 'labelMiterLimit'),
|
||||
padding: this.getItemProperty(item, 'labelPadding'),
|
||||
trim: this.getItemProperty(item, 'labelTrim'),
|
||||
textBaseline: this.getItemProperty(item, 'textBaseline'),
|
||||
whiteSpace: this.getItemProperty(item, 'labelWhiteSpace')
|
||||
}
|
||||
}
|
||||
|
||||
_drawGeometry(item) {
|
||||
let multiPolygonArray = []
|
||||
|
||||
if (Array.isArray(item.geometry))
|
||||
item.geometry.forEach(geometry => {
|
||||
let polygon = this._drawPolygon(geometry)
|
||||
if (polygon != null && polygon != []) multiPolygonArray.push(polygon)
|
||||
})
|
||||
else {
|
||||
let polygon = this._drawPolygon(item.geometry)
|
||||
if (polygon != null && polygon != []) multiPolygonArray.push(polygon)
|
||||
}
|
||||
const overlay = this
|
||||
return new GeoMultiShape(multiPolygonArray, {
|
||||
onDraw: function() {
|
||||
overlay._fill.call(overlay, this, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_drawPolygon(geometry) {
|
||||
let polygon = []
|
||||
if (geometry.type == 'Polygon') {
|
||||
if (GeoJson.validateCoordinates(geometry.type, geometry.coordinates)) {
|
||||
polygon = GeoJson.convert(geometry.type, geometry.coordinates)
|
||||
} else console.error(`Could not validate coordinates for type '${geometry.type}'.`, geometry.coordinates)
|
||||
} else {
|
||||
console.warn(`Geometry of type ${geometry.type} is not implemented yet, hence it was ignored.`)
|
||||
}
|
||||
|
||||
return polygon
|
||||
}
|
||||
|
||||
_drawRoutine(geographics, item, informationCallback = null) {
|
||||
this._createIcon(geographics, item)
|
||||
|
||||
const enabled = this.getItemProperty(item, 'enabled')
|
||||
if (enabled) this._createInformation(geographics, item, informationCallback)
|
||||
}
|
||||
|
||||
_createIcon(geographics, item) {
|
||||
let icon = this.getItemProperty(item, 'icon')
|
||||
if (icon) {
|
||||
if (icon.texture) {
|
||||
let sprite = new PIXI.Sprite(icon.texture)
|
||||
|
||||
const iconAnchor = this.getItemProperty(item, 'iconAnchor')
|
||||
sprite.anchor.set(iconAnchor.x, iconAnchor.y)
|
||||
|
||||
const iconScale = this.getItemProperty(item, 'iconScale')
|
||||
if (iconScale) sprite.scale.set(iconScale, iconScale)
|
||||
|
||||
const iconOffset = this.getItemProperty(item, 'iconOffset')
|
||||
if (iconOffset && iconOffset.x != null && iconOffset.y != null)
|
||||
sprite.position.set(iconOffset.x, iconOffset.y)
|
||||
|
||||
const iconColor = this.getItemProperty(item, 'iconColor')
|
||||
if (iconColor) sprite.tint = iconColor
|
||||
|
||||
const iconAlpha = this.getItemProperty(item, 'iconAlpha')
|
||||
if (iconAlpha) sprite.alpha = iconAlpha
|
||||
|
||||
geographics.graphics.addChild(sprite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createInformation(geographics, item, callback = null) {
|
||||
if (item.information) {
|
||||
/**
|
||||
* SO: The overlay class is quite convenient.
|
||||
* But managing the information creation solely inside this
|
||||
* class restricts us massively.
|
||||
*
|
||||
* Maybe a restructuring would be good, that we can handle
|
||||
* the creation of information with a callback and can adapt
|
||||
* to any occuring situation.
|
||||
*
|
||||
* e.g.
|
||||
*
|
||||
* overlay.informationHandler((geo, type, item)=>{
|
||||
* switch(type){
|
||||
* case "popup":
|
||||
* createPopup(item.information)
|
||||
* break;
|
||||
*
|
||||
* ....
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
|
||||
geographics.graphics.interactive = true
|
||||
const informationType = this.getItemProperty(item, 'informationType')
|
||||
|
||||
if (callback) callback.call(this, geographics, informationType, item)
|
||||
else {
|
||||
this.defaulInformationCallback(geographics, informationType, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaulInformationCallback(geographics, informationType, item) {
|
||||
// Make the items interactive.
|
||||
geographics.graphics.interactive = true
|
||||
geographics.graphics.on('pointerdown', () => {
|
||||
if (!geographics.active) {
|
||||
geographics.active = true
|
||||
|
||||
// Create and place the popup.
|
||||
let popup = new Popup({
|
||||
header: item.name,
|
||||
content: item.information,
|
||||
maxWidth: 350
|
||||
})
|
||||
|
||||
popup.position.set(
|
||||
geographics.graphics.position.x - popup.width / 2,
|
||||
geographics.graphics.position.y - popup.height - 20
|
||||
)
|
||||
|
||||
geographics.graphics.parent.addChild(popup)
|
||||
|
||||
// Remove the items after n milliseconds.
|
||||
const removeTime = 2000
|
||||
setTimeout(() => {
|
||||
geographics.active = false
|
||||
popup.parent.removeChild(popup)
|
||||
}, removeTime)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_fill(geographics, item) {
|
||||
const enabled = this.getItemProperty(item, 'enabled')
|
||||
const color = enabled ? this.getItemProperty(item, 'color') : this.getItemProperty(item, 'disabledColor')
|
||||
let alpha = enabled ? this.getItemProperty(item, 'fillAlpha') : this.getItemProperty(item, 'disabledAlpha')
|
||||
geographics.graphics.beginFill(color, alpha)
|
||||
}
|
||||
|
||||
_drawPoint(item, informationCallback = null) {
|
||||
const overlay = this
|
||||
let geographic = new GeoPoint(item.location, {
|
||||
onDraw: function() {
|
||||
overlay._fill.call(overlay, this, item)
|
||||
const size = overlay.getItemProperty(item, 'size') ? overlay.getItemProperty(item, 'size') : 0
|
||||
this.graphics.drawCircle(0, 0, size)
|
||||
overlay._drawRoutine.call(overlay, this, item, informationCallback)
|
||||
}
|
||||
})
|
||||
return geographic
|
||||
}
|
||||
|
||||
_logPropertyNotImplemented(property, value) {
|
||||
console.error(`Value '${value}' is not yet implemented in property '${property}'.`)
|
||||
}
|
||||
}
|
||||
|
||||
Overlay.Modes = { auto: null }
|
||||
|
||||
Overlay.createFactory = function(name, geographicsFactory) {
|
||||
if (Overlay.Modes.hasOwnProperty(name))
|
||||
console.warn(`The mode ${name} was already implemented and was overwritten!`)
|
||||
Overlay.Modes[name] = geographicsFactory
|
||||
}
|
||||
|
||||
Overlay.requestFactory = function(name) {
|
||||
return Overlay.Modes[name]
|
||||
}
|
||||
|
||||
Overlay.listFactories = function() {
|
||||
let modes = []
|
||||
for (let key of Object.keys(Overlay.Modes)) {
|
||||
modes.push(key)
|
||||
}
|
||||
return modes
|
||||
}
|
43
lib/pixi/maps/projections/mercator.js
Normal file
@ -0,0 +1,43 @@
|
||||
import Projection from './projection.js'
|
||||
|
||||
/**
|
||||
* This is a projection file, that grants access to the
|
||||
* MERCATOR projection.
|
||||
*
|
||||
* Regulary only few Projections will be used in one
|
||||
* project, therefore only required one's should be
|
||||
* loaded.
|
||||
*/
|
||||
|
||||
export default class Mercator extends Projection {
|
||||
forward(coords) {
|
||||
let lat = coords.x
|
||||
let lng = coords.y
|
||||
|
||||
const PI_180 = Math.PI / 180.0
|
||||
const PI_4 = Math.PI * 4
|
||||
|
||||
const sinLatitude = Math.sin(lat * PI_180)
|
||||
let y = 0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / PI_4
|
||||
let x = (lng + 180) / 360
|
||||
|
||||
y = y < 0 ? 0 : y > 1 ? 1 : y
|
||||
|
||||
return new PIXI.Point(x, y)
|
||||
}
|
||||
|
||||
backward(point) {
|
||||
let lng = point.x * 360 - 180
|
||||
let lat = (Math.asin(-2 / (Math.exp(4 * Math.PI * (0.5 - point.y)) + 1) + 1) * 180) / Math.PI
|
||||
|
||||
return new PIXI.Point(lat, lng)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Mercator Projection'
|
||||
}
|
||||
|
||||
get maxViewport() {
|
||||
return { min: new PIXI.Point(-85, -180), max: new PIXI.Point(85, 180) }
|
||||
}
|
||||
}
|
43
lib/pixi/maps/projections/projection.js
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* A projection determines how a geographical card has to
|
||||
* be interpreted to map coordinate to pixels.
|
||||
*
|
||||
* Most used transformation is the mercator projection,
|
||||
* which projects a sphere on a cylinder.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
|
||||
export default class Projection {
|
||||
/**
|
||||
* Transforms a coordinate to a normalized position on the map.
|
||||
*
|
||||
* @param {*} coords
|
||||
* @memberof Projection
|
||||
*/
|
||||
forward(coords) {
|
||||
console.error('You must override the forward function in ' + this.name + '.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a normalized point on the map to a coordinate.
|
||||
*
|
||||
* @param {*} point
|
||||
* @memberof Projection
|
||||
*/
|
||||
backward(point) {
|
||||
console.error('You must override the backward fuction in ' + this.name + '.')
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Projection (abstract)'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
get maxViewport() {
|
||||
return { min: new PIXI.Point(-90, -180), max: new PIXI.Point(90, 180) }
|
||||
}
|
||||
}
|
242
lib/pixi/maps/projections/projections.html
Normal file
@ -0,0 +1,242 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Projections</title>
|
||||
<link rel="stylesheet" href="../../../3rdparty/highlight/styles/default.css" />
|
||||
<link rel="stylesheet" href="../../../../css/doctest.css" />
|
||||
|
||||
<script src="../../../../dist/iwmlib.3rdparty.js"></script>
|
||||
<script src="../../../../dist/iwmlib.js"></script>
|
||||
<script src="../../../../dist/iwmlib.pixi.js"></script>
|
||||
|
||||
<style>
|
||||
.inline-showcase {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.map-example {
|
||||
display: inline-block;
|
||||
width: 256px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
position: relative;
|
||||
/* inline-block create additional space around child elements. */
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.long {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: yellowgreen;
|
||||
}
|
||||
|
||||
.point {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background-color: red;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.small.point {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
border: 0.5px solid red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="Doctest.run()">
|
||||
<h1>Projections</h1>
|
||||
<p>
|
||||
Projections are used on the mapdata to translate coordinates to pixelpositions. There are various
|
||||
projections that can be used. All implemented ones are showcased here.
|
||||
</p>
|
||||
|
||||
<section id="mercator">
|
||||
<h2>Mercator Projection</h2>
|
||||
<p>The most commonly used projection is the mercator projection.</p>
|
||||
|
||||
<div id="mercatorMap" class="map-wrapper">
|
||||
<img src="../../../examples/osm/0/0/0.png" alt="" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let boundaries = [
|
||||
// { x: -90, y: -180 },
|
||||
// { x: 90, y: 180 },
|
||||
// { x: 0, y: 0 },
|
||||
// { x: 90, y: -180 },
|
||||
|
||||
// Eastern Boundaries
|
||||
// { x: -22, y: 180 },
|
||||
// { x: -43, y: 180 },
|
||||
// { x: -63, y: 180 },
|
||||
// { x: -73, y: 180 },
|
||||
// { x: -83, y: 180 },
|
||||
// { x: -87, y: 180 },
|
||||
|
||||
// { x: 0, y: 180 },
|
||||
// { x: 22, y: 180 },
|
||||
// { x: 43, y: 180 },
|
||||
// { x: 63, y: 180 },
|
||||
// { x: 73, y: 180 },
|
||||
// { x: 83, y: 180 },
|
||||
{ x: 87, y: 180 },
|
||||
// { x: -90, y: 180 },
|
||||
|
||||
// Western Boundaries
|
||||
{ x: -22, y: -180 },
|
||||
{ x: -43, y: -180 },
|
||||
{ x: -63, y: -180 },
|
||||
{ x: -73, y: -180 },
|
||||
{ x: -83, y: -180 },
|
||||
{ x: -87, y: -180 },
|
||||
|
||||
{ x: 0, y: -180 },
|
||||
{ x: 22, y: -180 },
|
||||
{ x: 43, y: -180 },
|
||||
{ x: 63, y: -180 },
|
||||
{ x: 73, y: -180 },
|
||||
{ x: 83, y: -180 },
|
||||
{ x: 87, y: -180 },
|
||||
{ x: -90, y: -180 }
|
||||
]
|
||||
|
||||
let capitals = {
|
||||
abidjan: { x: 5, y: -5 },
|
||||
canberra: { x: -35.312146, y: 149.121539 },
|
||||
berlin: { x: 52.52543, y: 13.385291 },
|
||||
capetown: { x: -33.925448, y: 18.416962 },
|
||||
moscow: { x: 55.750892, y: 37.622799 },
|
||||
washington: { x: 38.89565, y: -77.031407 },
|
||||
rio: { x: -22.8714, y: -43.28049 },
|
||||
tokio: { x: 35.696278, y: 139.731366 }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createPointAtPoisition(position, className = '', style = {}) {
|
||||
console.log(position)
|
||||
let point = document.createElement('div')
|
||||
point.className = 'point ' + className
|
||||
|
||||
Object.assign(point.style, {
|
||||
backgroundColor: "0xff0000"
|
||||
}, style, {
|
||||
left: position.x * 100 + '%',
|
||||
top: position.y * 100 + '%'
|
||||
})
|
||||
|
||||
return point
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a vertical line at the zero longitude of a map.
|
||||
*/
|
||||
function drawZeroLongitude(projection, parent) {
|
||||
let element = document.createElement("div")
|
||||
element.className = "long"
|
||||
let position = projection.forward({ x: 0, y: 0 })
|
||||
element.style.left = position.x * 100 + "%"
|
||||
|
||||
parent.appendChild(element)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Plots a point for a specific interval on the map. Distance defines that interval.
|
||||
*/
|
||||
function plotGridPoints(distance = 10, projection, parent) {
|
||||
for (let lat = -90; lat <= 90; lat += distance) {
|
||||
for (let lng = -180; lng <= 180; lng += distance) {
|
||||
let relativePosition = projection.forward({ x: lat, y: lng })
|
||||
let point = createPointAtPoisition(relativePosition, 'small', {
|
||||
backgroundColor: '#0000ff'
|
||||
})
|
||||
robinsonMap.appendChild(point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script class="doctest">
|
||||
|
||||
; (function () {
|
||||
let mercatorProjection = new Projection.Mercator()
|
||||
|
||||
for (let position of Object.values(capitals)) {
|
||||
let relativePosition = mercatorProjection.forward(position)
|
||||
let point = createPointAtPoisition(relativePosition)
|
||||
mercatorMap.appendChild(point)
|
||||
}
|
||||
|
||||
boundaries.forEach(coord => {
|
||||
let relativePosition = mercatorProjection.forward(coord)
|
||||
let point = createPointAtPoisition(relativePosition, 'small', {
|
||||
backgroundColor: '#0000ff'
|
||||
})
|
||||
mercatorMap.appendChild(point)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
</section>
|
||||
<section id="robinson">
|
||||
<h2>Robinson Projection</h2>
|
||||
<p>
|
||||
The robinson projection is a more 'artistic' approach, as it follows a table instead of a strict
|
||||
formula. It has severe distortions at the poles, but that distortion declines rapidly heading towards
|
||||
the equator.
|
||||
</p>
|
||||
|
||||
<div id="robinsonMap" class="map-wrapper">
|
||||
<img src="../../assets/maps/wikimedia-world-robinson/2000px-BlankMap-World.png" alt="" width="512" />
|
||||
</div>
|
||||
|
||||
<script class="doctest">
|
||||
; (function () {
|
||||
|
||||
// Create the robinson projection.
|
||||
let robinsonProjection = new Projection.Robinson(10)
|
||||
// Note: The center on this map is at approximately latitude 10,
|
||||
// therefore the projection must be shifted.
|
||||
|
||||
|
||||
/**
|
||||
* Create a point for each capital.
|
||||
*/
|
||||
for (let position of Object.values(capitals)) {
|
||||
let relativePosition = robinsonProjection.forward(position)
|
||||
let point = createPointAtPoisition(relativePosition)
|
||||
robinsonMap.appendChild(point)
|
||||
}
|
||||
|
||||
drawZeroLongitude(robinsonProjection, robinsonMap)
|
||||
|
||||
plotGridPoints(5, robinsonProjection, robinsonMap)
|
||||
|
||||
})()
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
148
lib/pixi/maps/projections/robinson.js
Normal file
@ -0,0 +1,148 @@
|
||||
import Projection from './projection'
|
||||
|
||||
/* */
|
||||
|
||||
export default class Robinson extends Projection {
|
||||
constructor(lng = 0) {
|
||||
super()
|
||||
|
||||
this.lng0 = lng
|
||||
this.lengthOfParallel = [
|
||||
1.0,
|
||||
0.9986,
|
||||
0.9954,
|
||||
0.99,
|
||||
0.9822,
|
||||
0.973,
|
||||
0.96,
|
||||
0.9427,
|
||||
0.9216,
|
||||
0.8962,
|
||||
0.8679,
|
||||
0.835,
|
||||
0.7986,
|
||||
0.7597,
|
||||
0.7186,
|
||||
0.6732,
|
||||
0.6213,
|
||||
0.5722,
|
||||
0.5322
|
||||
]
|
||||
this.distancesFromEquator = [
|
||||
0.0,
|
||||
0.062,
|
||||
0.124,
|
||||
0.186,
|
||||
0.248,
|
||||
0.31,
|
||||
0.372,
|
||||
0.434,
|
||||
0.4958,
|
||||
0.5571,
|
||||
0.6176,
|
||||
0.6769,
|
||||
0.7346,
|
||||
0.7903,
|
||||
0.8435,
|
||||
0.8936,
|
||||
0.9394,
|
||||
0.9761,
|
||||
1.0
|
||||
]
|
||||
}
|
||||
forward(coords) {
|
||||
let { x: lat, y: lng } = coords
|
||||
|
||||
lng = this._adjustLng(lng)
|
||||
|
||||
// Get the required indices, the remainder in between low and hight as ratio
|
||||
// and the sign of the found indices, as the tables are only in positive direction.
|
||||
let { low, high, ratio, sign } = this._getInterpolationValues(lat, 90)
|
||||
|
||||
// Values that lie inbetween two indices are interpolated.
|
||||
let y = this._interpolate(this.distancesFromEquator[low], this.distancesFromEquator[high], ratio)
|
||||
|
||||
// Reapply the sign to the vertical position.
|
||||
y *= sign
|
||||
|
||||
// The center of the projection is in the center of the map. Therefore we shift the
|
||||
// center to the top left corner.
|
||||
y = 1 - (y + 1) / 2
|
||||
|
||||
// The lengthOfParallel table provides us with the corresponding scaling factor
|
||||
// for a specific latitude. Inbetween values are interpolated as before.
|
||||
let proportionalLength = this._interpolate(this.lengthOfParallel[low], this.lengthOfParallel[high], ratio)
|
||||
|
||||
//To normalize the value to a range from -1 to 1.
|
||||
let x = (proportionalLength * lng) / 180
|
||||
|
||||
x = (x + 1) / 2
|
||||
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
backward(position) {
|
||||
let { x, y } = position
|
||||
|
||||
y = 1 - 2 * y
|
||||
let sign = Math.sign(y)
|
||||
y = Math.abs(y)
|
||||
|
||||
let low = 0
|
||||
let high = 0
|
||||
for (let i = 0; i < this.distancesFromEquator.length - 1 && y > this.distancesFromEquator[i]; i++) {
|
||||
low = i
|
||||
high = i + 1
|
||||
}
|
||||
|
||||
let lowDist = this.distancesFromEquator[low]
|
||||
let highDist = this.distancesFromEquator[high]
|
||||
|
||||
let ratio = highDist - lowDist == 0 ? 0 : (y - lowDist) / (highDist - lowDist)
|
||||
|
||||
let lat = low * 5 + ratio * 5
|
||||
|
||||
let parallelLengthMin = this.lengthOfParallel[low]
|
||||
let parallelLengthMax = this.lengthOfParallel[high]
|
||||
|
||||
let completeLength = parallelLengthMin + (parallelLengthMax - parallelLengthMin) * ratio
|
||||
|
||||
x = x * 2 - 1
|
||||
let normalizedLength = x / completeLength
|
||||
|
||||
let lng = normalizedLength * 180
|
||||
|
||||
return { x: lat * sign, y: this._adjustLng(lng, true) }
|
||||
}
|
||||
|
||||
_adjustLng(lng, inv = false) {
|
||||
let moved = inv ? lng + this.lng0 : lng - this.lng0
|
||||
if (moved < -180) moved += 360
|
||||
if (moved > 180) moved -= 360
|
||||
|
||||
return moved
|
||||
}
|
||||
|
||||
_interpolate(a, b, ratio) {
|
||||
return a * (1 - ratio) + b * ratio
|
||||
}
|
||||
_getInterpolationValues(value, max) {
|
||||
let sign = Math.sign(value)
|
||||
value = Math.min(Math.abs(value), max)
|
||||
// Note that min and max can be the same. Which is true
|
||||
// when lat is dividable by 5. This also covers the edge cases 0 and 90.
|
||||
let minIndex = Math.floor(value / 5)
|
||||
let maxIndex = Math.ceil(value / 5)
|
||||
let ratio = (value % 5) / 5
|
||||
// console.log({ value, minIndex, maxIndex, ratio })
|
||||
// console.log(this.lengthOfParallel.length)
|
||||
return { low: minIndex, high: maxIndex, ratio, sign }
|
||||
}
|
||||
|
||||
toString() {
|
||||
return
|
||||
}
|
||||
get name() {
|
||||
return 'Robinson Projection'
|
||||
}
|
||||
}
|
50
lib/pixi/maps/projections/robinsontest.html
Normal file
@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Document</title>
|
||||
|
||||
<link rel="stylesheet" href="../../../3rdparty/highlight/styles/default.css" />
|
||||
<link rel="stylesheet" href="../../../../css/doctest.css" />
|
||||
|
||||
<script src="../../../../dist/iwmlib.3rdparty.js"></script>
|
||||
<script src="../../../../dist/iwmlib.js"></script>
|
||||
<script src="../../../../dist/iwmlib.pixi.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="solution">
|
||||
|
||||
</section>
|
||||
<script>
|
||||
|
||||
function write(msg) {
|
||||
|
||||
if (typeof msg === "object") {
|
||||
msg = JSON.stringify(msg)
|
||||
}
|
||||
|
||||
let log = document.createElement("p")
|
||||
log.innerHTML = msg
|
||||
solution.appendChild(log)
|
||||
}
|
||||
|
||||
|
||||
let coords = { x: -30, y: -30 }
|
||||
write(coords)
|
||||
|
||||
let robinson = new Projection.Robinson()
|
||||
|
||||
let pixels = robinson.forward(coords)
|
||||
write(pixels)
|
||||
|
||||
coords = robinson.backwards(pixels)
|
||||
write(coords)
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
151
lib/pixi/maps/scatter.html
Normal file
@ -0,0 +1,151 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>Scatter</title>
|
||||
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../../../css/doctest.css'>
|
||||
|
||||
<script src='../../../dist/iwmlib.3rdparty.js'></script>
|
||||
<script src='../../../dist/iwmlib.pixi.js'></script>
|
||||
<script src='../../../dist/iwmlib.js'></script>
|
||||
|
||||
<style>
|
||||
.inline-showcase {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.map-example {
|
||||
display: inline-block;
|
||||
width: 256px;
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Scatter</h1>
|
||||
<p>The Scatter in Tüsch specifies some classes to make them more suitable for Map applications.</p>
|
||||
|
||||
<h2>CoverScatter</h2>
|
||||
<p>Usually maps should cover the whole app, or a window inside the app. Therefore they support a cover option. This option
|
||||
allows prevents the scatter to move inside the window.
|
||||
|
||||
|
||||
|
||||
</p>
|
||||
<p>
|
||||
<h3>Legend</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span style="color: deepskyblue">Bounds:</span> The bounds represent the bounding boxes of the elements.
|
||||
</li>
|
||||
<li>
|
||||
<span style="color:red;">Polygon:</span> The polygon property of scatters describe their actual extends and are used for e.g. calculating
|
||||
overlap with the scatter container.
|
||||
</li>
|
||||
<li>
|
||||
<span style="color:greenyellow;">Boundaries:</span> You can set boundaries in the options in the form of
|
||||
<code>opts = {..., boundaries: {min: {x:0,y:0},
|
||||
max{x:1,y:1},...}}</code> these will be applied relatively to the image dimensions.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<canvas id="map_01">
|
||||
|
||||
</canvas>
|
||||
<script>
|
||||
|
||||
let app = new PIXIApp({
|
||||
view: map_01,
|
||||
width: 512,
|
||||
height: 512
|
||||
})
|
||||
|
||||
app.sceneFactory = () => {
|
||||
window.scatterContainer = new AdvancedScatterContainer(app.renderer, { showBounds: true, showPolygon: true, app })
|
||||
return window.scatterContainer
|
||||
}
|
||||
|
||||
app.setup().run()
|
||||
|
||||
|
||||
/*
|
||||
Create Rotated Container
|
||||
*/
|
||||
|
||||
let pad = 128
|
||||
let frame = new RigidContainer(app.width - 2 * pad, app.height - 2 * pad)
|
||||
frame.position.set(pad, pad)
|
||||
|
||||
|
||||
//Create Containers Scatter
|
||||
new CompactScatter(frame, app.renderer, { translatable: false, scalable: false, rotatable: false})
|
||||
app.scene.addChild(frame)
|
||||
|
||||
let oldCenter = new PIXI.Point()
|
||||
oldCenter.copy(frame.scatter.center)
|
||||
|
||||
frame.scatter.rotation = Math.PI / 4
|
||||
let offset = Points.subtract(frame.scatter.center, oldCenter)
|
||||
frame.scatter.position = Points.subtract(frame.scatter.position, offset)
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Create Regular Rect
|
||||
|
||||
*/
|
||||
|
||||
let map = new PIXI.Graphics()
|
||||
map.beginFill(0x00FFFF, 0.2)
|
||||
map.drawRect(0, 0, 100, 100)
|
||||
map.position.set(-10, -10)
|
||||
map.interactive = true
|
||||
new CoverScatter(map, app.renderer, { maxScale: Infinity,
|
||||
autoBringToFront:false,
|
||||
boundaries: {
|
||||
min: { x: 0.2, y: 0.2 },
|
||||
max: { x: 0.8, y: 0.8 }
|
||||
},
|
||||
alpha: 0.5,
|
||||
cover: true,
|
||||
debug:true
|
||||
})
|
||||
frame.addChild(map)
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Create Rotated Rect
|
||||
|
||||
*/
|
||||
|
||||
let advancedScatter = new PIXI.Graphics()
|
||||
advancedScatter.interactive = true
|
||||
new CoverScatter(advancedScatter, app.renderer, {
|
||||
maxScale: Infinity,
|
||||
autoBringToFront: false,
|
||||
alpha: 0.3, cover:false,
|
||||
debug:true
|
||||
})
|
||||
advancedScatter.beginFill(0x000000, 0.2)
|
||||
advancedScatter.drawRect(0, 0, 200, 200)
|
||||
app.scene.addChild(advancedScatter)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<h2>Note</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Cover behaviour is not yet working on rotated elements.
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
|
||||
</html>
|
558
lib/pixi/maps/scatter.js
Normal file
@ -0,0 +1,558 @@
|
||||
/* ES Lint */
|
||||
/* globals PIXI, requestAnimationFrame, performance, app*/
|
||||
|
||||
import { DisplayObjectScatter, ScatterContainer } from '../scatter.js'
|
||||
import { Points } from '../../utils.js'
|
||||
import { EventHandler } from './utils.js'
|
||||
|
||||
/**
|
||||
* The AdvancedScatterContainer extends the ScatterContainer, but
|
||||
* uses the findTargetNew instead of the findTarget method, which takes interactive elements
|
||||
* into account, when calculating the target.
|
||||
*/
|
||||
export class AdvancedScatterContainer extends ScatterContainer {
|
||||
applyToChildScattersRecursively(parent, root, applyFunc) {
|
||||
for (let child of parent.children) {
|
||||
if (child.scatter) {
|
||||
applyFunc(child, root)
|
||||
this.applyToChildScattersRecursively(child, root, applyFunc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The update method was adjusted to draw bounds and polygpns
|
||||
*/
|
||||
update(dt) {
|
||||
this.clear()
|
||||
this.lineStyle(1, 0x0000ff)
|
||||
if (this.showBounds) {
|
||||
this.applyToChildScattersRecursively(this, this, (child, root) => {
|
||||
let position = child.worldTransform.apply(new PIXI.Point(0, 0))
|
||||
|
||||
this.drawShape(child.scatter.bounds)
|
||||
|
||||
let center = {
|
||||
x: child.scatter.width / 2 / child.scatter.scale,
|
||||
y: child.scatter.height / 2 / child.scatter.scale
|
||||
}
|
||||
center = child.worldTransform.apply(center)
|
||||
|
||||
this.drawCircle(center.x, center.y, 4)
|
||||
this.drawCircle(position.x, position.y, 4)
|
||||
})
|
||||
|
||||
this.lineStyle(2, 0x0000ff)
|
||||
this.drawShape(this.bounds)
|
||||
}
|
||||
if (this.showPolygon) {
|
||||
this.lineStyle(2, 0xff0000)
|
||||
for (let child of this.children) {
|
||||
if (child.scatter) {
|
||||
let polygon = child.scatter.polygon
|
||||
let shape = new PIXI.Polygon(polygon.flatAbsolutePoints())
|
||||
if (
|
||||
shape.points[0] !== shape.points[shape.points.length - 2] ||
|
||||
shape.points[1] !== shape.points[shape.points.length - 1]
|
||||
) {
|
||||
shape.points.push(shape.points[0], shape.points[1])
|
||||
}
|
||||
this.drawShape(shape)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.showTouches) {
|
||||
let current = this.delegate.interaction.current
|
||||
for (let [key, point] of current.entries()) {
|
||||
let local = this.mapPositionToPoint(point)
|
||||
this.drawCircle(local.x, local.y, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.bounds.width
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.bounds.width
|
||||
}
|
||||
|
||||
findTarget(event, local, global) {
|
||||
// UO: still problematic. Does not find non interactive elements
|
||||
// which are needed for some stylus applications
|
||||
if (event.claimedByScatter) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.hitScatter = null
|
||||
let interactionManager = this.renderer.plugins.interaction
|
||||
|
||||
let displayObject = interactionManager.hitTest(local, this)
|
||||
console.log(displayObject.dontBlockScatter, displayObject.parent)
|
||||
if (displayObject.dontBlockScatter && displayObject.parent != null) {
|
||||
displayObject = interactionManager.hitTest(local, displayObject.parent)
|
||||
}
|
||||
|
||||
if (displayObject != null && displayObject.scatter != null) this.hitScatter = displayObject.scatter
|
||||
if (this.claimEvents) event.claimedByScatter = this.hitScatter
|
||||
|
||||
console.log(displayObject)
|
||||
return this.hitScatter
|
||||
}
|
||||
}
|
||||
|
||||
export class RigidScatterContainer extends AdvancedScatterContainer {
|
||||
constructor(width, height, renderer, opts) {
|
||||
super(renderer, opts)
|
||||
this._width = width
|
||||
this._height = height
|
||||
}
|
||||
|
||||
resize(width, height) {
|
||||
this._width = width
|
||||
this._height = height
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this._width
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this._height
|
||||
}
|
||||
|
||||
_calculateBounds() {
|
||||
this._bounds.clear()
|
||||
let bounds = new PIXI.Bounds()
|
||||
bounds.minX = this.parent.position.x
|
||||
bounds.minY = this.parent.position.y
|
||||
bounds.maxX = this.parent.position.x + this.width
|
||||
bounds.maxY = this.parent.position.y + this.height
|
||||
this._bounds.addBounds(bounds)
|
||||
}
|
||||
}
|
||||
|
||||
export class RigidContainer extends PIXI.Graphics {
|
||||
constructor(width, height, { onResize = null } = {}) {
|
||||
super()
|
||||
if (!width || !height) {
|
||||
console.log(
|
||||
`Width (${width}) or height (${height}) not set! If this is intended use another PIXI.DisplayObject instead.`,
|
||||
this
|
||||
)
|
||||
}
|
||||
this._width = width
|
||||
this._height = height
|
||||
this.beginFill(0xffffff, 0.3)
|
||||
this.drawRect(0, 0, this.width, this.height)
|
||||
|
||||
this.onResize = new EventHandler('resize', {
|
||||
listeners: onResize ? [onResize] : []
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the scattercontainer to the provided dimensions.
|
||||
*
|
||||
* @param {number} width - Target width of the resize.
|
||||
* @param {number} height - Target height of the resize.
|
||||
* @memberof RigidContainer
|
||||
*/
|
||||
resize(width, height) {
|
||||
this._width = width
|
||||
this._height = height
|
||||
this._updateHitArea()
|
||||
this.onResize.call(this, 'resize', { x: width, y: height })
|
||||
}
|
||||
|
||||
_updateHitArea() {
|
||||
// this.hitArea = new PIXI.Rectangle(0,0, this.width, this.height)
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this._width
|
||||
}
|
||||
get height() {
|
||||
return this._height
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialization of the DisplayObjectScatter with special behavior needed for maps.
|
||||
*/
|
||||
class AdvancedScatter extends DisplayObjectScatter {
|
||||
/**
|
||||
* Animates the throw and ensures that the map is always visible. This
|
||||
* is different from the standard behavior.
|
||||
*
|
||||
* @param {*} time
|
||||
* @memberof AdvancedScatter
|
||||
*/
|
||||
animateThrow(time) {
|
||||
// In rare cases animateThrow is called when the displayObject is already removed
|
||||
if (this.displayObject.parent == null) {
|
||||
return
|
||||
}
|
||||
|
||||
//Moved this outside, as the time continously increases,
|
||||
//when the object is not thrown.
|
||||
|
||||
if (this.velocity != null) {
|
||||
let dt = this._throwDeltaTime()
|
||||
// This seems to just correct the velocity when its calculated wrong.
|
||||
// In what case can it get bigger? Velocity tends to always shrink, when no force is added.
|
||||
let d = this._getThrowDelta(dt)
|
||||
this._move(d)
|
||||
this.onDragUpdate(d)
|
||||
if (dt == 0 || this.needsAnimation()) {
|
||||
requestAnimationFrame(this.animateThrow.bind(this))
|
||||
return
|
||||
} else {
|
||||
if (this.isOutside()) {
|
||||
requestAnimationFrame(this.animateThrow.bind(this))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
this.onDragComplete()
|
||||
}
|
||||
|
||||
get parent() {
|
||||
// Returns the parent is a scatter get the parent scatter.
|
||||
// If it's a scatter container, get the displayObject.
|
||||
// Other cases are not allowed.
|
||||
return this.displayObject.parent != null && this.displayObject.parent.scatter != null
|
||||
? this.displayObject.parent.scatter
|
||||
: this.displayObject.parent
|
||||
}
|
||||
|
||||
_getThrowDelta(dt) {
|
||||
this.velocity = this.nextVelocity(this.velocity)
|
||||
if (this.velocity != null) {
|
||||
return Points.multiplyScalar(this.velocity, dt)
|
||||
}
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) {
|
||||
this.translatable = translatable
|
||||
this.scalable = scalable
|
||||
this.rotatable = rotatable
|
||||
this.movableX = movableX
|
||||
this.movableY = movableY
|
||||
}
|
||||
|
||||
unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) {
|
||||
this.freeze({
|
||||
translatable,
|
||||
scalable,
|
||||
rotatable: false,
|
||||
movableX,
|
||||
movableY
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class SubmapScatter extends DisplayObjectScatter {
|
||||
get width() {
|
||||
return this.displayObject.width * this.displayObject.scale.x
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.displayObject.height * this.displayObject.scale.y
|
||||
}
|
||||
|
||||
freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) {
|
||||
this.translatable = translatable
|
||||
this.scalable = scalable
|
||||
this.rotatable = rotatable
|
||||
this.movableX = movableX
|
||||
this.movableY = movableY
|
||||
}
|
||||
|
||||
unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) {
|
||||
this.freeze({
|
||||
translatable,
|
||||
scalable,
|
||||
rotatable: false,
|
||||
movableX,
|
||||
movableY
|
||||
})
|
||||
}
|
||||
|
||||
onZoomed(about) {
|
||||
super.onZoomed(about)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* A scatter which width does not increase when child elements are added.
|
||||
*/
|
||||
export class CompactScatter extends AdvancedScatter {}
|
||||
|
||||
/**
|
||||
* A problem with PIXI Objects is, that the bounding box changes
|
||||
* with child elements. Therefore the sized scatter has a rigid size, that is not
|
||||
* changed by elements, but only with their width and height property.
|
||||
*/
|
||||
export class CoverScatter extends AdvancedScatter {
|
||||
constructor(a, b, opts) {
|
||||
super(a, b, opts)
|
||||
|
||||
opts = Object.assign(
|
||||
{
|
||||
debug: false,
|
||||
cover: true,
|
||||
boundaries: {
|
||||
min: { x: 0, y: 0 },
|
||||
max: { x: 1, y: 1 }
|
||||
}
|
||||
},
|
||||
opts
|
||||
)
|
||||
|
||||
this.debug = opts.debug
|
||||
this.cover = opts.cover
|
||||
this._boundaries = opts.boundaries
|
||||
|
||||
if (this.debug) {
|
||||
this.debugGraphics = new PIXI.Graphics()
|
||||
this.displayObject.addChild(this.debugGraphics)
|
||||
this.debugGraphics.lineStyle(0.2, 0x00ff00)
|
||||
this.debugGraphics.drawRect(
|
||||
this.width * this.boundaries.min.x,
|
||||
this.height * this.boundaries.min.y,
|
||||
this.width * (this.boundaries.max.x - this.boundaries.min.x),
|
||||
this.height * (this.boundaries.max.y - this.boundaries.min.y)
|
||||
)
|
||||
|
||||
this.debugGraphics.endFill()
|
||||
}
|
||||
|
||||
if (this.cover) {
|
||||
// The reference to the element handler needs to be stored,
|
||||
// that we can remove it later on.
|
||||
const eventHandler = this._applyInitialCover.bind(this, null, true)
|
||||
this._applyInitialCover(eventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
_applyInitialCover(eventHandler, removeListener = false) {
|
||||
if (this.debug) console.log('ApplyInitialCover: ', parent)
|
||||
if (removeListener) {
|
||||
this.displayObject.off('added', eventHandler)
|
||||
}
|
||||
|
||||
if (this.displayObject.parent)
|
||||
this.forceCover(this.displayObject.parent.width, this.displayObject.parent.height)
|
||||
else {
|
||||
this.displayObject.on('added', eventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
get boundaries() {
|
||||
if (this._boundaries) return this._boundaries
|
||||
else
|
||||
return {
|
||||
min: { x: 0, y: 0 },
|
||||
max: { x: 1, y: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
transform(translate, zoom, rotate, anchor) {
|
||||
if (this.cover) {
|
||||
if (!this.parent) {
|
||||
return new PIXI.Rectangle(0, 0, window.innerWidth, window.innerHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the scaling violates the minCoverScale, the zoom is adjusted to perfectly match
|
||||
* the minCoverScale.
|
||||
*/
|
||||
let minCoverScale = this.calculateMinCoverScale(this.parent.width, this.parent.height)
|
||||
|
||||
let { zoom: fixedZoom, scale } = this.calculateScale(zoom)
|
||||
if (scale < minCoverScale) {
|
||||
zoom = minCoverScale / this.scale
|
||||
}
|
||||
}
|
||||
|
||||
super.transform(translate, zoom, rotate, anchor)
|
||||
if (this.cover) {
|
||||
let postTranslate = this.outsideBoundaries()
|
||||
super.transform(postTranslate, 1, 0, { x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
outsideBoundaries(movement = { x: 0, y: 0 }, { boundaries = this.boundaries } = {}) {
|
||||
if (this.parent == null) {
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
let left = -(this.displayObject.position.x + movement.x + this.width * boundaries.min.x)
|
||||
let right = -left + this.width * (boundaries.max.x - boundaries.min.x) - this.parent.width
|
||||
|
||||
let top = -(this.displayObject.position.y + movement.y + this.height * boundaries.min.y)
|
||||
let bot = -top + this.height * (boundaries.max.y - boundaries.min.y) - this.parent.height
|
||||
|
||||
// Helper function to limit an Axis inside a container, or 'glue' it
|
||||
// to the lower side, if the size is smaller than the size of the
|
||||
// container.
|
||||
function limitAxis(low, high, contentSize, containerSize) {
|
||||
let val = 0
|
||||
if (low < 0) {
|
||||
val = low
|
||||
} else if (high < 0) {
|
||||
if (contentSize > containerSize) {
|
||||
// ... and the element is bigger as the container
|
||||
// - reset it to the containersize.
|
||||
val = -high
|
||||
} else {
|
||||
// ... and the size is not high enough, then
|
||||
// 'glue' it to the low axis.
|
||||
val = low
|
||||
}
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// Use the helper function to correct the movement to cover x and y.
|
||||
let correctionX = limitAxis(left, right, this.width * (boundaries.max.x - boundaries.min.x), this.parent.width)
|
||||
let correctionY = limitAxis(top, bot, this.height * (boundaries.max.y - boundaries.min.y), this.parent.height)
|
||||
let fixedMovement = {
|
||||
x: correctionX == 0 ? movement.x : correctionX + movement.x,
|
||||
y: correctionY == 0 ? movement.y : correctionY + movement.y
|
||||
}
|
||||
|
||||
return fixedMovement
|
||||
}
|
||||
|
||||
calculateMinCoverScale(width, height) {
|
||||
let scale = 0
|
||||
|
||||
if (!(this.width == 0 && this.height == 0)) {
|
||||
let actualWidth = this.width / this.scale
|
||||
let actualHeight = this.height / this.scale
|
||||
|
||||
let boundaryWidth = (this.boundaries.max.x - this.boundaries.min.x) * actualWidth
|
||||
let boundaryHeight = (this.boundaries.max.y - this.boundaries.min.y) * actualHeight
|
||||
|
||||
let coverWidth = width / boundaryWidth
|
||||
let coverHeight = height / boundaryHeight
|
||||
|
||||
scale = Math.max(coverWidth, coverHeight)
|
||||
}
|
||||
|
||||
return scale
|
||||
}
|
||||
|
||||
_getThrowDelta(dt) {
|
||||
let delta = super._getThrowDelta(dt)
|
||||
if (this.cover) {
|
||||
delta = this.outsideBoundaries(delta, {
|
||||
boundaries: this.boundaries
|
||||
})
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
containerChanged(width, height) {
|
||||
if (this.cover) this.forceCover(width, height)
|
||||
}
|
||||
|
||||
forceCover() {
|
||||
this.requestScale(this.scale)
|
||||
let translate = this.outsideBoundaries()
|
||||
this.transform(translate, 1, 0, { x: 0, y: 0 })
|
||||
}
|
||||
|
||||
requestScale(scale) {
|
||||
if (this.scalable && this.parent != null) {
|
||||
if (this.cover) {
|
||||
let minCoverScale = this.calculateMinCoverScale(this.parent.width, this.parent.height)
|
||||
scale = scale < minCoverScale ? minCoverScale : scale
|
||||
}
|
||||
this.scale = scale
|
||||
}
|
||||
}
|
||||
|
||||
requestFocus(point) {
|
||||
let allowedMovement = this.outsideBoundaries(Points.subtract(point, this.position))
|
||||
this.transform(allowedMovement, 1, 0, { x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The MapObjectScatter somewhat inverts the behaviour of
|
||||
* regular throwables. Regular throwables should bounce of the wall, when they
|
||||
* overlap to a certain amount. Maps on the otherhand are intended to overlap with the
|
||||
* scatter container heavily to hide irrelevant parts of the map without revealing
|
||||
* any background.
|
||||
*
|
||||
* @class
|
||||
* @extends DisplayObjectScatter
|
||||
*/
|
||||
|
||||
export class MapObjectScatter extends CoverScatter {
|
||||
/**
|
||||
* x=null, y=null,
|
||||
minScale=0.1,
|
||||
maxScale=1.0,
|
||||
startScale=1.0,
|
||||
autoBringToFront=true,
|
||||
translatable=true, scalable=true, rotatable=true, resizable=false,
|
||||
movableX=true,
|
||||
movableY=true,
|
||||
throwVisibility=44,
|
||||
throwDamping = 0.95,
|
||||
autoThrow=true,
|
||||
rotationDegrees=null,
|
||||
rotation=null,
|
||||
onTransform = null }
|
||||
*/
|
||||
constructor(displayObject, renderer, opts = {}) {
|
||||
opts = Object.assign(
|
||||
{
|
||||
debug: false,
|
||||
cover: false,
|
||||
startScale: 1,
|
||||
boundaries: {
|
||||
min: { x: 0, y: 0 },
|
||||
max: { x: 1, y: 1 }
|
||||
},
|
||||
autoBringToFront: false
|
||||
},
|
||||
opts
|
||||
)
|
||||
|
||||
if (!renderer) {
|
||||
console.error('Renderer was not set!')
|
||||
return
|
||||
}
|
||||
|
||||
super(displayObject, renderer, opts)
|
||||
this.cover = opts.cover
|
||||
}
|
||||
|
||||
moveTo() {
|
||||
super.moveTo(...arguments)
|
||||
}
|
||||
|
||||
lock() {
|
||||
this.rotatable = false
|
||||
this.moveable = false
|
||||
this.scaleable = false
|
||||
}
|
||||
|
||||
unlock() {
|
||||
this.rotatable = true
|
||||
this.moveable = true
|
||||
this.scaleable = true
|
||||
}
|
||||
|
||||
getWorldScatter() {
|
||||
return this.parent
|
||||
}
|
||||
}
|
134
lib/pixi/maps/utils.html
Normal file
@ -0,0 +1,134 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>Utils</title>
|
||||
<link rel='stylesheet' href='../iwmlib/lib/3rdparty/highlight/styles/default.css'>
|
||||
<link rel='stylesheet' href='../iwmlib/lib/../css/doctest.css'>
|
||||
<script src='../iwmlib/lib/3rdparty/highlight/highlight.pack.js'></script>
|
||||
<script src='../iwmlib/lib/3rdparty/all.js'></script>
|
||||
<script src='../iwmlib/lib/all.js'></script>
|
||||
<script src='../iwmlib/lib/pixi/all.js'></script>
|
||||
<script src="all.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="Doctest.run()">
|
||||
<h1>Utils</h1>
|
||||
<p>Some utility functionalities for the Tuesch.</p>
|
||||
|
||||
<h2>Event Handler</h2>
|
||||
<p>
|
||||
The event handler class generalizes a common design principle in javascript. When an event occurs, that is of relevance for
|
||||
other objects, then this event should be sent out to all objects, that are interested in that event. Objects interested
|
||||
in that event, subscribe the event handler and get notified via a callback function.
|
||||
</p>
|
||||
<script class="doctest">
|
||||
let result = []
|
||||
function a() { result.push("a") }
|
||||
function b() { result.push("b") }
|
||||
function c() { result.push("c") }
|
||||
function d() { result.push("d") }
|
||||
function e() { result.push("e") }
|
||||
function f() { result.push("f") }
|
||||
|
||||
//Init and call.
|
||||
let eventHandler = new EventHandler("manual", { listeners: [a, b] })
|
||||
eventHandler.call()
|
||||
Doctest.expect(result.join(","), ["a", "b"].join(","))
|
||||
result = []
|
||||
|
||||
// Add single
|
||||
eventHandler.add(c)
|
||||
eventHandler.call()
|
||||
Doctest.expect(result.join(","), ["a", "b", "c"].join(","))
|
||||
result = []
|
||||
|
||||
// Add multiple
|
||||
eventHandler.addMultiple(d, e, f)
|
||||
eventHandler.call()
|
||||
Doctest.expect(result.join(","), ["a", "b", "c", "d", "e", "f"].join(","))
|
||||
result = []
|
||||
|
||||
//Remove inbetween
|
||||
eventHandler.remove(c)
|
||||
eventHandler.call()
|
||||
Doctest.expect(result.join(","), ["a", "b", "d", "e", "f"].join(","))
|
||||
result = []
|
||||
|
||||
// Remove first
|
||||
eventHandler.remove(a)
|
||||
eventHandler.call()
|
||||
Doctest.expect(result.join(","), ["b", "d", "e", "f"].join(","))
|
||||
result = []
|
||||
|
||||
// Remove all remaining elements.
|
||||
eventHandler.empty()
|
||||
eventHandler.call()
|
||||
Doctest.expect(result.join(","), [].join(","))
|
||||
result = []
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<h2>DomUtils</h2>
|
||||
|
||||
Utility functions that help handling the DOM.
|
||||
|
||||
<h3>positionOnElement(element, position)</h3>
|
||||
Function that returns the global position for a normalized position.
|
||||
|
||||
<div id="positionOnElementBox" class="box" style="width: 512px;height:256px;border: 1px solid black; box-sizing: border-box; transform: rotate(30deg); margin:100px;"></div>
|
||||
|
||||
<script class="doctest">
|
||||
|
||||
|
||||
let target = document.getElementById("positionOnElementBox")
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
|
||||
let positions = [
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[0.5,0.5],
|
||||
[0.2, 0.2],
|
||||
[0.2, 0.8],
|
||||
[0.8, 0.2],
|
||||
[0.8, 0.8],
|
||||
]
|
||||
|
||||
positions.forEach(position => {
|
||||
position = { x: position[0], y: position[1] }
|
||||
let transformedPosition = DomUtils.positionOnElement(target, position)
|
||||
let dot = document.createElement("div");
|
||||
const size = 10;
|
||||
|
||||
|
||||
Object.assign(dot.style, {
|
||||
width: size + "px",
|
||||
height: size + "px",
|
||||
top: target.offsetTop + target.offsetHeight / 2 + transformedPosition.y -size/2+ "px",
|
||||
left: target.offsetLeft + target.offsetWidth / 2 + transformedPosition.x -size/2 + "px",
|
||||
position: "absolute",
|
||||
backgroundColor: "green",
|
||||
borderRadius: "50%"
|
||||
})
|
||||
|
||||
|
||||
document.body.appendChild(dot)
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
482
lib/pixi/maps/utils.js
Normal file
@ -0,0 +1,482 @@
|
||||
import { Points } from '../../utils.js'
|
||||
|
||||
export class ObjectUtils {
|
||||
/**
|
||||
* Finds the previous key in an object.
|
||||
*
|
||||
* @param {string} targetKey - The target key, of whom the predecessor is wanted.
|
||||
*/
|
||||
static findPredecessor(targetKey, obj) {
|
||||
// Initialize pointers ...
|
||||
let prev = null
|
||||
let first = null
|
||||
// ... and a hit flag.
|
||||
let hit = false
|
||||
|
||||
//Iterate over all available items.
|
||||
for (let key in obj)
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (!hit) {
|
||||
//Assign both values,
|
||||
//while not hit
|
||||
prev = first
|
||||
first = key
|
||||
|
||||
//When hit, set flag.
|
||||
if (key == targetKey) {
|
||||
hit = true
|
||||
if (prev) return prev //If it was not hit on the first item, we can return prev.
|
||||
}
|
||||
} else {
|
||||
//Otherwise, when first item was hit, we iterate over the entire object.
|
||||
first = key
|
||||
}
|
||||
}
|
||||
|
||||
return first
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the successing key in an object.
|
||||
*
|
||||
* @param {string} targetKey - The key of whom the successor key should be found.
|
||||
* @param {object} obj - The object, that is scanned for the successor.
|
||||
*/
|
||||
static findSuccessor(targetKey, obj) {
|
||||
let first = null
|
||||
let next = false
|
||||
|
||||
for (let key in obj)
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (!first) first = key
|
||||
|
||||
if (next) {
|
||||
return key
|
||||
}
|
||||
|
||||
if (targetKey == key) {
|
||||
next = true
|
||||
}
|
||||
}
|
||||
|
||||
return first
|
||||
}
|
||||
|
||||
static fromPath(obj, path, separator = '.') {
|
||||
let arr = path.split(separator)
|
||||
let result = obj
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (result[arr[i]] !== null) {
|
||||
result = result[arr[i]]
|
||||
} else return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export class PathUtils {
|
||||
static fixTrailingSlash(url) {
|
||||
return url.replace(/\/?$/, '/')
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeUtils {
|
||||
static minutesToMs(minutes) {
|
||||
return TimeUtils.minutesToSeconds(minutes) * 1000
|
||||
}
|
||||
|
||||
static minutesToSeconds(minutes) {
|
||||
return minutes * 60
|
||||
}
|
||||
}
|
||||
|
||||
export class StringUtils {
|
||||
/* Used from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript */
|
||||
static capitalize(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
export class GeometryUtils {
|
||||
static RandomRange(min, max) {
|
||||
return GeometryUtils.toRadians(Math.random() * (max - min) + min)
|
||||
}
|
||||
|
||||
static RandomSign() {
|
||||
return Math.sign(Math.random() - 0.5)
|
||||
}
|
||||
|
||||
static toDegrees(radians) {
|
||||
return (radians * 180) / Math.PI
|
||||
}
|
||||
|
||||
static toRadians(degrees) {
|
||||
return (degrees / 180) * Math.PI
|
||||
}
|
||||
|
||||
/*
|
||||
Recursion is buggy.
|
||||
*/
|
||||
static subdivide(polygon, factor = 0.5, n = 0) {
|
||||
console.log(polygon.length, factor)
|
||||
let points = []
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
let prev = i - 1 < 0 ? polygon[polygon.length - 1] : polygon[i - 1]
|
||||
let next = i + 1 < polygon.length ? polygon[i + 1] : polygon[0]
|
||||
let current = polygon[i]
|
||||
points.push(GeometryUtils.linearInterpolation(prev, current, factor))
|
||||
points.push(GeometryUtils.linearInterpolation(current, next, 1 - factor))
|
||||
}
|
||||
if (n > 0) {
|
||||
n--
|
||||
points = GeometryUtils.subdivide(points, n)
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
static linearInterpolation(first, second, n) {
|
||||
let point = new PIXI.Point()
|
||||
|
||||
point.x = first.x * n + second.x * (1 - n)
|
||||
point.y = first.y * n + second.y * (1 - n)
|
||||
return point
|
||||
}
|
||||
}
|
||||
|
||||
export class Matrix {
|
||||
constructor(a, b, d, e, c = 0, f = 0) {
|
||||
Object.assign(this, {
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
g: 0,
|
||||
h: 0,
|
||||
i: 1
|
||||
})
|
||||
}
|
||||
|
||||
static Transpose(m) {
|
||||
let first = ['b', 'c', 'f']
|
||||
let second = ['d', 'g', 'h']
|
||||
|
||||
for (let i = 0; i < first.length; i++) {
|
||||
let tmp = m[first[i]]
|
||||
first[i] = second[i]
|
||||
second[i] = tmp
|
||||
}
|
||||
}
|
||||
|
||||
static Rotation(deg) {
|
||||
let rad = (2 * Math.PI * deg) / 180
|
||||
return new Matrix(Math.cos(rad), -Math.sin(rad), Math.sin(rad), Math.cos(rad))
|
||||
}
|
||||
|
||||
static Transform(x, y) {
|
||||
return new Matrix(1, 0, 0, 1, x, y)
|
||||
}
|
||||
|
||||
static MultiplyPoint(m, p) {
|
||||
x = p.x * m.a + m.b * p.y + m.c * 1
|
||||
y = p.x * m.d + p.y * m.e + m.f * 1
|
||||
|
||||
return {
|
||||
x,
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DomUtils {
|
||||
static getCenter(element) {
|
||||
let x = (element.clientLeft + element.clientWidth) / 2
|
||||
let y = (element.clientTop + element.clientHeight) / 2
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
static getTransformedPosition(element) {
|
||||
let mat = this.getCSSMatrix(element)
|
||||
return { x: mat[4], y: mat[5] }
|
||||
}
|
||||
|
||||
static getCSSMatrix(element) {
|
||||
let matrix = window.getComputedStyle(element).transform
|
||||
|
||||
if (matrix == 'none') {
|
||||
return [1, 0, 0, 1, 0, 0]
|
||||
} else {
|
||||
const pre = 'matrix('
|
||||
let values = matrix.substring(pre.length, matrix.length - 1).split(',')
|
||||
for (let i in values) {
|
||||
values[i] = parseFloat(values[i])
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
static positionOnElement(element, position) {
|
||||
const matrix = this.getCSSMatrix(element)
|
||||
|
||||
const inX = element.offsetWidth * (position.x - 0.5)
|
||||
const inY = element.offsetHeight * (position.y - 0.5)
|
||||
|
||||
let outX = matrix[0] * inX + matrix[2] * inY + matrix[4]
|
||||
let outY = matrix[1] * inX + matrix[3] * inY + matrix[5]
|
||||
|
||||
let out = {
|
||||
x: outX,
|
||||
y: outY
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
static applyTransform(target, transformedPosition, size = 0) {
|
||||
return {
|
||||
x: target.offsetLeft + target.offsetWidth / 2 + transformedPosition.x - size / 2,
|
||||
y: target.offsetTop + target.offsetHeight / 2 + transformedPosition.y - size / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Vector {
|
||||
static length(vector) {
|
||||
return Math.sqrt(vector.x * vector.x + vector.y * vector.y)
|
||||
}
|
||||
|
||||
static normalize(vector) {
|
||||
return Points.multiplyScalar(vector, 1 / this.length(vector))
|
||||
}
|
||||
|
||||
static scaleTo(vector, length) {
|
||||
let normalized = this.normalize(vector)
|
||||
return Points.multiplyScalar(normalized, length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The EventHandler class is used to take care of a event based design
|
||||
* pattern. Callbacks can subscribe to an event and these unknown sources
|
||||
* get notified whenever the event changes.
|
||||
*
|
||||
* @export
|
||||
* @class EventHandler
|
||||
*/
|
||||
export class EventHandler {
|
||||
/**
|
||||
* Creates an instance of EventHandler.
|
||||
* @param {any} name
|
||||
* @param {any} [{
|
||||
* listeners = [] - With the listnerers parameter the user can specify a function, array of functions or null (no function - useful when used in constructor with optional parameter).
|
||||
* }={}]
|
||||
* @memberof EventHandler
|
||||
*/
|
||||
constructor(name, { listeners = [] } = {}) {
|
||||
this.name = name
|
||||
this.listeners = []
|
||||
this.onces = []
|
||||
|
||||
/**
|
||||
* One may initialize the eventListener using a parameter
|
||||
* that is either passed or null.
|
||||
*/
|
||||
|
||||
if (listeners == null) {
|
||||
// Null is a valid value as the EventHandler assumes no listener is passed on purpose.
|
||||
// This is useful, when a default parameter is passed as null.
|
||||
} else if (Array.isArray(listeners)) this.listeners = listeners
|
||||
else if (typeof listeners == 'function') {
|
||||
this.listeners = []
|
||||
this.add(listeners)
|
||||
} else {
|
||||
console.warn(
|
||||
"The provided 'listeners' is neither an Array of functions, nor a function. No eventcallback was added!",
|
||||
listeners,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
addMultiple(...callbacks) {
|
||||
for (let callback of callbacks) {
|
||||
this.listeners.push(callback)
|
||||
}
|
||||
}
|
||||
|
||||
add(callback) {
|
||||
this.listeners.push(callback)
|
||||
return callback
|
||||
}
|
||||
|
||||
once(callback) {
|
||||
this.onces.push(callback)
|
||||
}
|
||||
|
||||
remove(func) {
|
||||
for (const [idx, listener] of this.listeners.entries()) {
|
||||
if (listener === func) {
|
||||
this.listeners.splice(idx, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
empty() {
|
||||
this.listeners = []
|
||||
}
|
||||
|
||||
call(context, ...args) {
|
||||
this.listeners.forEach(listener => listener.call(context, ...args))
|
||||
this.onces.forEach(listener => listener.call(context, ...args))
|
||||
this.onces = []
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.listeners.length + this.onces.length
|
||||
}
|
||||
}
|
||||
|
||||
export class Dom {
|
||||
/**
|
||||
* Popups should be displayed right over the text.
|
||||
* Normally we would expect the popup to appear right over
|
||||
* the center of the text. A problem in HTML is, that it's hard
|
||||
* to determine the position of a text link, when it has a line-break
|
||||
* in it.
|
||||
*
|
||||
* This function solves this problem in the (so far) only possible way.
|
||||
*
|
||||
* 1. It removes the link from the dom tree.
|
||||
* 2. It adds an empty copy A of the link to the dom tree. (Copy is important, as the same styles have to be applied.)
|
||||
* 3. The contents of the link are added one by one to A.
|
||||
* 4. If the resulting boundingRect is bigger than the previous one, a line break is detected.
|
||||
* 5. The old line is tested, if the point was inside that boundingBox. If so save that bounding box (Goto: 7), else:
|
||||
* 6. Saves the content to a preceding clone B. And repeats from 3.
|
||||
* 7. Replace A with the initial content
|
||||
* 8. Return the found BoundingBox. If none found. Return the last bounding box.
|
||||
*/
|
||||
static getTextHitRect(link, point) {
|
||||
// We cannot use it as it produces axis aligned bounding boxes
|
||||
/* if (true) {
|
||||
let rects = link.getClientRects()
|
||||
let target = null
|
||||
for (let [idx, rect] of Object.entries(rects)) {
|
||||
target = rect
|
||||
if (Rect.contains(rect, point))
|
||||
break
|
||||
}
|
||||
return target
|
||||
|
||||
} else {*/
|
||||
let processedText = link.cloneNode(true)
|
||||
let content = processedText.innerHTML
|
||||
let words = content.split(/ /g)
|
||||
processedText.innerHTML = ''
|
||||
event.target.innerHTML = ''
|
||||
// let lineRect = event.target.getBoundingClientRect()
|
||||
let local = Points.fromPageToNode(event.target.parentNode, point)
|
||||
console.log(local)
|
||||
|
||||
let target = event.target
|
||||
let height = 0
|
||||
|
||||
while (words.length > 0) {
|
||||
let word = words.pop()
|
||||
target.innerHTML += word + ' '
|
||||
if (target.height != height) {
|
||||
// New line was reached.
|
||||
console.log('NEW LINE WAS REACHED!')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
bottom: 100,
|
||||
width: 100,
|
||||
height: 100
|
||||
}
|
||||
|
||||
// let total = words.length
|
||||
// while (words.length > 0) {
|
||||
// let lastRect = lineRect
|
||||
// let lastContent = event.target.innerHTML
|
||||
// let added = words.length == total ? "" : " "
|
||||
// added += words.shift()
|
||||
// event.target.innerHTML += added
|
||||
// lineRect = event.target.getBoundingClientRect()
|
||||
|
||||
// // When new line or last line:
|
||||
// if (lineRect.height != lastRect.height) {
|
||||
// //Reconstructure last line.
|
||||
// event.target.innerHTML = lastContent
|
||||
|
||||
// //Create Rect from last line.
|
||||
// lineRect = event.target.getBoundingClientRect()
|
||||
// rects.push(lineRect)
|
||||
|
||||
// //Copy last line content to processed text.
|
||||
// processedText.innerHTML += lastContent
|
||||
|
||||
// // Create content of new line.
|
||||
// event.target.innerHTML = added
|
||||
|
||||
// if (!processedText.parentNode) event.target.parentNode.insertBefore(processedText, event.target)
|
||||
|
||||
// if (Rect.contains(lineRect, point)) {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (words.length == 0) {
|
||||
// lineRect = event.target.getBoundingClientRect()
|
||||
// processedText.innerHTML += event.target.innerHTML
|
||||
// rects.push(lineRect)
|
||||
// }
|
||||
// }
|
||||
|
||||
// event.target.innerHTML = content
|
||||
// if (processedText.parentNode) processedText.parentNode.removeChild(processedText)
|
||||
|
||||
// return lineRect
|
||||
// }
|
||||
}
|
||||
|
||||
static printDomRect(rect, { color = 'red', pad = 0, parent = document.body }) {
|
||||
let element = document.createElement('div')
|
||||
Object.assign(element.style, {
|
||||
padding: pad + 'px',
|
||||
borderColor: color,
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'solid',
|
||||
top: rect.top + 'px',
|
||||
left: rect.left + 'px',
|
||||
width: rect.width + 'px',
|
||||
height: rect.height + 'px',
|
||||
position: 'absolute',
|
||||
zIndex: 10000,
|
||||
opacity: 0.3
|
||||
})
|
||||
parent.appendChild(element)
|
||||
}
|
||||
|
||||
static printDomPoint(point, { color = 'red', parent = document.body }) {
|
||||
let element = document.createElement('div')
|
||||
Object.assign(element.style, {
|
||||
position: 'absolute',
|
||||
top: point.y + 'px',
|
||||
left: point.x + 'px',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
backgroundColor: color,
|
||||
zIndex: 10000
|
||||
})
|
||||
parent.appendChild(element)
|
||||
}
|
||||
}
|
@ -78,6 +78,7 @@ export class ScatterContainer extends PIXI.Graphics {
|
||||
get bounds() {
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
||||
// @container: We need to call the constant values, as the container
|
||||
// gets resized, when a child moves outside the original boundaries.
|
||||
let w = this.container ? this.containerDimensions.x : this.backgroundWidth || this.app.width
|
||||
@ -161,9 +162,6 @@ export class ScatterContainer extends PIXI.Graphics {
|
||||
}
|
||||
|
||||
findHitScatter(data, displayObject, hit) {
|
||||
// if (hit) {
|
||||
// console.log("findHitScatter", displayObject)
|
||||
// }
|
||||
if (hit && this.hitScatter === null && typeof displayObject != undefined) {
|
||||
this.hitScatter = displayObject.scatter ? displayObject.scatter : null
|
||||
}
|
||||
@ -203,11 +201,13 @@ export class ScatterContainer extends PIXI.Graphics {
|
||||
if (event.claimedByScatter) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.hitScatter = null
|
||||
let interactionManager = this.renderer.plugins.interaction
|
||||
let displayObject = interactionManager.hitTest(local, this)
|
||||
if (displayObject != null && displayObject.scatter != null) this.hitScatter = displayObject.scatter
|
||||
if (this.claimEvents) event.claimedByScatter = this.hitScatter
|
||||
|
||||
return this.hitScatter
|
||||
}
|
||||
|
||||
|
44
package-lock.json
generated
@ -2845,8 +2845,7 @@
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"expand-brackets": {
|
||||
"version": "2.1.4",
|
||||
@ -3277,8 +3276,7 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -3299,14 +3297,12 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -3321,20 +3317,17 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -3451,8 +3444,7 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -3464,7 +3456,6 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -3479,7 +3470,6 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -3487,14 +3477,12 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@ -3513,7 +3501,6 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -3594,8 +3581,7 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -3607,7 +3593,6 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -3693,8 +3678,7 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -3730,7 +3714,6 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -3750,7 +3733,6 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -3794,14 +3776,12 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|