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.
This commit is contained in:
Severin Opel 2019-11-04 10:59:08 +01:00
parent e2ea89cc0b
commit 86b23f4e6f
121 changed files with 12779 additions and 40 deletions

31
dist/iwmlib.js vendored
View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
}
}
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@ -0,0 +1,4 @@
lincense: Public Domain
date: 29-10-2019
source: https://commons.wikimedia.org/wiki/File:BlankMap-World.svg
author: Canuckguy

View File

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

View 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>

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

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

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

View 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>

View 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'
}
}

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

View File

@ -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
View File

@ -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
} }
} }
}, },