diff --git a/dist/iwmlib.js b/dist/iwmlib.js
index 4d2024d..e107bce 100644
--- a/dist/iwmlib.js
+++ b/dist/iwmlib.js
@@ -184,7 +184,7 @@
let pre = document.createElement('pre');
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
// let re = /Doctest\.expect\(([\s\S]*)[\,\s\S]*([\s\S]*)\)/g
- let lines = text.value.split('\n');
+ let lines = text.value ? text.value.split('\n') : text.split('\n');
let better = [];
for (let line of lines) {
if (replaceExpect && line.trim().startsWith('Doctest.expect(')) {
@@ -508,6 +508,19 @@
static toLine(event) {
return `${event.type} #${event.target.id} ${event.clientX} ${event.clientY}`
+ let result = event.type;
+ let selector = this.selector(event.target);
+ result += ' selector: ' + selector;
+ if (event.target != document.querySelector(selector)) console.log('Cannot resolve', selector);
+ let keys = ['layerX', 'layerY', 'pageX', 'pageY', 'clientX', 'clientY'];
+ for (let key of keys) {
+ try {
+ result += ' ' + key + ':' + event[key];
+ } catch (e) {
+ console.log('Invalid key: ' + key);
+ }
+ }
+ return result
}
static compareExtractedWithSimulated() {
@@ -2651,6 +2664,7 @@
if (found != null) {
this.interaction.addTarget(key, found);
}
+ console.log(this.target);
}
let size = this.interaction.current.size;
let limit = this.logInteractionsAbove;
@@ -10980,6 +10994,21 @@
}
speak() {
+ /**
+ * This is a little bit ugly, but imho the most elegant of all dirty solutions.
+ *
+ 5ht * Within the plugins we have no knowledge of other cards and such. But must differentiate the
+ * clicks by their corresponding owner. The SpeechUtterance just takes a text and has no knowledge
+ * about the node that is currently read to the user.
+ *
+ * This means, that we can identify same text, but not differentiate same text on different nodes.
+ * To account for that, we add the node to the speechSynthesis object (#benefitsOfJavaScript) and
+ * have access to the node, by - let's say - expanding the functionality of the SpeechSynthesis object.
+ *
+ * SO -17.07.19
+ */
+
+ let activeNode = window.speechSynthesis['speechPluginNode'];
this._updateText();
}
diff --git a/dist/iwmlib.pixi.js b/dist/iwmlib.pixi.js
index f255d3c..5343fb5 100644
--- a/dist/iwmlib.pixi.js
+++ b/dist/iwmlib.pixi.js
@@ -1335,6 +1335,19 @@
static toLine(event) {
return `${event.type} #${event.target.id} ${event.clientX} ${event.clientY}`
+ let result = event.type;
+ let selector = this.selector(event.target);
+ result += ' selector: ' + selector;
+ if (event.target != document.querySelector(selector)) console.log('Cannot resolve', selector);
+ let keys = ['layerX', 'layerY', 'pageX', 'pageY', 'clientX', 'clientY'];
+ for (let key of keys) {
+ try {
+ result += ' ' + key + ':' + event[key];
+ } catch (e) {
+ console.log('Invalid key: ' + key);
+ }
+ }
+ return result
}
static compareExtractedWithSimulated() {
@@ -6296,6 +6309,7 @@
if (found != null) {
this.interaction.addTarget(key, found);
}
+ console.log(this.target);
}
let size = this.interaction.current.size;
let limit = this.logInteractionsAbove;
@@ -11525,6 +11539,7 @@
get bounds() {
let x = 0;
let y = 0;
+
// @container: We need to call the constant values, as the container
// gets resized, when a child moves outside the original boundaries.
let w = this.container ? this.containerDimensions.x : this.backgroundWidth || this.app.width;
@@ -11608,9 +11623,6 @@
}
findHitScatter(data, displayObject, hit) {
- // if (hit) {
- // console.log("findHitScatter", displayObject)
- // }
if (hit && this.hitScatter === null && typeof displayObject != undefined) {
this.hitScatter = displayObject.scatter ? displayObject.scatter : null;
}
@@ -11650,11 +11662,13 @@
if (event.claimedByScatter) {
return null
}
+
this.hitScatter = null;
let interactionManager = this.renderer.plugins.interaction;
let displayObject = interactionManager.hitTest(local, this);
if (displayObject != null && displayObject.scatter != null) this.hitScatter = displayObject.scatter;
if (this.claimEvents) event.claimedByScatter = this.hitScatter;
+
return this.hitScatter
}
@@ -16807,6 +16821,5222 @@
}
}
+ /**
+ * 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
+ */
+ 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) ; 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
+ }
+ }
+
+ /* ES Lint */
+
+ /**
+ * The AdvancedScatterContainer extends the ScatterContainer, but
+ * uses the findTargetNew instead of the findTarget method, which takes interactive elements
+ * into account, when calculating the target.
+ */
+ 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
+ }
+ }
+
+ 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);
+ }
+ }
+
+ 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
+ });
+ }
+ }
+
+ /**
+ * TODO
+ * A scatter which width does not increase when child elements are added.
+ */
+ 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.
+ */
+ 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
+ */
+
+ 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
+ }
+ }
+
+ /**
+ * 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}
+ */
+ 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
+ }
+ }
+
+ 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;
+ }
+ }
+
+ /**
+ * 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
+ */
+
+ 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) }
+ }
+ }
+
+ /**
+ * 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.
+ */
+
+ 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) }
+ }
+ }
+
+ /**
+ * 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}
+ */
+
+ 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}
+ */
+
+ 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.
+ */
+
+ 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
+ }
+ }
+
+ /**
+ * 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.
+ *
+ */
+ 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
+ }
+ }
+
+ /* */
+
+ 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'
+ }
+ }
+
+ /**
+ * The class CoordinateDisplay shows the coordinates of the center of the sceen
+ * in the top left of the renderer.
+ *
+ * @private
+ * @class
+ * @extends PIXI.Graphics
+ * @see {@link http://pixijs.download/dev/docs/PIXI.Graphics.html|PIXI.Graphics}
+ */
+ class CoordinateDisplay extends PIXI.Graphics {
+ /**
+ * Creates an instance of a CoordinateDisplay.
+ *
+ * @constructor
+ * @param {MapApp} mapApp - The MapApp where the coordinates should be displayed.
+ */
+ constructor(mapApp) {
+ super();
+
+ this.app = mapApp;
+
+ this.crosshair = new PIXI.Graphics();
+
+ this.outerSize = 15;
+ this.innerSize = 5;
+ this.thickness = 2;
+ this.color = 0xe73230;
+
+ this.crosshair.lineStyle(this.thickness, this.color);
+ this.crosshair.moveTo(-this.outerSize, 0);
+ this.crosshair.lineTo(-this.innerSize, 0);
+ this.crosshair.moveTo(this.innerSize, 0);
+ this.crosshair.lineTo(this.outerSize, 0);
+
+ this.crosshair.moveTo(0, -this.outerSize);
+ this.crosshair.lineTo(0, -this.innerSize);
+ this.crosshair.moveTo(0, this.innerSize);
+ this.crosshair.lineTo(0, this.outerSize);
+
+ this.text = new PIXI.Text(
+ 'Pending ...',
+ new PIXI.TextStyle({
+ fontFamily: 'Arial',
+ fontSize: 14,
+ fontWeight: 'bold',
+ fill: '#f6f6f6',
+ stroke: '#434f4f',
+ strokeThickness: 3
+ })
+ );
+
+ this.refreshCoordinates();
+
+ this.yOffset = 20;
+ this.volume = 32;
+ this.margin = 5;
+
+ this._updateFrame();
+
+ this.text.position.set(20 + this.margin, this.yOffset + this.volume + 2 * this.margin);
+
+ this.addChild(this.text);
+ this.addChild(this.crosshair);
+ this.setCrosshair();
+
+ window.setInterval(this.refreshCoordinates.bind(this), 300);
+ }
+
+ _updateFrame() {
+ this.clear();
+ this.lineStyle(3, 0x434f4f, 1)
+ .beginFill(0x434f4f, 0.6)
+ .drawRoundedRect(20, this.yOffset + this.volume + this.margin, this.text.width + 2 * this.margin, 32, 5)
+ .endFill();
+ }
+
+ toggleCrosshair() {
+ if (this.crosshair.parent) this.disableCrosshair();
+ else this.enableCrosshair();
+ }
+
+ enableCrosshair() {
+ this.addChild(this.crosshair);
+ }
+
+ disableCrosshair() {
+ this.removeChild(this.crosshair);
+ }
+
+ /**
+ * Refreshes displayed coordinates.
+ *
+ * @return {MapApp} Returns the MapApp object for chaining.
+ */
+ refreshCoordinates() {
+ if (this.app.mapLayer.mapview.focus && this.app.mapLayer.mapview.zoom) {
+ //TODO pass a mapview to avoid global access.
+ var coords = this.app.mapLayer.mapview.focus;
+ this.setCrosshair();
+ this.text.text =
+ 'Lat: ' +
+ coords.x.toFixed(4) +
+ '| Lng: ' +
+ coords.y.toFixed(4) +
+ '| Zoom: ' +
+ this.app.mapLayer.mapview.zoom.toFixed(2);
+ this._updateFrame();
+ }
+ }
+ setCrosshair() {
+ this.crosshair.position.set(this.app.center.x, this.app.center.y);
+ }
+ }
+
+ /**
+ * The Layers Framework makes managing layers, that may have a different underlying
+ * technology (e.g. PIXI and DOM) more convenient.
+ *
+ * Layer is an abstract class, which makes the Framework quite generic and expandable.
+ */
+ class Layer {
+ constructor({ name = 'unnamed', layers = [], debug = false, container = null, root = null } = {}) {
+ this.parent = null;
+
+ this._container = container;
+ if (this.container) {
+ this._defineAsLayer();
+ }
+
+ // Layers should be distinct from regular elements.
+ this.name = name;
+ //Object to backup values when manipulating the Dom.
+ this.backup = {};
+
+ this.layers = [];
+ layers.forEach(layer => this.addLayer(layer));
+
+ this.elements = [];
+
+ // Adds the root Element to the page.
+ if (root) {
+ this._appendRoot(root);
+ }
+
+ this.debug = debug;
+ if (debug) this._visualizeLayer();
+ }
+
+ /**
+ * If the element is a root element, it has to know
+ * how it appends to the page.
+ */
+ _appendRoot(ground) {
+ console.error('Method _appendRoot(_appendRoot) is abstract. Overload it in subclass', this, ground);
+ }
+
+ /**
+ * The container has to know which layer it corresponds to.
+ * Therefore we need to set an attribute to identify the layer.
+ * This may change depending on the elements.
+ * Therefore subclasses have to overload it.
+ */
+ _defineAsLayer() {
+ console.error('Method _defineAsLayer is abstract. Overload it in subclass.', this);
+ }
+
+ /**
+ * Adds an element as child, whether it's an element or an layer.
+ *
+ * @param {object} elem - Element or Layer to add as child to the layer.
+ */
+ place(elem) {
+ if (elem instanceof Layer) this.placeLayer(elem);
+ else {
+ this.placeElement(elem);
+ }
+ }
+
+ /**
+ * Adds an layer as child of the layer-
+ *
+ * @param {Layer} layer - Layer to be added as child.
+ */
+ placeLayer(layer) {
+ if (this.container) this.placeElement(layer.container);
+ this.layers.push(layer);
+ layer.parent = this;
+ return true
+ }
+
+ remove(elem) {
+ if (elem instanceof Layer) {
+ this.removeLayer(elem);
+ } else this._removeElement(elem);
+ }
+
+ removeLayer(layer) {
+ let idx = this.layers.indexOf(layer);
+ let removedArr = this.layers.splice(idx, 1);
+ if (removedArr.length > 0) {
+ let container = removedArr[0].container;
+ if (container) container.parent.removeChild(container);
+ }
+ }
+
+ /**
+ * Removes an element from the Layer.
+ * Calls the abstract method removeElement for custom behaviours in
+ * subclasses.
+ *
+ * @private
+ * @param {object} element - Element within the 'this.children' array to be removed.
+ * @memberof Layer
+ */
+ _removeElement(element) {
+ let elementIndex = this.elements.indexOf(element);
+
+ if (elementIndex === -1) {
+ console.error('The element you wanted to remove was not a child of the layer.', element, this);
+ } else {
+ this.elements.splice(elementIndex, 1);
+ this.elements.forEach(child => console.log(child));
+ this.removeElement(element);
+ }
+ }
+
+ /**
+ * Removes an element from the Layer.
+ *
+ * @publuc
+ * @param {object} element - Element within the 'this.children' array to be removed.
+ * @memberof Layer
+ */
+ removeElement(element) {
+ console.error('Method removeElement(element) is abstract. Overload it in subclass.', this, element);
+ }
+
+ getLayerByName(name) {
+ for (let i = 0; i < this.layers.length; i++) {
+ if (this.layers[i].name == name) {
+ return this.layers[i]
+ }
+ }
+ return null
+ }
+
+ /**
+ * The container, that holds the child elements of
+ * the respective technology.
+ */
+ get container() {
+ return this._container
+ }
+
+ /**
+ * Adds a non-layer child to the layer.
+ */
+ placeElement(element) {
+ console.error('Method placeElement(element) is abstract. Overload it in subclass.', this, element);
+ }
+
+ /**
+ * Moves the layer one up.
+ */
+ up() {
+ if (this.parent) this.parent.requestUp(this);
+ else console.warn("Root layer can't be moved up: ", this);
+ }
+
+ /**
+ * Moves a child element one layer up.
+ *
+ * @param {object} child - Child element to change.
+ */
+ requestUp(child) {
+ console.error('Method requestUp(child) is abstract. Overload it in subclass.', child);
+ }
+
+ /**
+ * Moves the layer one down.
+ */
+ down() {
+ if (this.parent) this.parent.requestDown(this);
+ else console.warn("Root layer can't moved down: ", this);
+ }
+
+ /**
+ * Moves a child element one layer down.
+ *
+ * @param {object} child - Child element to change.
+ */
+ requestDown(child) {
+ console.error('Method requestDown(child) is abstract. Overload it in subclass.', child);
+ }
+
+ /**
+ * Bring the element on Top of the layer.
+ *
+ * @param {object} element - Child element, that should be put on top.
+ */
+ toFront() {
+ console.log(this);
+ if (this.parent && this.container) this.parent.requestToFront(this.container);
+ else console.warn("Root layer can't be put toFront: ", this);
+ }
+
+ /**
+ * Brings the element to the top of the layer.
+ *
+ * @param {object} element - Child element, that should be put on top.
+ */
+ requestToFront(element) {
+ console.error('Method requestToFront(element) is abstract. Overload it in subclass.', element);
+ }
+
+ /**
+ * Brings the layer to the bottom.
+ */
+ toBottom() {
+ if (this.parent && this.container) this.parent.requestToBottom(this.container);
+ else console.warn("Root layer can't be put ToBottom: ", this);
+ }
+
+ /**
+ * Brings the element to the bottom of the layer.
+ *
+ * @param {object} element - Child element, that should be put on bottom.
+ */
+ requestToBottom(element) {
+ console.error('Method requestToBottom(element) is abstract. Overload it in subclass.', element);
+ }
+
+ get position() {
+ console.error('Getter position is abstract. Overload is in subclass:', this);
+ }
+
+ set position(value) {
+ console.error('Setter position is abstract. Overload is in subclass:', this, value);
+ }
+
+ /*
+ * Hides the layer.
+ */
+ hide() {
+ console.error('Method hide() is abstract. Overload is in subclass:', this);
+ }
+
+ /*
+ * Shows the layer.
+ */
+ show() {
+ console.error('Method show() is abstract. Overload is in subclass:', this);
+ }
+
+ get visible() {
+ console.error("Getter 'visible' is abstract. Overload it in subclass.", this);
+ }
+
+ toggle() {
+ if (this.visible) this.hide();
+ else this.show();
+ }
+
+ get root() {
+ if (this.parent) return this.parent.root
+ else return this
+ }
+
+ static find(element) {
+ console.error('Call the find method of an appropriate child method: ', element);
+ }
+
+ subscribeEvent(name, callback, { bubbleOnly = false, captureOnly = false, stopPropagation = false } = {}) {
+ let event = {
+ callback,
+ options: { bubbleOnly, captureOnly, stopPropagation }
+ };
+ if (!this.eventListeners.hasOwnProperty(name)) {
+ this.eventListeners.name = event;
+ } else {
+ if (Array.isArray(this.eventListeners.name)) {
+ this.eventListeners.name.push(event);
+ } else {
+ let arr = [this.eventListeners.name];
+ this.eventListeners.name = arr;
+ this.eventListeners.name.push(event);
+ }
+ }
+ }
+
+ dispatchEvent(name, data, { bubble = false, capture = false } = {}) {
+ if (bubble && this.parent) this.parent._eventOccured(name, data, this, 'bubble');
+ if (capture)
+ this.layers.forEach(layer => {
+ layer.dispatchEvent(name, data, this, 'capture');
+ });
+ }
+
+ _eventOccured(name, data, target, phase) {
+ if (this.eventListeners.hasOwnProperty(name)) {
+ this.eventListeners.name.callback({ name, data, target, phase });
+ }
+ }
+
+ /**
+ * Clones the layer with all its children.
+ */
+ clone() {
+ console.error('Call of abstract method Layer.clone(). Inherit in subclass.', this);
+ }
+
+ /**
+ * Clones an element on the layer.
+ */
+ static cloneElement(element) {
+ console.error('Call of abstract static method Layer.cloneElement(element). Inherit in subclass.', this);
+ }
+ }
+
+ /**
+ * The Layer class for DOM elements.
+ */
+ class DOMLayer extends Layer {
+ constructor(opts) {
+ super(
+ Object.assign(
+ {
+ container: DOMLayer._createContainer()
+ },
+ opts
+ )
+ );
+ }
+
+ _appendRoot(ground) {
+ ground.appendChild(this.container);
+ }
+
+ _visualizeLayer() {
+ if (this.container) {
+ let span = document.createElement('span');
+ span.innerHTML = this.name;
+ this.container.appendChild(span);
+ }
+ }
+
+ _defineAsLayer() {
+ this._container.layer = this;
+ }
+
+ static _createContainer() {
+ let container = document.createElement('div');
+ return container
+ }
+
+ placeLayer(layer) {
+ layer.container.position = 'absolute';
+ super.placeLayer(layer);
+ }
+
+ placeElement(domElement) {
+ if (domElement instanceof HTMLElement) {
+ this.elements.push(domElement);
+ this._container.appendChild(domElement);
+ } else console.error('Could not add element, it is not a DOM Element.', domElement);
+ }
+
+ static createLayer(style) {
+ let dom = document.createElement('div');
+
+ return dom
+ }
+
+ static find(element) {
+ if (element.layer) return element.layer
+
+ while (element.parentNode) {
+ element = element.parentNode;
+ if (element.layer) return element.layer
+ }
+
+ return null
+ }
+
+ /**
+ * Moves the layer one up.
+ */
+ up() {
+ if (this.parent && this.container) this.parent.requestUp(this.container);
+ else console.warn("Root layer can't be moved up: ", this);
+ }
+
+ /**
+ * Moves a child element one layer up.
+ *
+ * @param {object} child - Child element to change.
+ */
+ requestUp(child) {
+ if (child.nextSibling) child.parentNode.insertBefore(child.nextSibling, child);
+ }
+
+ /**
+ * Moves the layer one down.
+ */
+ down() {
+ if (this.parent && this.container) this.parent.requestDown(this.container);
+ else console.warn("Root layer can't moved down: ", this);
+ }
+
+ /**
+ * Moves a child element one layer down.
+ *
+ * @param {object} child - Child element to change.
+ */
+ requestDown(child) {
+ if (child.previousSibling) child.parentNode.insertBefore(child, child.previousSibling);
+ }
+
+ /**
+ * Brings the element to the top of the layer.
+ *
+ * @param {object} element - Child element, that should be put on top.
+ */
+ requestToFront(element) {
+ console.log(element);
+ element.parentNode.appendChild(element);
+ }
+
+ /**
+ * Brings the element to the bottom of the layer.
+ *
+ * @param {object} element - Child element, that should be put on bottom.
+ */
+ requestToBottom(element) {
+ element.parentNode.prepend(element);
+ }
+
+ /*
+ * Hides the layer.
+ */
+ hide() {
+ if (this._container.style.display != 'none') {
+ this.backup.display = this._container.style.display;
+ }
+ this._container.style.display = 'none';
+ }
+
+ /*
+ * Shows the layer.
+ */
+ show() {
+ this._container.style.display = this.backup.display ? this.backup.display : 'initial';
+ }
+
+ get visible() {
+ return this._container.style.display == 'none'
+ }
+
+ get position() {
+ let x = this.container && this.container.style.left ? parseInt(this.container.style.left) : 0;
+ let y = this.container && this.container.style.top ? parseInt(this.container.style.top) : 0;
+ return { x, y }
+ }
+
+ set position(value) {
+ if (this.container) {
+ this.container.style.left = value.x + 'px';
+ this.container.style.top = value.y + 'px';
+ }
+ }
+ }
+
+ /**
+ * The Layer class for PIXI Elements.
+ */
+ class PIXILayer extends Layer {
+ constructor(opts = {}) {
+ opts = Object.assign(
+ {
+ container: new PIXI.Graphics(),
+ debug: false
+ },
+ opts
+ );
+ super(opts);
+
+ if (opts.debug) {
+ this.container.interactive = true;
+ this.container.on('pointerdown', event => {
+ console.log(this.name);
+ });
+ }
+ }
+
+ static cloneElement(element) {
+ if (element instanceof PIXI.Graphics) {
+ let clone = element.clone();
+ console.log('Clone Parent: ', clone.parent);
+ // PIXI does not clone the transform properties.
+ // We have to apply them manually.
+
+ clone.position = element.position;
+ clone.rotation = element.rotation;
+ clone.scale = element.scale;
+
+ return clone
+ } else {
+ console.error('Cloning of PIXI element not yet implemented: ', element);
+ }
+ }
+
+ _appendRoot(ground) {
+ ground.stage.addChild(this.container);
+ }
+
+ _defineAsLayer() {
+ this._container.layer = this;
+ }
+
+ transform(event) {
+ if (event.about) {
+ let relScale = this.scale / event.scale - 1;
+ let localAnchor = this.toLocal(event.about);
+ event.translate = Points.add(event.translate, Points.multiplyScalar(localAnchor, relScale));
+ }
+
+ super.transform(event);
+ }
+
+ toLocal(point) {
+ return Points.subtract(point, this.position)
+ }
+
+ _visualizeLayer() {
+ this.place(new PIXI.Text(this.name));
+ }
+
+ _update(delta) {
+ const margin = 30;
+ const length = 100;
+
+ let origin = new PIXI.Polygon([
+ new PIXI.Point(this.position.x, this.position.y + length * this.scale),
+ new PIXI.Point(this.position.x, this.position.y),
+ new PIXI.Point(this.position.x, this.position.y - length * this.scale),
+ new PIXI.Point(this.position.x, this.position.y),
+ new PIXI.Point(this.position.x - length * this.scale, this.position.y),
+ new PIXI.Point(this.position.x, this.position.y),
+ new PIXI.Point(this.position.x + length * this.scale, this.position.y)
+ ]);
+
+ let rect = new PIXI.Rectangle(
+ this.position.x + margin * this.scale,
+ this.position.y + margin * this.scale,
+ length * this.scale,
+ length * this.scale
+ );
+
+ this.container.drawShape(origin);
+ this.container.drawShape(rect);
+ }
+
+ placeElement(pixiElement) {
+ try {
+ pixiElement.layer = this;
+ this._container.addChild(pixiElement);
+ this.elements.push(pixiElement);
+ return true
+ } catch (e) {
+ console.error(e, this, pixiElement);
+ }
+ return false
+ }
+
+ requestUp(child) {
+ if (child instanceof Layer) {
+ if (child.container) child = child.container;
+ else return
+ }
+
+ const oldIdx = this.children.indexOf(child);
+ if (oldIdx > -1) {
+ const newIdx = oldIdx + 1;
+
+ if (newIdx < this.children.length) {
+ let elem = this.children.splice(oldIdx, 1);
+ this.children.splice(newIdx, 0, elem[0]);
+ }
+ }
+ }
+
+ requestDown(child) {
+ if (child instanceof Layer) {
+ if (child.container) child = child.container;
+ else return
+ }
+
+ const oldIdx = this.children.indexOf(child);
+ if (oldIdx > -1) {
+ const newIdx = oldIdx - 1;
+
+ if (newIdx >= 0) {
+ let elem = this.children.splice(oldIdx, 1);
+ this.children.splice(newIdx, 0, elem[0]);
+ }
+ }
+ }
+
+ requestToFront(child) {
+ const oldIdx = this.children.indexOf(child);
+ this.children.splice(oldIdx, 1);
+ this.children.push(child);
+ }
+
+ requestToBottom(child) {
+ const oldIdx = this.children.indexOf(child);
+ this.children.splice(oldIdx, 1);
+ this.children.unshift(child);
+ }
+
+ get children() {
+ return this.container.children
+ }
+
+ follow(layer) {
+ super.follow(layer);
+ this.container.position = this.position;
+ this.container.scale = { x: this.scale, y: this.scale };
+ }
+
+ set position(value) {
+ if (this.container) this.container.position = value;
+ }
+
+ get position() {
+ if (this.container) return this.container.position
+ }
+
+ show() {
+ this.container.visible = true;
+ }
+
+ hide() {
+ this.container.visible = false;
+ }
+
+ get visible() {
+ return this.container.visible
+ }
+
+ static find(element) {
+ if (element.layer) return element.layer
+
+ while (element.parent) {
+ element = element.parent;
+ if (element.layer) return element.layer
+ }
+
+ return null
+ }
+
+ removeElement(element) {
+ element.parent.removeChild(element);
+ }
+ }
+
+ //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.
+ */
+ 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.
+ */
+ 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$1.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;
+
+ /**
+ * MapApp is responsible for showing fullscreen
+ * map applications.
+ *
+ */
+ 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$1.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;
+ }
+
+ // 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);
+ }
+ }
+
+ /* globals PIXI */
+
+ 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
+ }
+ }
+
+ 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;
+ }
+ }
+
+ 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)
+ }
+ }
+
+ 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
+ }
+ }
+
+ /**
+ * 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
+ */
+ 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.
+ */
+ 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() {}
+ }
+
+ 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();
+ }
+ }
+ }
+
+ 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. 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. GeoPoint is a single coordinate in the map. Geo line is a set of points, that are rendered as a line and can be updated individually. 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. 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.
+ This implementation is roughly based on
+ RFC7946.
+ GeoJson objects consist of two objects:
+ GeoGraphics
+ GeoPoint
+ GeoLine
+ Geoshape
+ GeoJson
+
+
+ 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
+ Point section).
+
Type | +Implemented | +
Point | ++ + | +
Line | ++ + | +
Polygon | ++ + | +
MultiPoint | ++ + | +
MultiLine | ++ + | +
MultiPolygon | ++ + | +
Points represent a single point on the map. In the following all valid coordinate types are shown
+ +The line can be used to show a path or connection between multiple points.
+ + +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.
+ +Multipolygons are an array of polygons.
+ +The GeoJson data is used to create a + GeoGraphics object.
+ + + + \ No newline at end of file diff --git a/lib/pixi/maps/geojson.js b/lib/pixi/maps/geojson.js new file mode 100644 index 0000000..4f6e919 --- /dev/null +++ b/lib/pixi/maps/geojson.js @@ -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 + } + } +} diff --git a/lib/pixi/maps/geolayer.js b/lib/pixi/maps/geolayer.js new file mode 100644 index 0000000..d2c58d2 --- /dev/null +++ b/lib/pixi/maps/geolayer.js @@ -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 diff --git a/lib/pixi/maps/map.js b/lib/pixi/maps/map.js new file mode 100644 index 0000000..90486d3 --- /dev/null +++ b/lib/pixi/maps/map.js @@ -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 + } +} diff --git a/lib/pixi/maps/mapapp.html b/lib/pixi/maps/mapapp.html new file mode 100644 index 0000000..7754845 --- /dev/null +++ b/lib/pixi/maps/mapapp.html @@ -0,0 +1,105 @@ + + + + + ++ 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. +
+ + +WHAT TO SEE: The map should focus Paris.
+ + + + \ No newline at end of file diff --git a/lib/pixi/maps/mapapp.js b/lib/pixi/maps/mapapp.js new file mode 100644 index 0000000..6102923 --- /dev/null +++ b/lib/pixi/maps/mapapp.js @@ -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) + } +} diff --git a/lib/pixi/maps/mapdata.html b/lib/pixi/maps/mapdata.html new file mode 100644 index 0000000..8d26a08 --- /dev/null +++ b/lib/pixi/maps/mapdata.html @@ -0,0 +1,201 @@ + + + + + +Mapdata calculates is responsible for transforming map coordinates to pixel coordinates and backwards.
+ +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).
+Coordinates: + +
+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.
+Coordinates: + +
+Maps can be also translated, if the whole world is shown and clipping is not an option.
+Coordinates: + +
+Maps represent a geographical image in a PIXI.Application. Preferably in a MapApp to have more convenient methods to + handle the maps.
+ +Usually maps should cover the whole app, or a window inside the app. Therefore they support a cover option. + This option allows the +
+ + +A map, that is represented by a single image.
+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.
++ 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. +
++ 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. +
+ + ++ 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. +
+ ++ The overlayclass creates a convenient way to create and design + complex map overlays. +
+ + + + + + + \ No newline at end of file diff --git a/lib/pixi/maps/overlay.js b/lib/pixi/maps/overlay.js new file mode 100644 index 0000000..5253990 --- /dev/null +++ b/lib/pixi/maps/overlay.js @@ -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 +} diff --git a/lib/pixi/maps/projections/mercator.js b/lib/pixi/maps/projections/mercator.js new file mode 100644 index 0000000..0349d21 --- /dev/null +++ b/lib/pixi/maps/projections/mercator.js @@ -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) } + } +} diff --git a/lib/pixi/maps/projections/projection.js b/lib/pixi/maps/projections/projection.js new file mode 100644 index 0000000..68acd62 --- /dev/null +++ b/lib/pixi/maps/projections/projection.js @@ -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) } + } +} diff --git a/lib/pixi/maps/projections/projections.html b/lib/pixi/maps/projections/projections.html new file mode 100644 index 0000000..a7ac5c4 --- /dev/null +++ b/lib/pixi/maps/projections/projections.html @@ -0,0 +1,242 @@ + + + + + ++ 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. +
+ +The most commonly used projection is the mercator projection.
+ ++ 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. +
+ +The Scatter in Tüsch specifies some classes to make them more suitable for Map applications.
+ +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. + + + +
++
opts = {..., boundaries: {min: {x:0,y:0},
+ max{x:1,y:1},...}}
these will be applied relatively to the image dimensions.Some utility functionalities for the Tuesch.
+ ++ 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. +
+ + +