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');
|
let pre = document.createElement('pre');
|
||||||
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
|
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
|
||||||
// let re = /Doctest\.expect\(([\s\S]*)[\,\s\S]*([\s\S]*)\)/g
|
// 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 = [];
|
let better = [];
|
||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
if (replaceExpect && line.trim().startsWith('Doctest.expect(')) {
|
if (replaceExpect && line.trim().startsWith('Doctest.expect(')) {
|
||||||
@ -508,6 +508,19 @@
|
|||||||
|
|
||||||
static toLine(event) {
|
static toLine(event) {
|
||||||
return `${event.type} #${event.target.id} ${event.clientX} ${event.clientY}`
|
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() {
|
static compareExtractedWithSimulated() {
|
||||||
@ -2651,6 +2664,7 @@
|
|||||||
if (found != null) {
|
if (found != null) {
|
||||||
this.interaction.addTarget(key, found);
|
this.interaction.addTarget(key, found);
|
||||||
}
|
}
|
||||||
|
console.log(this.target);
|
||||||
}
|
}
|
||||||
let size = this.interaction.current.size;
|
let size = this.interaction.current.size;
|
||||||
let limit = this.logInteractionsAbove;
|
let limit = this.logInteractionsAbove;
|
||||||
@ -10980,6 +10994,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
speak() {
|
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();
|
this._updateText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5266
dist/iwmlib.pixi.js
vendored
@ -90,7 +90,7 @@ export default class Doctest {
|
|||||||
let pre = document.createElement('pre')
|
let pre = document.createElement('pre')
|
||||||
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
|
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
|
||||||
// let re = /Doctest\.expect\(([\s\S]*)[\,\s\S]*([\s\S]*)\)/g
|
// 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 = []
|
let better = []
|
||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
if (replaceExpect && line.trim().startsWith('Doctest.expect(')) {
|
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) {
|
if (found != null) {
|
||||||
this.interaction.addTarget(key, found)
|
this.interaction.addTarget(key, found)
|
||||||
}
|
}
|
||||||
|
console.log(this.target)
|
||||||
}
|
}
|
||||||
let size = this.interaction.current.size
|
let size = this.interaction.current.size
|
||||||
let limit = this.logInteractionsAbove
|
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.LabeledGraphics = LabeledGraphics
|
||||||
window.FontInfo = FontInfo
|
window.FontInfo = FontInfo
|
||||||
window.Text = Text
|
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() {
|
get bounds() {
|
||||||
let x = 0
|
let x = 0
|
||||||
let y = 0
|
let y = 0
|
||||||
|
|
||||||
// @container: We need to call the constant values, as the container
|
// @container: We need to call the constant values, as the container
|
||||||
// gets resized, when a child moves outside the original boundaries.
|
// gets resized, when a child moves outside the original boundaries.
|
||||||
let w = this.container ? this.containerDimensions.x : this.backgroundWidth || this.app.width
|
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) {
|
findHitScatter(data, displayObject, hit) {
|
||||||
// if (hit) {
|
|
||||||
// console.log("findHitScatter", displayObject)
|
|
||||||
// }
|
|
||||||
if (hit && this.hitScatter === null && typeof displayObject != undefined) {
|
if (hit && this.hitScatter === null && typeof displayObject != undefined) {
|
||||||
this.hitScatter = displayObject.scatter ? displayObject.scatter : null
|
this.hitScatter = displayObject.scatter ? displayObject.scatter : null
|
||||||
}
|
}
|
||||||
@ -203,11 +201,13 @@ export class ScatterContainer extends PIXI.Graphics {
|
|||||||
if (event.claimedByScatter) {
|
if (event.claimedByScatter) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hitScatter = null
|
this.hitScatter = null
|
||||||
let interactionManager = this.renderer.plugins.interaction
|
let interactionManager = this.renderer.plugins.interaction
|
||||||
let displayObject = interactionManager.hitTest(local, this)
|
let displayObject = interactionManager.hitTest(local, this)
|
||||||
if (displayObject != null && displayObject.scatter != null) this.hitScatter = displayObject.scatter
|
if (displayObject != null && displayObject.scatter != null) this.hitScatter = displayObject.scatter
|
||||||
if (this.claimEvents) event.claimedByScatter = this.hitScatter
|
if (this.claimEvents) event.claimedByScatter = this.hitScatter
|
||||||
|
|
||||||
return this.hitScatter
|
return this.hitScatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
package-lock.json
generated
@ -2845,8 +2845,7 @@
|
|||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||||
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
|
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"expand-brackets": {
|
"expand-brackets": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
@ -3277,8 +3276,7 @@
|
|||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@ -3299,14 +3297,12 @@
|
|||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@ -3321,20 +3317,17 @@
|
|||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -3451,8 +3444,7 @@
|
|||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@ -3464,7 +3456,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -3479,7 +3470,6 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@ -3487,14 +3477,12 @@
|
|||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@ -3513,7 +3501,6 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@ -3594,8 +3581,7 @@
|
|||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@ -3607,7 +3593,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@ -3693,8 +3678,7 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -3730,7 +3714,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@ -3750,7 +3733,6 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -3794,14 +3776,12 @@
|
|||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|