project files added

This commit is contained in:
mhalfmann
2021-06-15 16:00:08 +02:00
parent e156e2f053
commit db46afa351
13928 changed files with 1569902 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
/**
* 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}
*/
export 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)
}
}
File diff suppressed because one or more lines are too long
+774
View File
@@ -0,0 +1,774 @@
import { Points } from '../../utils.js'
import { EventHandler } from './utils.js'
import { FlagPolygon } from '../graphics/label.js'
import { DeepZoomMap } from './map.js'
import { PIXIUtils } from './utils.js'
/**
* * GeoGraphics are graphical objects, that does not store the graphics information
* in screen space, but in geographical coordinates. Therefore GeoGraphics must be
* placed on GeoLayers to work properly.
*
* (Note: As GeoLayers are always children of a map layer. When the map is changed
* all GeoLayers are notified via the 'adaptTo(map)' method.)
*
* The geolayers forward this 'adaptTo' to all children that are GeoGraphics.
* Which adjust their so called 'point' data to the new map.
*
* @export
* @class GeoGraphics
*/
export class GeoGraphics {
constructor(coordinates, { scale = 1, onDraw = null, onDrawEnd = null, debug = false } = {}) {
this._coordinates = coordinates
this.debug = debug
this.graphics = new PIXI.Graphics()
this.scale = scale
this.drawHandler = new EventHandler('onDraw', { listeners: onDraw })
this.drawEndHandler = new EventHandler('onDrawEnd', { listeners: onDrawEnd })
this._points = null
this._position = null
}
/**
* The coordinates of the geographics.
*
* @member {array}
* @readonly
* @memberof GeoGraphics
*/
get coordinates() {
return this._coordinates
}
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 layer() {
return this._layer ? this._layer : null
}
setLayer(layer) {
this._layer = layer
}
/**
* Map of the containing layer. Null if on no layer.
*
* @readonly
* @memberof GeoGraphics
*/
get map() {
let map = null
if (this.mapLayer) {
map = this.mapLayer.map
}
return map
}
/**
* MapLayer of the containing layer. Null if on no layer.
*
* @member {MapLayer}
* @readonly
* @memberof GeoGraphics
*/
get mapLayer() {
let mapLayer = null
if (this.layer) {
mapLayer = this.layer.mapLayer
}
return mapLayer
}
/**
* Prepare draw is a private function, that prepares the graphics
* for the next draw call. It also fires the drawHandler.
*
* @private
* @memberof GeoGraphics
*/
_prepareDraw() {
this.graphics.clear()
/*
Set a fillcolor and a stroke style for
debugging. Can be overloaded using the onDraw
event function.
*/
if (this.debug) this.graphics.beginFill(0xff00ff)
}
_updatePosition() {
let points = this._getPoints()
this._position = GeoGraphics.calculateCenterOfMass(points)
this._manipulatePoints(point => {
point = Points.subtract(point, this._position)
return point
})
this.graphics.position = this._position
}
/**
* Calculates the center of Mass for a set of points.
*
* @static
* @param {Array} points - Array of points in the format {x: a, y:b}
* @returns {object} - Returns a point containing the center of mass of the polygon.
* @memberof GeoGraphics
*/
static calculateCenterOfMass(points) {
let com = new PIXI.Point()
points.forEach(p => {
let point = new PIXI.Point(p.x, p.y)
com = Points.add(com, point)
})
return Points.multiplyScalar(com, 1 / points.length)
}
}
/**
* Represents a single point on the Map.
*
* This GeoGraphics does not provide any visual representation.
* Draw the desired shape in the onDraw callback.
*
* @export
* @class GeoPoint
* @extends {GeoGraphics}
*/
export class GeoPoint extends GeoGraphics {
clone() {
return new GeoPoint(this.coordinates, this._cloneOptions())
}
_adaptCoordinates(map) {
let scale = 1
if (map instanceof DeepZoomMap) {
scale = map.image.scale.x
}
scale = scale / 4
return map.coordinatesToPoint(this.coordinates)
}
_getPoints() {
return [this.points]
}
_manipulatePoints(func) {
this._points = func(this._points)
}
_draw() {}
}
/**
* Represensts a line between two locations.
*
* @export
* @class GeoLine
* @extends {GeoGraphics}
*/
export class GeoLine extends GeoGraphics {
/**
* @param {object} opts - Optional values
* @param {array} [opts.points=[]] - Initial points of the geo shape.
* @param {boolean} [closed=false] - Defines if the
*/
constructor(coordinates, { closed = false, size = 1, onDraw = null } = {}) {
super(coordinates, {
size,
onDraw
})
this._closed = closed
}
clone() {
return new GeoLine(this.coordinates, this._cloneOptions)
}
_cloneOptions() {
let options = super._cloneOptions()
Object.assign(options, {
closed: this.closed
})
}
/**
* Adds a point to the geo line.
*/
addPoint(coordinate) {
this.coordinates.push(coordinate)
}
_manipulatePoints(func) {
this.points.forEach((point, idx, array) => {
array[idx] = func(point)
})
}
_getPoints() {
return this.points
}
_adaptCoordinates(map) {
let points = []
this.coordinates.forEach(point => {
points.push(map.coordinatesToPoint(point))
})
return points
}
_prepareDraw() {
this.graphics.clear()
if (this.debug) this.graphics.lineStyle(0.5, 0xff00ff)
this.drawHandler.call(this)
}
_draw() {
/**
* This resets the fill.
*
* DISCUSS: SO: "I'm not sure how the line should be defined.
* On the one hand. The line is clearly intended to
* represent a line and not an area. On the other hand,
* why should the user be prevented from using a fill for the
* area within the line. But if he want's a fill, why don't take
* a Polygon in the first place?
*
* (But if it's a predefined GeoJSON object obtained through e.g. elasticsearch,
* then the user is not in full control of the object type and it may be a good
* addition to grant the user this additional design choice.)
*
* The opportunity to do so would result in additional conditions, when creating the
* GeoGraphics of an overlay."
* */
this.graphics.beginFill(0, 0)
if (this.points.length > 0) {
this.graphics.moveTo(this.points[0].x, this.points[0].y)
for (let i = 1; i < this.points.length; i++) {
this.graphics.lineTo(this.points[i].x, this.points[i].y)
}
if (this.closed) {
this.graphics.lineTo(this.points[0].x, this.points[0].y)
}
}
}
get closed() {
return this._closed
}
set closed(val) {
if (val != this._closed) {
this._closed = val
this.draw()
}
}
}
/**
* Represents a shape on a map.
*
* @export
* @class GeoShape
* @extends {GeoGraphics}
*/
export class GeoShape extends GeoGraphics {
clone() {
return new GeoShape(this.coordinates, this._cloneOptions)
}
_manipulatePoints(func) {
this.constructor._manipulatePoints(this.points, func)
}
/**
* Mets the requirements for the _manipulatePointsMethod().
* Primarily used in subclasses to get the processing steps from
* their superclass.
*
* @static
* @protected
* @param {Array.<PIXI.Points>} points - The points array that shold be manipulated.
* @param {function} func - The function that changes the single point value. Has to return a new point.
* @memberof GeoShape
*/
static _manipulatePoints(points, func) {
points.forEach((pointArray, arrIdx) => {
pointArray.forEach((point, idx) => {
points[arrIdx][idx] = func(point)
})
})
}
_getPoints() {
return this.constructor._getPointsFrom(this.points)
}
/**
* Returns all points of a polygon array.
* Useful for when getting sub polygons in child class.
*/
static _getPointsFrom(shape) {
let concatArray = []
shape.forEach(array => {
concatArray = concatArray.concat(array)
})
return concatArray
}
_adaptCoordinates(map) {
let val = this.constructor._adaptPoint(this.coordinates, map)
return val
}
static _adaptPoint(coordinates, map) {
return coordinates.map(array => {
return array.map(point => {
return map.coordinatesToPoint(point)
})
})
}
_draw() {
this._drawFrom(this.points)
this.graphics.position = this.position
}
_drawFrom(shape) {
const { polygon, hole } = this.constructor._pointsToShape(shape)
this._drawShape(polygon, hole)
}
/**
* Separates the points array into it's two parts:
* - the solid polygon
* - a hole that is cut into the polygon (optional)
*
* @private
* @returns {object} - Returns an object containing the polygon and the hole in the form of {polygon: [...PIXI.Point], hole: [...PIXI.Point]}
*/
static _pointsToShape(points) {
let polygon = GeoShape._transformToPIXI(points[0])
let hole = points[1] ? GeoShape._transformToPIXI(points[1]) : []
return { polygon, hole }
}
/**
* Transform an array of poins into an array of PIXI.Points.
* Note: When creating PIXI.Polygons, for some reason the points
* need to be a PIXI.Points object (at least the first one).
*
* @param {array} points - Points in the form of {x:a,y:b}
* @returns An array of PIXI.Points
* @memberof GeoPolygon
*/
static _transformToPIXI(points = []) {
let polygon = []
points.forEach(point => {
polygon.push(new PIXI.Point(point.x, point.y))
})
return polygon
}
/**
* Draws a single shape onto the graphics object.
* Useful when being called from subclass.
*
* @protected
* @param {Array.<PIXI.Point>} polygon - An array of PIXI.Points for drawing a polygon.
* @param {Array.<PIXI.Point>} [hole=[]] - An Array of PIXI.Points for cutting a hole into the polygon
* @memberof GeoShape
*/
_drawShape(polygon, hole = []) {
// We save the fill specified in the onDraw event handler.
let { fill, alpha } = PIXIUtils.saveFill(this.graphics)
/**
* a) Draw the hole with a polygon.
*
* This may seem redundant to (c), but it's required (in this order(!))
* to make the hole clickable.
*
* It was a bit confusing, so I made a CodePen
* for it: https://codepen.io/Ukmasmu/pen/WJEaoK
*/
if (hole.length > 0) {
this.graphics.beginFill(0x0000ff, 0.0000001)
this.graphics.drawPolygon(hole)
}
/**
* b) Draw the shape.
*/
this.graphics.beginFill(fill, alpha)
this.graphics.drawPolygon(polygon)
/**
* c) Add the hole.
*/
if (hole.length > 0) {
this.graphics.beginHole()
this.graphics.drawPolygon(hole)
this.graphics.endHole()
}
}
}
/**
* The MultiGraphics makes use of the other GeoGraphics to
* create a single graphics element out of multiple different
* GeoGraphics.
*
* @class GeoMultiGraphics
* @extends {GeoGraphics}
*/
class GeoMultiGraphics extends GeoGraphics {
/**
* The _adaptCoordinates is called first by the adaptTo Method.
* Here all coordinates are transformed into point coordinates.
* This must be overloaded in subclass.
*
* @abstract
*/
_adaptCoordinates(map) {}
/**
* Gets all screen points in a single array.
*
* @abstract
* @returns {array} - Array of all points in the GeoGraphic.
*/
_getPoints() {
console.error(`Call of abstract method _getPoints(func). Overwrite in subclass.`, this)
}
/**
* Manipulates all points depending on a function.
* Mainly used to transform points to local space.
*
* @abstract
* @param {function} func
* @memberof GeoGraphics
*/
_manipulatePoints(func) {
console.error(`Call of abstract method _manipulatePoints(func). Overwrite in subclass.`, this)
}
/**
* The _draw method is called last on adaptation. It creates the GraphicData
* of the specified subclass. To manipulate the style of the graphic, hook an onDraw listener
* to the GeoGraphics object. It is called before the _draw and lets the user modify color and
* lineStyle of the drawn object.
*
* Note: It could also be used for more radical manipulations on the graphics object.
* But this should be used with care.
*
* @abstract
*/
_draw() {
console.error(`Call of abstract method _draw(). Overwrite in subclass.`, this)
}
}
/**
* Text that is attatched to a GeoPoint.
*
* @export
* @class GeoText
* @extends {GeoPoint}
*/
export class GeoText extends GeoPoint {
constructor(coordinates, text, opts) {
super(coordinates, opts)
this.align = opts.align
this.textStyle = Object.assign(new PIXI.TextStyle(), opts.textStyle)
this._text = new PIXI.Text(text, this.textStyle)
switch (this.align) {
case 'left':
break
case 'center':
this.text.position.x -= this.text.width / 2
break
case 'right':
this.text.position.x -= this.text.width
break
default:
console.error('Align type is not implemented: ' + this.align + ". Use 'left', 'right' or 'center'.")
}
this.text.position.y -= this.text.height / 2
this.graphics.addChild(this.text)
}
get text() {
return this._text
}
_draw() {
super._draw()
}
adaptTo(map) {
super.adaptTo(map)
}
}
/**
* A geotext with a notch at a certain Position.
*
* TODO: This is a specialization for the Tüsch. This should be inside the Tüsch project.
*/
export class GeoFlagLabel extends GeoText {
constructor(coordinates, text, opts) {
super(
coordinates,
text,
Object.assign(opts, {
align: 'left'
})
)
if (!opts.flag) {
opts.flag = {}
}
const {
alpha = 1,
color = 0xffffff,
borderAlpha = 1,
borderColor = 0x000000,
borderThickness = 0,
fillNotch = false
} = opts
Object.assign(this, {
alpha,
color,
fillNotch,
borderAlpha,
borderColor,
borderThickness
})
this.flagOptions = Object.assign(
{
notchSize: 10,
notchWidth: null,
notchHeight: null,
padding: { x: 0, y: 0 },
originOffset: { x: 0, y: 0 }
},
opts.flag
)
this.border = this.borderThickness > 0
Object.assign(this.flagOptions, {
width: this.text.width + this.flagOptions.padding.x * 2,
height: this.text.height + this.flagOptions.padding.y * 2
})
this.flagPolygon = new FlagPolygon(this.flagOptions)
// hover effect
//--------------------
const duration = 0.2
this.graphics.interactive = true
this.graphics.on('pointerover', event => {
TweenLite.to(this.graphics, duration, { alpha: 0.6 })
})
this.graphics.on('pointerout', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
this.graphics.on('pointerupoutside', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
this.graphics.on('pointercancel', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
this.graphics.on('pointertap', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
// this.graphics.on('pointerdown', event => {
// TweenLite.to(this.graphics, duration, { alpha: 0.45 })
// })
// this.graphics.on('pointerup', event => {
// TweenLite.to(this.graphics, duration, { alpha: 0.6 })
// })
}
_draw() {
super._draw()
this.graphics.beginFill(this.color, this.alpha)
this.graphics.drawPolygon(this.flagPolygon)
this.flagPolygon.placeText(this.text, this.flagOptions.padding)
if (this.border) {
const lineStyle = [this.borderThickness, this.borderColor, this.borderAlpha]
if (this.fillNotch) {
this.graphics.beginFill(this.borderColor)
this.graphics.drawPolygon(this.flagPolygon.notch)
this.graphics.endFill()
this.graphics.lineStyle(...lineStyle)
this.graphics.drawPolygon(this.flagPolygon.rect)
this.graphics.drawPolygon(this.flagPolygon.notch)
} else {
this.graphics.lineStyle(...lineStyle)
this.graphics.drawPolygon(this.flagPolygon)
}
}
}
}
/**
* The GeoMultiShape displays multiple forms.
*
* @export
* @class GeoMultiShape
* @extends {GeoShape}
*/
export class GeoMultiShape extends GeoShape {
static _manipulatePoints(points, func) {
points.forEach(shape => {
GeoShape._manipulatePoints(shape, func)
})
}
static _getPointsFrom(multiShapeArray) {
let points = []
multiShapeArray.forEach(shape => {
points = points.concat(GeoShape._getPointsFrom(shape))
})
return points
}
static _adaptPoint(coordinates, map) {
let points = []
coordinates.forEach(shape => {
let adaptedPoint = GeoShape._adaptPoint(shape, map)
points.push(adaptedPoint)
})
return points
}
calculateLocation() {
let coms = []
this.coordinates.forEach(polygon => {
coms.push(GeoGraphics.calculateCenterOfMass(polygon))
})
return GeoGraphics.calculateCenterOfMass(coms)
}
_drawFrom(multiShape) {
multiShape.forEach(shape => {
super._drawFrom(shape)
})
}
}
+380
View File
@@ -0,0 +1,380 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>GeoJson</title>
<link rel='stylesheet' href='../../iwmlib/lib/3rdparty/highlight/styles/default.css'>
<link rel='stylesheet' href='../../iwmlib/lib/../css/doctest.css'>
<script src='../../iwmlib/lib/3rdparty/highlight/highlight.pack.js'></script>
<script src='../../iwmlib/lib/3rdparty/all.js'></script>
<script src='../../iwmlib/lib/all.js'></script>
<script src='../../iwmlib/lib/pixi/all.js'></script>
<script src="../all.js"></script>
<style>
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.eot);
/* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.woff2) format('woff2'),
url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.woff) format('woff'),
url(../../../../assets/fonts/iconfont/MaterialIcons-Regular.ttf) format('truetype');
}
.inline-showcase {
display: flex
}
.map-example {
display: inline-block;
width: 256px;
margin: 5px;
}
table {
border-radius: 3px;
box-shadow: 1px 2px 3px rgba(0, 0, 0, 0.5);
color: white;
background: #333;
padding: 40px;
margin: 20px auto;
box-sizing: border-box;
}
thead td {
border-bottom: 1px solid white;
}
td {
padding: 5px;
}
span.implemented {
content: "";
color: greenyellow
}
span:before {
font-size: 1.5em;
}
span.implemented:before {
font-family: "Material Icons";
content: 'check';
width: 32px;
height: 32px;
}
span.not-implemented {
color: tomato;
}
span.not-implemented:before {
font-family: "Material Icons";
content: 'close';
}
</style>
</head>
<body onload='Doctest.run()'>
<h1>GeoJson</h1>
<p>GeoJson is a standardized format of how to display geometry in a geographical context, using latitude/longitude
pairs
to display one (or multiple) Point, Line or Polygon.
</p>
<p> This implementation is roughly based on
<a class="external" href="https://tools.ietf.org/html/rfc7946">RFC7946</a>.
</p>
<p>GeoJson objects consist of two objects:
<ul>
<li>type: The type of the object. All listed in the table below.</li>
<li>coordinates: The coordinate point(s)* of which the object is composed.</li>
</ul>
The points are an array of exactly two values in the format [longitude, latitude]. Here the implementation
breaks with the
standardized format, as multiple point formats can be used for convenience (see
<a href="#point">Point</a> section).
</p>
<table>
<thead>
<tr>
<td>Type</td>
<td>Implemented</td>
</tr>
</thead>
<tbody>
<tr>
<td>Point</td>
<td>
<span class="implemented"></span>
</td>
</tr>
<tr>
<td>Line</td>
<td>
<span class="implemented"></span>
</td>
</tr>
<tr>
<td>Polygon</td>
<td>
<span class="implemented"></span>
</td>
</tr>
<tr>
<td>MultiPoint</td>
<td>
<span class="not-implemented"></span>
</td>
</tr>
<tr>
<td>MultiLine</td>
<td>
<span class="not-implemented"></span>
</td>
</tr>
<tr>
<td>MultiPolygon</td>
<td>
<span class="implemented"></span>
</td>
</tr>
</tbody>
</table>
</ul>
<canvas id="canv_0"></canvas>
<script>
// As map an image of europe is used.
let europe = "../../../../var/examples/maps/europe/europe.jpg"
let europeMapProjection = new MapProjection(MERCATOR, {
clip: {
min: { x: 32.863294, y: -18.58 },
max: { x: 57.467973, y: 44.277158 }
}
})
let app = window.app = new MapApp({
debug: true,
view: canv_0,
focus: { x: 46.053113042094864, y: 13.150212801583365 },
zoom: 0.25,
width: 880,
height: 512,
coordsLogging: true
})
// The sprites of image maps should be loaded by the
// apps resources loader.
app.loadTextures([europe], (textures) => ready(textures), { resolutionDependent: false })
window.readyHandler = new EventHandler("onReady")
function ready(textures) {
let europeMap = new ImageMap(new PIXI.Sprite(textures.get(europe)), europeMapProjection)
app.setMap("germany", europeMap)
app.setup().run()
readyHandler.call()
}
</script>
<h2 id="point">Point</h2>
<p>Points represent a single point on the map. In the following all valid coordinate types are shown</p>
<script class="doctest">
let geoJsonPoint = [
{
//Madrid - only valid GeoJson format.
"type": "Point",
"coordinates": [-3.701517, 40.417485]
}, {
//London - most clean version.
"type": "Point",
"coordinates": {
"latitude": 51.508271,
"longitude": -0.128505
}
},
{
//Wien - short version.
"type": "Point",
"coordinates": {
"lat": 48.212156,
"lng": 16.377807
}
},
{
//Athens - PIXI point in form {x: latitude, y: longitude}.
"type": "Point",
"coordinates": {
"x": 37.971782,
"y": 23.726510
}
}
]
</script>
<h2>Line</h2>
<p>The line can be used to show a path or connection between multiple points.</p>
<script class="doctest">
const places = {
"Madrid": [-3.701517, 40.417485],
"London": [-0.128505, 51.508271],
"Wien": [16.377807, 48.212156],
"Athens": [23.726510, 37.971782],
}
let geoJsonLine = {
"type": "LineString",
"coordinates": [
places.Madrid,
places.London,
places.Wien,
places.Athens
]
}
</script>
<h2>Polygon</h2>
<p>Polygons are used to represent shapes. They are an array of two pointarrays. The first represent the shape, the
optional
second one a hole that can be cut into the shape.</p>
<script class="doctest">
let geoJsonPolygon = {
"type": "Polygon",
"coordinates": [
[
[14.220297, 53.948800],
[14.098276, 52.869102],
[14.792306, 50.835383],
[12.267321, 50.160485],
[13.864702, 48.732998],
[12.957140, 47.509061],
[10.222090, 47.331850],
[7.615329, 47.578565],
[8.240349, 48.981837],
[6.431923, 49.474461],
[6.521907, 49.810873],
[5.998715, 51.837875],
[7.012748, 52.396167],
[7.294660, 53.673047],
[8.979846, 53.896205],
[8.644994, 54.895603],
[9.917260, 54.808699],
[11.042638, 53.970400],
[13.226022, 54.471929]
],
[
[12.364498326583373, 49.348267001730406],
[12.364498326583373, 48.653439624053],
[6.8644970015833735, 49.734852799255926,],
[6.929973207833399, 50.49886609496484,]
]
]
}
</script>
<h2>MultiPolygon</h2>
<p>Multipolygons are an array of polygons.</p>
<script class="doctest">
let geoJsonMultiPolygon = {
"type": "MultiPolygon",
"coordinates": [
[
[
[2.4534564721784875, 39.602018178422234],
[3.2148986405775872, 39.944805466067685],
[3.589576532964429, 39.73370002861697],
[3.1967690651395344, 39.30481714119756],
[2.9066958581303197, 39.389861165290846],
[2.870436707254214, 39.55963858264072]
]
],
[
[
[1.4261138640210334, 39.02533795687074],
[1.6497119610906168, 39.12020148440332],
[1.7645326055317128, 39.02293427288866],
[1.5651072757129327, 38.877400666886004],
[1.3898547131448993, 38.867887468083474],
[1.3838115213322055, 38.972462606739654],
[1.456329823084502, 38.9629621453345]
]
],
[
[
[3.9340384662878307, 40.04714135355371],
[3.970297617163993, 39.93948536023678],
[4.109291028855864, 39.93011595102749],
[4.399364235865022, 39.817583017567095],
[4.405407427677716, 39.92543076543482],
[4.254327632360457, 40.06584681024191],
[3.9642544253512995, 40.06117092743255]
]
]
]
}
</script>
<h2>Applying GeoJson Objects to the Map</h2>
<p>The GeoJson data is used to create a
<a href="./geographics.html" target="_blank">GeoGraphics</a> object.</p>
<script class="doctest">
readyHandler.add(() => {
// For this demo, we bundle all GeoJson types.
let geographics = [].concat(geoJsonMultiPolygon, geoJsonPolygon, geoJsonLine, geoJsonPoint)
// We need a geoLayer for the GeoGraphics to live on.
// (Currently not working with the MapLayer itself. SO: 13-06-2018)
let geoJsonLayer = new GeoLayer("GeoJson Layer")
app.mapLayer.place(geoJsonLayer)
// Add all items individually.
geographics.forEach(geoJson => {
const type = geoJson.type
const geometry = geoJson.coordinates
console.log(type, geometry)
// Converts all points to PIXI format.
let coordinates = GeoJson.validateAndConvert(type, geometry)
console.log("Converted: ", type, geometry)
// Use a Utils function to get an appropriate GeoGraphics for every type.
let graphics = GeoUtils.fromGeoJsonToGeoGraphics(type, coordinates, {
//The GeoGraphics shoul
onDraw: GeoJson.isLineType(type) ? function () {
this.graphics.lineStyle(10, 0x00FFFF)
} : function () {
this.graphics.beginFill(0xFF0000, 0.8)
if (type == "MultiPolygon") {
this.graphics.drawCircle(0, 0, 200)
} else if (type == "Point") {
this.graphics.drawCircle(0, 0, 50)
}
this.graphics.beginFill(0x00FF00)
}
})
// Finally place the GeoGraphics element.
geoJsonLayer.place(graphics)
})
geoJsonLayer.adapt()
})
</script>
</body>
</html>
+348
View File
@@ -0,0 +1,348 @@
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
*/
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']
}
/**
* Takes a JSON object that contains a FeatureCollection and returns an array
* of GeoJson objects.
*
* @static
* @param {array} featureCollection - Array of GeoJSON objects that were contained in the feature collection.
* @returns {array} Returns an array of geo json objects.
* @memberof GeoJson
*/
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 })
})
return list
}
/**
* Validates and converts one set of coordinates of a specific type.
*
* @static
* @param {string} type - Type of the GeoJson.
* @param {array} coordinates - array of points.
* @returns {array}
* @memberof GeoJson
*/
static validateAndConvert(type, coordinates) {
let converted = null
if (!GeoJson.validateType(type)) throw new GeoJson.InvalidTypeError(type)
else {
if (GeoJson.validateCoordinates(type, coordinates)) {
converted = GeoJson.convert(type, coordinates)
} else {
console.error(
`Coordinates are invalid. They must be in format of type '${type} - ${GeoJson._getFormatStringOfType(
type
)}'`
)
}
}
return converted
}
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 {boolean}
* @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
}
}
}
+461
View File
@@ -0,0 +1,461 @@
import { GeoMap } from './map.js'
import { EventHandler } from './utils.js'
import { GeoGraphics } from './geographics.js'
import { MapList } from './maplist.js'
import MapViewport from './mapviewport.js'
import { ScatterContainer } from '../scatter.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
*/
export class GeoLayer {
constructor(displayObject, opts = {}) {
if (displayObject == null || !(displayObject instanceof PIXI.DisplayObject)) {
console.error(
`You need to provide a displayObject to make a ${this.constructor.name} out of it.`,
displayObject
)
return null
} else {
this.geographics = []
// displayObject.map = this
this.displayObject = displayObject
this.pixiAddChild = displayObject.addChild.bind(displayObject)
displayObject.addChild = (...elements) => {
elements.forEach(element => {
if (element instanceof GeoGraphics) {
element.setLayer(this)
this.geographics.push(element)
this.pixiAddChild(element.graphics)
} else {
this.pixiAddChild(element)
}
})
}
}
this.name = opts['name'] != null ? opts.name : 'Unnamed Layer'
this.parent = null
this.parentMapLayerTransformedHandler = new EventHandler('onParentMapLayerTransformed')
this.layers = []
this._visibility = { min: 0, max: Number.MAX_VALUE }
}
parentMapLayerTransformed(mapLayer) {
this.layers.forEach(layer => {
if (!(layer instanceof MapList)) {
layer.parentMapLayerTransformed()
}
})
this.parentMapLayerTransformedHandler.call(null, mapLayer)
this.rescaleChildren()
}
rescaleChildren() {
let map = this.map
if (this.rescale) {
if (map != null) {
let scale = map.image.scatter.scale
this.displayObject.children.forEach(graphics => {
graphics.scale.set(1 / scale, 1 / scale)
})
}
}
let mapLayer = this.mapLayer
if (this.visibility && mapLayer != null) {
const zoom = mapLayer.mapview.zoom
// TODO
// Currently I dont know what elemnts was.
// We just log an error and resolve this on a later point.
if (zoom > this.visibility.min && zoom < this.visibility.max) {
this.displayObject.children.forEach(it => (it.visible = true))
} else {
this.displayObject.children.forEach(it => (it.visible = false))
}
}
}
set visibility(value) {
let { min = 0, max = Infinity } = value
this._visibility = { min, max }
}
get visibility() {
return this._visibility
}
/**
* Alias for geoLayer.displayObject.addChild.
*
* @public
* @param {GeoGraphics | PIXI.DisplayObject} element - Element to add to the displayObject.
* @memberof GeoLayer
*/
addChild(element) {
this.displayObject.addChild(element)
}
/**
* 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)
})
this.rescaleChildren()
} else console.error('There was no map specified.', this)
}
removeFromParent() {
if (this.parent) {
this.parent.removeLayer(this)
}
}
removeLayer(layer) {
let idx = this.layers.indexOf(layer)
if (idx != -1) {
layer.parent = null
this.layers.splice(idx, 1)
if (layer.displayObject.parent) {
layer.displayObject.parent.removeChild(layer.displayObject)
}
} else console.warn('Tried to remove layer that was not set.', this, layer)
}
remove(graphics) {
if (graphics instanceof GeoGraphics) {
let index = this.geographics.indexOf(geographics)
if (index != -1) {
this.displayObject.removeChild(geographics)
} else {
console.error('Could not remove geographics from geolayer.', this, geographics)
}
} else {
this.displayObject.removeChild(graphics)
}
}
set parent(parent) {
this._parent = parent
}
get parent() {
return this._parent
}
/**
* Adds a GeoLayer as child to the GeoLayer.
*
* @public
* @param {GeoLayer} layer - GeoLayer to add.
* @memberof GeoLayer
*/
addLayer(layer) {
if (layer instanceof GeoLayer) {
layer.removeFromParent()
this.layers.push(layer)
layer.parent = this
layer.parentChanged()
this.displayObject.addChild(layer.displayObject)
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)
}
parentChanged() {
this.rescaleChildren()
}
//GeoLayers have to be children of a map layer,
// therefore we can recursively get the map.
get map() {
return this.mapLayer ? this.mapLayer.map : null
}
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
// }
}
/**
* The map layer is responsible for showing certain maps, at a specific position It contains
* a list of available maps and can switch between them seamlessly. GeoGraphics placed on the MapLayer itself
* or child Geolayers will be adapted to maps and adjusted on map change automatically.
*
* The map layer is the 'king' of the geo layers. Every geolayer
* needs a map layer at it's root. Otherwise they won't work-
*
* @export
* @class MapLayer
* @extends {GeoLayer}
*/
export class MapLayer extends GeoLayer {
constructor(
mapList,
scatterContainer,
displayObject,
{
onTransform = null,
onChange = null,
focus = null,
zoom = null,
viewport = null,
name = null,
mapChangeLocked = false
} = {}
) {
super(displayObject, {
name
})
this.transformHandler = new EventHandler('onTransform', {
listeners: onTransform
})
this.scatterContainer = scatterContainer
this.changeHandler = new EventHandler('onChange', {
listeners: onChange
})
this._mapview = new MapViewport({
zoom,
focus,
viewport
})
this.mapList = mapList
// //TODO Implement error handling here.
// this.maps = maps
// if (opts.map) this.placeMap(opts.map)
this.dynamicElements = new Map()
this._mapChangeLocked = mapChangeLocked
// Binds the transformed callback beforehand.
this.transformed = this.transformed.bind(this)
this.changeMap(mapList.active)
}
//Todo: rename to mapviewport.
/**
* Returns the MapViewport of this map layer.
*
* @readonly
* @member {MapViewport}
* @memberof MapLayer
*/
get mapview() {
return this._mapview
}
get mapChangeLocked() {
return this._mapChangeLocked
}
lockMapChange() {
this._mapChangeLocked = true
}
unlockMapChange() {
this._mapChangeLocked = false
}
/**
* Adapts all child layers and their GeoGraphics.
*
* This is called primarily on a map change.
*
* @private
* @memberof MapLayer
*/
adapt() {
this.layers.forEach(layer => {
if (layer.adapt) layer.adapt(this.map)
})
}
transformed(e) {
this.mapview.update(this.map)
this.layers.forEach(layer => layer.parentMapLayerTransformed(this))
this.transformHandler.call(this)
}
/**
* Clones the map layer-
*
* @param {ScatterContainer} scatterContainer - ScatterContainer of the app.
* @param {PIXI.DisplayObject} [container=null] - Container of the newly created MapLayer. If null, an empty PIXI.Container will be created.
* @returns {MapLayer} - Returns the cloned MapLayer.
* @memberof MapLayer
*/
clone(scatterContainer, container = null) {
let mapList = this.mapList.clone()
container = container == null ? new PIXI.Container() : container
let mapLayerClone = new MapLayer(mapList, scatterContainer, container, {
name: MapLayer.idx++,
viewport: this.mapview.viewport,
focus: this.mapview.focus,
zoom: this.mapview.zoom,
mapList
})
mapLayerClone.childrenVisibility = this.childrenVisibility
return mapLayerClone
}
/**
* Helper function to quickly display the next map.
* Order is defined by the key ordering of the maplist.
*
* @memberof MapLayer
*/
next() {
let nextMap = this.mapList.next()
this.changeMap(nextMap)
}
/**
* Changes the map to the specified one, keeping the position and the zoom of the old map.
*
* @public
* @param {GeoMap} map
* @memberof MapLayer
*/
changeMap(
name
/* map ,
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.*/
) {
if (!this.mapChangeLocked) {
console.log('🗺️ Change map to: ', name)
let oldMap = this.map
this.mapList.select(name)
if (oldMap) {
oldMap.unload()
oldMap.onTransform.remove(this.transformed)
}
let map = this.map
if (map) {
map.load()
this.scatterContainer.addChild(map.image)
this.mapview.apply(map)
map.image.addChild(this.displayObject)
// A geolayer's displayObject is on the parent layer.
// A maplayer's displayobject is always the child of the map.
this.adapt()
this.changeHandler.call(this, map, oldMap)
//Call transform one time manually.
this.transformed()
map.onTransform.add(this.transformed)
} else {
console.error(`Could not change map to ${name}.`)
}
}
}
/**
* Applies the mapviews focus to the map.
* This may be useful, if the container was modified.
*
* @memberof MapLayer
*/
refocus() {
this.mapview.apply(this.map)
}
/**
* @public
* @returns {GeoMap} - Returns the active map.
* @readonly
* @memberof MapLayer
*/
get map() {
return this.mapList.map
}
/**
*
* This is required for the geo layers.
* MapLayer requests from the geoLayers traverse up to the next MapLayer.
*
* @public
* @returns {MapLayer} - Returns this MapLayer.
* @readonly
* @memberof MapLayer
*/
get mapLayer() {
return this
}
/**
* Cleans up the MapLayer.
*
* @public
* @memberof MapLayer
*/
cleanup() {
this.mapList.cleanup()
}
}
MapLayer.idx = 0
+112
View File
@@ -0,0 +1,112 @@
<html>
<head>
<title>PIXI Maps Doctests</title>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport"
content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0" />
<link rel="stylesheet" href="../../../css/index.css">
<script src="../../../dist/iwmlib.js"></script>
<template id="itemTemplate">
<a class="wrapper" href="">
<div class="preview">
<div class="thumbnail-container">
<div class="thumbnail">
<img class="icon" src="thumbnails/notfound.png">
<!-- <iframe src="" frameborder="0"></iframe> -->
</div>
</div>
<div class="title"></div>
</div>
</a>
</template>
<style>
body {
font-size: 20px;
display: flex;
flex-direction: column;
}
#logo {
left: 0;
position: absolute;
top: 30px;
}
#logo>img {
width: 80px;
}
header>h1 {
text-transform: uppercase;
letter-spacing: 0.05em;
word-spacing: 0.25em;
margin-top: 0;
}
header>p {
max-width: 720px;
line-height: 1.5em;
color: rgb(207, 207, 207);
}
header {
font-family: "Open Sans", sans-serif;
background-color: #4c4f4f;
color: whitesmoke;
padding: 68px 50px 10px 150px;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
.container {
justify-content: center;
flex: 1;
height: auto;
min-height: auto;
width: auto;
min-width: auto;
margin: 0 60px;
}
</style>
</head>
<body>
<header>
<a id="logo" target="_blank" href="http://www.iwm-tuebingen.de">
<img src="../../../assets/logos/iwm_logo_2015_twitter.png">
</a>
<h1>Maps Module</h1>
<p>The maps module provides a handy toolkit to easily integrate maps in an application. Create a full screen map
application by using the mapapp. Utilize the GeoLayer-system to integrate maps in an existing application.
Draw graphics onto the map using geographical positions instead of pixel positions with the GeoGraphics.
Or just use an Overlay to quickly draw icons for each point on a map.
</p>
</header>
<div id="container" class="container">
</div>
<script>
const index = new Index(itemTemplate, [
["GeoGraphics", "geographics.html"],
["GeoJson", "geojson.html"],
["GeoMap", "map.html"],
["MapApp", "mapapp.html"],
["MapProjection", "mapprojection.html"],
["MapViewport", "mapviewport.html"],
["Overlay", "overlay.html"],
["Scatter", "scatter.html"]
],
null)
index.load()
</script>
</body>
</html>
+145
View File
@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="en" class="dark-mode">
<head>
<meta charset="UTF-8" />
<title>Map</title>
<script src="../../3rdparty/highlight/highlight.pack.js"></script>
<link rel="stylesheet" href="../../../fonts/material-icon-font/material-icons.css">
<link rel='stylesheet' href='../../3rdparty/highlight/styles/vs2015.css'>
<link rel='stylesheet' href='../../../css/doctest.css'>
<script src="../../../dist/iwmlib.3rdparty.js"></script>
<script src="../../../dist/iwmlib.js"></script>
<script src="../../../dist/iwmlib.pixi.js"></script>
<style>
.controls {
display: flex;
}
</style>
</head>
<body onload="Doctest.run()">
<h1>Map</h1>
<p>
The Map class shows a geographical map using a scatter element.
</p>
<h2 id="DeepZoomMap">DeepZoomMap</h2>
<p>Usually a DeepZoomMap will be used to display maps. It uses a <a
href="../deepzoom/deepzoom.html">DeepZoomImage</a> to display the map in different resolutions depending on
the zoom factor.</p>
<canvas id="deepZoomCanvas">
</canvas>
<script>
</script>
<script class="doctest" data-collapsible data-collapsed data-title="Script">
(function () {
// Create the mapapp.
let app = window.DeepZoomMapApp = new MapApp({
view: deepZoomCanvas,
coordsLogging: true,
width: 512,
height: 512
})
// Load or specify a tilesConfig as required by the DeepZoomImage.
const tilesConfig = {
"tileSize": 256,
"format": "png",
"overlap": 0,
"type": "map",
"height": 1024,
"width": 1024,
"path": "../assets/maps/osm",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
// Create a projection.
const projection = new Projection.Mercator()
// Create a map data object.
const osmDeepZoomMapProjection = new DeepZoomMapProjection(projection, tilesConfig, { app })
// Create the map
const osmMap = new DeepZoomMap(osmDeepZoomMapProjection, tilesConfig)
// Add the map to the app.
app.addMap("osm", osmMap)
// Run the app when at least one map is set.
app.setup().run()
})()
</script>
<h2 id="imageMap">ImageMap</h2>
<p>Single images can be also used as maps. This can be useful for examples, debugging purposes or other use-cases
when there are no different tiles required or available.</p>
<canvas id="imageMapCanvas">
</canvas>
<script>
</script>
<script class="doctest" data-collapsible data-collapsed data-title="Script">
(function () {
// Create the mapapp.
let app = window.ImageMapApp = new MapApp({
view: imageMapCanvas,
coordsLogging: true,
width: 512,
height: 512
})
const mapTexture = "../assets/maps/wikimedia-world-robinson/2000px-BlankMap-World.png"
// The images used by the image map need to be loaded beforehand.
// Therefore this loading step is required.
app.loadSprites([mapTexture], sprites => spritesLoaded(sprites), {
resolutionDependent: false
})
spritesLoaded = (sprites) => {
// Create a projection.
const projection = new Projection.Robinson(10)
// Create a map data object.
let mapProjection = new MapProjection(projection)
// Create the map
let imageMap = new ImageMap(sprites.get(mapTexture), mapProjection)
// Add the map to the app.
app.addMap("europe", imageMap)
// Run the app when at least one map is set.
app.setup().run()
}
})()
</script>
</body>
</html>
+936
View File
@@ -0,0 +1,936 @@
import { MapObjectScatter } from './scatter.js'
import { DeepZoomImage, DeepZoomInfo } from '../deepzoom/image.js'
import { MapProjection, DeepZoomMapProjection } from './mapprojection.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 MapProjection 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 {MapProjection}[mapProjection={}] - The map projection 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(
mapProjection = {},
{
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 = mapProjection.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._mapProjection = mapProjection
this.overlays = {}
/**
* Adjust the viewport depending on the mapProjection clipping.
*/
if (this.mapProjection.clip) {
const vp = this.viewport
const cp = this.mapProjection.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
}
}
/**
* Determines if the scatter covers the container.
* @member {boolean}
* @readonly
* @memberof GeoMap
*/
get cover() {
// For cloning we want to store the cover inside the map.
// But the scatter is responsible for the cover state.
// So we update the app's cover state every time
// we get the value.
if (this.image && this.image.scatter && this.image.scatter.cover != null) {
this._cover = this.image.scatter.cover
}
return this._cover
}
/**
* Returns the image object used by the GeoMap.
*
* @readonly
* @memberof GeoMap
*/
get image() {
return this._image
}
/**
* The mapProjection of the map.
*
* @member {MapProjection}
* @readonly
* @memberof GeoMap
*/
get mapProjection() {
return this._mapProjection
}
/**
* Clears all EventHandlers.
*
* @memberof GeoMap
*/
flushHandlers() {
this.onLoad.empty()
this.onTransform.empty()
}
/**
* Locks all transformations on the map.
* Single parameters can be set if necessary. False means the value is locked, true means they can be modified.
*
* @public
* @param {object} [{ rotatable = false, translatable = false, movableX = false, movableY = false, scalable = false }={}]
* @memberof GeoMap
*/
lock({ rotatable = false, translatable = false, movableX = false, movableY = false, scalable = false } = {}) {
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
}
}
/**
* Unlocks all transformations on the map.
* Single parameters can be set if necessary. False means the value is locked, true means they can be modified.
*
* @public
* @param {object} [{ rotatable = false, translatable = false, movableX = false, movableY = false, scalable = false }={}]
* @memberof GeoMap
*/
unlock({ rotatable = true, translatable = true, movableX = true, movableY = true, scalable = true } = {}) {
this.lock({ rotatable, translatable, movableX, movableY, scalable })
}
/**
* Unloads the image of the map.
*
* @public
* @memberof GeoMap
*/
unload() {
if (this.image) {
if (this.image.parent) {
this.image.parent.removeChild(this.image)
}
if (this.scatter) {
this.scatter.killAnimation()
this.image.scatter = null
}
}
}
/**
* Removes the map, freeing all memory ba flushing handlers and removing the image.
*
* @public
* @memberof GeoMap
*/
remove() {
if (this.image) this.image.mask = null
this.removeFrame()
this.onTransform.empty()
this.onLoad.empty()
this.unload()
}
/**
* 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.
* creates the image.
*
* @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 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,
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, y}.
* @returns {object} - Coordinates on the map in form of {x: latitude, y: longitude}.
*/
coordinatesFromPoint(point) {
let coords = this.mapProjection.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 {Point} - Returns a image position in form of {x: x, y: y}.
*/
coordinatesToPoint(coordinates) {
return this.toAbsolutePixelCoordinates(this.mapProjection.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
}
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.')
}
get alpha() {
return this._alpha
}
/**
* 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
}
removeFrame() {
this.frame = null
}
/**
* The complete Triforce, or one or more components of the Triforce.
* @typedef {Object} Frame
* @property {number} x - X position of the frame.
* @property {number} y - Y position of the frame.
* @property {number} width - Width of the frame.
* @property {number} height - Height od the frame.
* @property {Point} localCenter - Local center of the map.
* @property {Point} center - Global center of the map.
*/
/**
* Sets the frame if the map.
*
* 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.
*
* @param {Frame} frame
* @memberof GeoMap
*/
setFrame(frame) {
this.frame = frame
}
/**
* Gets the frame if the map.
*
* 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.
*
* @returns {Frame} - Returns the frame of the map.
* @memberof GeoMap
*/
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
}
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, root = './') {
let error = { message: '' }
let maps = {}
if (GeoMap._validateJson(json, error)) {
for (let [mapname, data] of Object.entries(json)) {
data.tiles.path = root + data.tiles.path
maps[mapname] = GeoMap._createMap(data)
maps[mapname].name = mapname
}
} 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
* @private
* @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
* @private
* @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 mapProjection = new DeepZoomMapProjection(projection, tilesConfig)
return new DeepZoomMap(mapProjection, 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.
*
* @export
* @class DeepZoomMap
* @extends {GeoMap}
*/
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 {MapProjection} mapProjection - A MapProjection 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(mapProjection, tilesConfig, opts = {}) {
opts = Object.assign(
{
maxScale: Math.min(tilesConfig.width, tilesConfig.height) / tilesConfig.tileSize,
minScale: mapProjection.getMinScale,
highResolution: true,
debug: false
},
opts
)
super(mapProjection, opts)
this.tilesConfig = tilesConfig
this._verifyMapProjection()
}
_verifyMapProjection() {
if (!(this.mapProjection instanceof MapProjection)) {
console.error('Use the MapProjection object for creating maps!')
} else {
if (!(this.mapProjection instanceof DeepZoomMapProjection)) {
console.error('Use the DeepZoomMapProjection object.')
}
}
}
/**
* Creates the DeepZoomImage.
*
* @private
*/
load(container = null, scatter = null) {
if (!this.mapProjection.app) console.error('App was not set in the mapProjection.')
this.info = new DeepZoomInfo(this.tilesConfig)
let image = new DeepZoomImage(this.info, {
app: this.mapProjection.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
} else {
containerCenter = {
x: this.image.parent.width / 2,
y: this.image.parent.height / 2
}
}
//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
}
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.mapProjection, 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
})
return map
}
tint() {
let color = DeepZoomMap.tintcolors[DeepZoomMap.tintcolor++ % DeepZoomMap.tintcolors.length]
this._forEachTile(tile => {
tile.tint = color
})
}
_forEachTile(callback) {
this.image.children[0].children.forEach(tiles => {
tiles.children.forEach(callback)
})
}
setAlpha(alpha) {
this._forEachTile(tile => {
tile.alpha = alpha
})
}
// /** 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}
*/
export class ImageMap extends GeoMap {
constructor(sprite, mapProjection, opts = {}) {
super(mapProjection, opts)
if (this.debug) console.log('Construct Image Map', sprite, mapProjection, 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.mapProjection, {
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)
return map
}
/**
*
*
* @param {*} coordinates
* @param {*} [zoom=null]
* @param {*} [{ animate = 0 }={}]
* @memberof ImageMap
*/
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 = 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
} else {
containerCenter = {
x: this.image.parent.width / 2,
y: this.image.parent.height / 2
}
}
//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
}
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
return factor
}
}
+163
View File
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en" class="dark-mode">
<head>
<meta charset="UTF-8" />
<title>MapApp</title>
<script src="../../3rdparty/highlight/highlight.pack.js"></script>
<link rel="stylesheet" href="../../../fonts/material-icon-font/material-icons.css">
<link rel='stylesheet' href='../../3rdparty/highlight/styles/vs2015.css'>
<link rel='stylesheet' href='../../../css/doctest.css'>
<script src="../../../dist/iwmlib.3rdparty.js"></script>
<script src="../../../dist/iwmlib.js"></script>
<script src="../../../dist/iwmlib.pixi.js"></script>
<style>
.controls {
display: flex;
}
</style>
</head>
<body onload="Doctest.run()">
<h1>MapApp</h1>
<p>
This class extends the PIXIApp to simplify the process of rendering
Maps in the canvas. For that reason, it contains useful functions
for an easier handling of maps in the canvas.
</p>
<canvas id="canvas"></canvas>
<div id="mapControl"></div>
<div id="cityControl" class="controls"></div>
<p><strong>WHAT TO SEE:</strong> The map should focus Paris.</p>
<script>
let osmConfig = {
"projection": "mercator",
"type": "deepzoom",
"tiles": {
"tileSize": 256,
"format": "png",
"overlap": 0,
"type": "map",
"height": 1024,
"width": 1024,
"path": "../assets/maps/osm",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
}
let testConfig = {
"projection": "mercator",
"type": "deepzoom",
"tiles": {
"tileSize": 128,
"format": "jpg",
"overlap": 0,
"type": "map",
"height": 4096,
"width": 4096,
"path": "../assets/maps/test",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
}
</script>
<script class="doctest">
let capitals = {
london: { x: 51.5, y: -0.083333 },
rome: { x: 41.9, y: 12.483333 },
madrid: { x: 40.4, y: -3.683333 },
paris: { x: 48.833986, y: 2.346989 }
}
// You may define a focus point ...
let focus = capitals.paris
// ... and a zoom level.
let zoom = 1
// Name has to be app (like all other PIXIApps).
const app = (window.app = new MapApp({
focus,
zoom,
view: canvas,
coordsLogging: true,
width: 512,
height: 512
}))
// As map an image of europe is used.
let europe = '../assets/maps/pixabay/europe.jpg'
//Preload all required sprites for the image map.
app.loadSprites([europe], sprites => ready(sprites), {
resolutionDependent: false
})
// The map projection object contains informations,
// how the displayed map has to be interpreted.
// e.g. which projection is used or how the
// image is clipped.
let europeData = new MapProjection(new Projection.Mercator(), {
clip: {
min: { x: 32.863294, y: -18.58 },
max: { x: 57.467973, y: 44.277158 }
}
})
function ready(sprites) {
const cover = true
// When resources are loaded, the ImageMap can be instantiated.
let imageMap = new ImageMap(sprites.get(europe), europeData, {
coordsLogging: true,
maxScale: 1,
cover
})
let testMapProjection = new DeepZoomMapProjection(new Projection.Mercator(), testConfig.tiles, {
app
})
let testMap = new DeepZoomMap(testMapProjection, Object.assign({}, testConfig.tiles, { app }), { cover })
app.addMap("test", testMap)
let osmMapProjection = new DeepZoomMapProjection(new Projection.Mercator(), osmConfig.tiles, {
app
})
let deepZoomMap = new DeepZoomMap(osmMapProjection, Object.assign({}, osmConfig.tiles, { app }), { cover })
app.addMap("osm", deepZoomMap)
// Finally apply the map to the MapApp
app.setMap('europe', imageMap)
// The app requires a map before beeing able to run.
// So start the app here.
app.setup().run()
for (let [key, val] of Object.entries(app.mapList.maps)) {
let mapBtn = document.createElement("button")
mapBtn.innerText = key
mapBtn.addEventListener("click", () => {
app.mapLayer.changeMap(val)
})
mapControl.appendChild(mapBtn)
}
}
for (let [key, val] of Object.entries(capitals)) {
let cityBtn = document.createElement("button")
cityBtn.innerText = key
cityBtn.addEventListener("click", () => {
app.mapLayer.map.moveTo(val)
})
cityControl.appendChild(cityBtn)
}
</script>
</body>
</html>
+667
View File
@@ -0,0 +1,667 @@
import PIXIApp from '../app.js'
import { CoordinateDisplay } from './display.js'
import { MapLayer } from './geolayer.js'
import { RigidScatterContainer } from './scatter.js'
import { EventHandler } from './utils.js'
import { Points } from '../../utils.js'
import Logging from '../../logging.js'
import { MapList } from './maplist.js'
/**
* A PIXI.Point or object in form {x,y}.
*
* @typedef {Object} Point
* @property {number} x - Indicates whether the Courage component is present.
* @property {number} y - Indicates whether the Power component is present.
*/
/**
* A coordinate point is a PIXI.Point or object in form {x,y} that contains map coordinates
* instead of pixel values, where x represents the latitude and y the longitude.
* @typedef {Object} CoordinatePoint
* @property {number} x - Indicates whether the Courage component is present.
* @property {number} y - Indicates whether the Power component is present.
*/
/**
* MapApp is responsible for showing fullscreen
* map applications.
*
* @export
* @class MapApp
* @extends {PIXIApp}
*/
export default class MapApp extends PIXIApp {
/**
*Creates an instance of MapApp.
* @memberof MapApp
*/
constructor(opts = {}) {
super(opts)
// Default values.
opts = Object.assign(
{
debug: false,
maps: {}, //required
view: null, //required
wrapper: null, // Required if you want to use DOM Overlays.
startmap: null,
coordsLogging: false,
overlays: {},
keycodes: {},
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.keycodes = this._extractKeyCodes(opts.keycodes)
this.coordsLogging = opts.coordsLogging
this.overlays = opts.overlays
this.focus = opts.focus
this.zoom = opts.zoom
this.wrapper = opts.wrapper
this.onMapChanged = new EventHandler('mapChanged', {
listeners: opts.onMapChanged
})
this.onSizeChanged = new EventHandler('sizeChanged', {
listeners: opts.onSizeChanged
})
this.onTransform = new EventHandler('transformed', {
listeners: opts.onTransform
})
/**
* When in debug mode, this allows the user to copy the center coordinates to the clipboard.
*/
if (this.debug) {
this.DRAW_MODES = {
PIXI_POINT: 0,
PATH_MODE: 1,
POLYGON_MODE: 2,
getName: function(num) {
let result = null
for (const [key, val] of Object.entries(this)) {
if (val == num) result = key
}
return result
}
}
this.drawMode = this.DRAW_MODES.PIXI_POINT
this.drawData = []
}
this.mapList = new MapList(opts.startmap ? opts.startmap : null, opts.maps ? opts.maps : {})
this._setupKeyboardUtils()
}
/**
* Log boundaries for a shown map.
* This is for development purposes only, if you want
* to find the boundaries of a shown map.
*
* @memberof MapApp
*/
logMapBoundaries() {
let map = this.mapLayer.map
let boundaries = {
min: this.mapLayer.mapview.coordinatesFromWindowPoint(map, { x: 0, y: 0 }),
max: this.mapLayer.mapview.coordinatesFromWindowPoint(map, { x: 0, y: 0 })
}
console.log(JSON.stringify(boundaries))
}
/**
* Creates the MapLayer.
*
* @private
* @memberof MapApp
*/
_setupMapLayer() {
this.mapContainer = new PIXI.Container()
this._mapLayer = new MapLayer(this.mapList, this.scene, this.mapContainer, {
name: 'Root Map Layer',
focus: this.focus,
zoom: this.zoom,
onChange: this._mapChanged.bind(this)
})
}
setup() {
super.setup()
// TODO get access to fps display
let fpsDisplay = null
this.stage.children.forEach(element => {
if (element.refreshFps) fpsDisplay = element
})
this._setupMapLayer()
this.ui = new PIXI.Container()
this.scene.addChild(this.ui)
if (this.fpsLogging != null && fpsDisplay != null) this.ui.addChild(fpsDisplay)
if (this.coordsLogging) {
this.coordsDisplay = new CoordinateDisplay(this)
this.ui.addChild(this.coordsDisplay)
}
this.__dragging = false
this.__events = new Map()
const scene = this.scene
scene.interactive = true
scene.on('pointercancel', this.__onEnd.bind(this))
scene.on('pointerdown', this.__onStart.bind(this))
scene.on('pointermove', this.__onMove.bind(this))
scene.on('pointerout', this.__onEnd.bind(this))
scene.on('pointerup', this.__onEnd.bind(this))
scene.on('pointerupoutside', this.__onEnd.bind(this))
return this
}
/**
* You can add Dom elements directly to the app's DomLayer by calling
* appendChild(). The elements will be forced to use an absolute positioning.
* This shall prevent unwanted sideeffects with the app.
*
* @param {HTMLElement} element - Element to add to DOMRoot.
* @memberof MapApp
*/
appendChild(element) {
if (!this.wrapper)
console.error(
`When using DOMElements on ${this.constructor.name} you must specify a wrapper in the constructor.`
)
if (!(element instanceof HTMLElement))
console.error(
'You can only append HTMLElements to DOM. If you want to add PIXI elements to the scene call app.scene.addChild(element).'
)
else {
Object.assign(element.style, { position: 'absolute' })
this.wrapper.appendChild(element)
}
}
/**
* Relayouts the app. E.g. called when the window is resized.
*
* @param {number} width - Desired width of the app.
* @param {number} height - Desired height of the app.
* @memberof MapApp
*/
layout(width, height) {
this.scene.resize(width, height)
this.mapLayer.mapview.update()
this.onSizeChanged.call(this)
}
/**
* Overrides the sceneFactory of the PIXIApp to create a RigidScatterContainer instead of
* a regular PIXI.Container()
*
* @private
* @returns {RigidScatterContainer} - Returns the newly created RigidScatterContainer.
* @memberof MapApp
*/
sceneFactory() {
return new RigidScatterContainer(this.width, this.height, this.renderer, {
app: this,
showBounds: false,
showTouches: false,
showPolygon: false,
container: this,
stopEvents: false,
claimEvents: false
})
}
/**
* Changes the map to the given key.
*
* @param {string} key - Identifier of the map to change to.
* @memberof MapApp
*/
selectMap(key) {
if (this.debug) console.log('Select map', key, result)
let result = this.mapList.select(key)
if (result && this.mapLayer) {
this.mapLayer.changeMap(this.mapList.map)
}
}
/**
* Adds and sets a map to the mapapp.
*
* @param {string} key - Name of the map.
* @param {GeoMap} map - Map to add.
* @memberof MapApp
*/
setMap(key, map) {
this.addMap(key, map)
this.selectMap(key)
}
/**
* Adds a map to the maplist.
* If no map is set, the added map will be set as default.
*
* @param {string} key - Identifier for the map.
* @param {GeoMap} map - Map object to add.
* @memberof MapApp
*/
addMap(key, map) {
if (this.mapList) this.mapList.add(key, map)
else console.error('Cannot access mapLayer. It was not initialized yet.')
}
/**
* Adds multiple maps at once.
*
* @param {object} mapObject
* @memberof MapApp
*/
addMaps(mapObject) {
for (let [key, val] of Object.entries(mapObject)) {
if (val instanceof GeoMap) {
this.addMap(key, val)
} else console.warn('Tried adding maps that are not og Type GeoMap.')
}
}
transformed(event) {
this.onTransform.call(this, event)
}
/**
*
* Called when the mapLayer changed the map.
*
* @private
* @param {*} lastMap
* @memberof MapApp
*/
_mapChanged(lastMap) {
if (lastMap) {
lastMap.flushHandlers()
}
this.transformed()
this.onMapChanged.call(this, this.map)
this.map.onTransform.add(this.transformed.bind(this))
if (this.ui && this.ui.parent) {
const parent = this.ui.parent
parent.removeChild(this.ui)
parent.addChild(this.ui)
}
}
/**
* Returns the mapLayer of the map.
*
* @member {MapLayer}
* @memberof MapApp
*/
get mapLayer() {
return this._mapLayer
}
_doesOverlayElementExist(layer, type, name) {
let layerElements = this.overlayElements.get(layer)
return layerElements != undefined && layerElements[type] != null && layerElements[type][name] != null
}
_getOverlayElement(layer, type, name) {
let layerElements = this.overlayElements.get(layer)
return layerElements[type][name]
}
_setOverlayElement(layer, type, name, value) {
let obj = this.overlayElements.get(layer)
if (obj == undefined) obj = {}
if (obj[type] == null) obj[type] = {}
obj[type][name] = value
this.overlayElements.set(layer, obj)
}
/**
* Copies the current location to the clipboard.
*
* @memberof MapApp
*/
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)
}
/**
* Can be used to copy polygons to the clipboard.
*
* Useful for debugging or to roughly trace a shape in the map.
* The generated pointarray can be used as geometry of a geographic
* or inside an overlay to draw that shape onto the map.
*
* @memberof MapApp
*/
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)
}
/**
* Returns the active map.
*
* @readonly
* @memberof MapApp
*/
get map() {
return this.mapList.map
}
clearDrawData() {
this.drawData = []
}
/**
* Logs a text field on the map.
* The text element is a DOMElement.
*
* @param {string} msg - Message to log.
* @memberof MapApp
*/
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
})
}
/**
* @typedef KeyCode
* @type {object}
* @property {string} key - an ID.
* @property {boolean} altKey - Defines if KeyCode requires the alt key to be pressed.
* @property {boolean} shiftKey - Defines if KeyCode requires the shift key to be pressed.
* @property {boolean} ctrlKey - Defines if KeyCode requires the ctrl key to be pressed.
*
*/
/**
* Check's if a key event matches a defined KeyCode.
*
* @private
* @param {KeyboardEvent} event - Event that is fired on keydown, -pressed or -up.
* @param {KeyCode} keyCode - KeyCode is an object in the form of : {key:number, altKey:boolean,shiftKey:boolean,ctrlKey:boolean }
* @returns {boolean} - True, when the event matches the keycode, false otherwise.
* @memberof MapApp
*/
_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)
})
}
/**
* Checks on every key down if it matches a keycode.
*
* @private
* @param {KeyboardEvent} event
* @memberof MapApp
*/
_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
}
/**
* @typedef KeyCodePairs
* @type {object}
* @property {string} name - Name of the KeyCode.
* @property {KeyCode} keyCode - KeyCode
*
*/
/**
* Extracts keycodes from a string.
*
* KeycodeStrings may look like this 'ctrl+shift+b'
*
* @private
* @param {KeyCodePairs} nameKeyCodePairs
* @returns {array} - Returns an array of KeyCode objects.
* @memberof MapApp
*/
_extractKeyCodes(nameKeyCodePairs) {
let out = {}
for (let [name, combinationString] of Object.entries(nameKeyCodePairs)) {
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
}
}
}
__onEnd(event) {
this.__dragging = false
this.__events.delete(event.data.identifier)
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* MapList is a list of maps with one active index.
* It contains some utility functions to change the map.
*
* @export
* @class MapList
*/
export class MapList {
constructor(active = null, maps = {}) {
this._maps = maps
this._active = active
if (Object.keys(maps).length > 0) this.select(active)
}
/**
* Selects a map from the map list.
*
* @public
* @param {string} active - Name of the map to select.
* @returns {Map} - Returns the active map. Returns null if no map was added to the MapList.
* @memberof MapList
*/
select(active) {
let map = null
if (active !== this.active) {
let keys = Object.keys(this.maps)
if (keys.length > 0) {
if (this.maps[active] == null) {
let altActive = keys[0]
console.warn(
`The MapList does not contain the provided active key '${active}'. Used '${altActive}' as fallback.`
)
active = altActive
}
if (this.active !== active) {
this._active = active
map = this.maps[active]
}
} else {
console.error(`Could not provide a fallback map! The map object is empty.`)
}
}
return map
}
/**
* Clones the entire maplist.
*
* @public
* @returns {MapList} - Returns a cloned instance of this map list.
* @memberof MapList
*/
clone() {
let maps = {}
for (let name of Object.keys(this.maps)) {
maps[name] = this.maps[name].clone()
}
return new MapList(this.active, maps)
}
/**
* Adds a new map to the map list.
*
* @public
* @param {string} key - Key to identify the map.
* @param {GeoMap} map - The GeoMap to add.
* @memberof MapList
*/
add(key, map) {
if (this.maps[key] != null) consol.warn('Key already in mapList. The existing key was overwritten.')
if (this.active == null) this._active = key
map.name = key
this.maps[key] = map
}
/**
* Returns the the active map.
* If none is set, it returns null.
*
*@public
* @readonly
* @member {GeoMap}
* @memberof MapList
*/
get map() {
return this.maps && this.maps[this.active] ? this.maps[this.active] : null
}
/**
* Returns the list of addedd cards.
*
* @member {object}
* @readonly
* @memberof MapList
*/
get maps() {
return this._maps
}
/**
* Returns the active key.
*
* @member {string}
* @readonly
* @memberof MapList
*/
get active() {
return this._active
}
/**
* Selects the next map in the map array.
*
* @public
* @returns {GeoMap} - Returns the next map in the list.
* @memberof MapList
*/
next() {
let keys = Object.keys(this.maps)
let idx = keys.indexOf(this.active)
let next = idx + 1 < keys.length ? keys[idx + 1] : keys[0]
return next
}
/**
* Removes all maps from the maplist.
* And cleans up all maps.
*
* @public
* @memberof MapList
*/
cleanup() {
for (let key in this.maps) {
let map = this.maps[key]
map.remove()
}
}
}
+203
View File
@@ -0,0 +1,203 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>MapProjection</title>
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
<link rel='stylesheet' href='../../../css/doctest.css'>
<script src="../../../dist/iwmlib.3rdparty.js"></script>
<script src="../../../dist/iwmlib.js"></script>
<script src="../../../dist/iwmlib.pixi.js"></script>
<style>
.inline-showcase {
display: flex
}
.map-example {
display: inline-block;
width: 256px;
margin: 5px;
}
.map-container {
position: relative;
display: inline-flex;
}
.map-marker {
position: absolute;
transform: translate(-50%, -75%) scale(0.5);
}
</style>
</head>
<body onload='Doctest.run()'>
<h1>MapProjection</h1>
<p>The map projection calculates is responsible for transforming map coordinates to pixel coordinates and backwards.</p>
<h2>Static Squared World Map</h2>
<p>The most simple example is a squared world map, thats projected with mercator transformation. Ranging from
Longitude
-180 to 180 and Latitude from -90 to 90 (Exclusive, because mercator gets infinite at +/-90). </p>
<p>Coordinates:
<strong id="map_coords_0"></strong>
</p>
<div id="mapContainerGeneral" class="map-container">
<img src="../assets/maps/osm/0/0/0.png" id="map_image_0" alt="Image is missing." width="512">
</div>
<script>
let capitals = {
abidjan: { x: 5.349470, y: -4.006472 },
berlin: { x: 52.525430, y: 13.385291 },
canberra: { x: -35.282025, y: 149.128648 },
capetown: { x: -33.925448, y: 18.416962 },
moscow: { x: 55.750892, y: 37.622799 },
washington: { x: 38.895650, y: -77.031407 },
rio: { x: -22.871400, y: -43.280490 },
tokio: { x: 35.696278, y: 139.731366 }
}
const europeanCapitalCities = {
berlin: { x: 52.505949, y: 13.379400 },
london: { x: 51.5, y: -0.083333 },
rome: { x: 41.9, y: 12.483333 },
madrid: { x: 40.4, y: -3.683333 },
paris: { x: 48.833986, y: 2.346989 }
}
//Helper function to print the coordinates to a dom element.
function printCoordinates(element, mapProjection, out_dom) {
element.addEventListener("mousemove", (event) => {
let coords = mapProjection.toCoordinates({ x: event.offsetX / event.target.width, y: event.offsetY / event.target.height })
out_dom.innerHTML = "Lat: " + coords.x.toFixed(4) + " Lng: " + coords.y.toFixed(4)
})
}
function placeMarker(parent, position) {
const markerIconPath = "../../../assets/icons/room.png"
let marker = document.createElement("img")
marker.src = markerIconPath
marker.className = "map-marker"
Object.assign(marker.style, {
left: (position.x * 100) + "%",
top: (position.y * 100) + "%"
})
parent.appendChild(marker)
console.log(marker)
}
</script>
<script class="doctest">
// Instantiate our map projection.
let squared_world = new MapProjection(new Projection.Mercator());
// Define a position, we are interested in in {x: latitude, y: longitude}
let iceland = { x: 64.514979, y: -19.020796 }
// Gets the normalized positition of the coordinates
let pos_0 = squared_world.toPixel(iceland)
// Helper function to set the dom marker.
placeMarker(mapContainerGeneral, pos_0)
</script>
<script>
printCoordinates(map_image_0, squared_world, map_coords_0)
</script>
<h2 id="germany">Clipped Map</h2>
<p>Often we don't use the whole map, or our map is a subsection of the world. MapProjection can clip those cases, using
the a
bounding box of min and max coordinates.</p>
<p>Coordinates:
<strong id="map_coords_1"></strong>
</p>
<div class="map-container" id="mapContainerClipped">
<img src="../assets/maps/pixabay/europe.jpg" id="map_image_1" alt="Image is missing." width="512">
</div>
<script class="doctest">
//Same procedure as above, just add a clipping parameter to the MapProjection object.
let europe = new MapProjection(new Projection.Mercator(), {
clip: {
min: { x: 32.863294, y: -18.58 },
max: { x: 57.467973, y: 44.277158 }
}
})
//Same as above
for (let coordinates of Object.values(europeanCapitalCities)) {
let pixelPosition = europe.toPixel(coordinates)
placeMarker(mapContainerClipped, pixelPosition)
}
</script>
<script>
printCoordinates(map_image_1, germany, map_coords_1)
</script>
<h2>Translated Map</h2>
<p>Maps can be also translated, if the whole world is shown and clipping is not an option.</p>
<p>Coordinates:
<strong id="map_coords_2"></strong>
</p>
<div id="map_image_2" class="map-container"">
</div>
<script class=" doctest">
const size = 256
// Fake offset by using the old map projection.
let translation = squared_world.toPixel({ x: 90-10, y: -140 })
console.log(translation)
//This map is clipped at the bottom
// And also translated in hoizontal direction.
// The translate option corrects that.
let translated_world = new MapProjection(new Projection.Mercator(), {
translate: { x: -10, y: 40 },
})
console.log(translation)
Object.assign(map_image_2.style, {
width: size + "px",
height: size + "px",
backgroundImage: "url(../assets/maps/osm/0/0/0.png)",
backgroundColor: "red",
backgroundPosition: translation.x * size + "px " +translation.y * size + "px"
})
//Same as above
for (let coordinates of Object.values(capitals)) {
let position = translated_world.toPixel(coordinates)
placeMarker(map_image_2, position)
}
</script>
<script>
map_image_2.addEventListener("mousemove", (event) => {
let map = map_image_2
console.log(event.offsetX / map.width)
let coords = translated_world.toCoordinates({ x: event.offsetX / map.offsetWidth, y: event.offsetY / map.offsetHeight })
map_coords_2.innerHTML = "Lat: " + coords.x.toFixed(4) + " Lng: " + coords.y.toFixed(4)
})
</script>
</body>
</html>
+194
View File
@@ -0,0 +1,194 @@
/**
* MapProjection 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 mapprojection.html}
*/
export class MapProjection {
/**
* Creates instance of MapProjection
*
* @constructor
* @param {Projection}[projection] - Specifies the projection of the map (e.g. Mercator Projection).
* @param {object}[opts] - Addiditonal options.
* @param {array}[opts.bounds] - Describes the minimum and maximum coordinates on the map in the form of {[[minLat, minLng],[maxLat, maxLng]]}.
* @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.opts.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
}
}
}
}
/**
* The projection used by the map projection.
*
* @member {Projection}
* @readonly
* @memberof MapProjection
*/
get projection() {
return this._projection
}
/**
* Transforms a pixel point on the map to a geographical coordinate.
*
* @public
* @param {Point} point - A pixel position on the map.
* @returns {CoordinatePoint} A geographical coordinate.
* @memberof MapProjection
*/
toCoordinates(point) {
if (this.opts.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
}
/**
* Transform a geographical coordinate to a pixel point on the map.
*
* @public
* @param {CoordinatePoint} coordinates - A point in the form of {x:lat,y:lng}.
* @returns {Point} A pixel position on the map.
* @memberof MapProjection
*/
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
}
/**
* Clipping describes the
* piece of the map that is shown. The returned object contains a min and max value of the clipping in form of: {min: {x,y}, max:{x,y}}. Where x and y are in between 0 and 1.
*
* E.g. if we just show a map of
* europe, then we have to set the clipping properly, otherwise
* the preojection would produce the wrong results when transforming
* from a point to coordinates or the other way around.
*
* @readonly
* @member {object}
* @memberof MapProjection
*/
get clip() {
let unclipped = {
min: { x: 0, y: 0 },
max: { x: 1, y: 1 }
}
return this.opts.clip ? this.opts.clip : unclipped
}
/**
* Returns the biggest viewport the map projection allows.
* This is determined by the projecton or the clipping on the mapapp.
*
* @readonly
* @memberof MapProjection
*/
get maxViewport() {
return this.opts.clip ? this.opts.clip : this.projection.maxViewport
}
}
/**
* Special map projection for DeepZoomMap objects.
*
* Note: It just transform the clipping parameter of the tiles config
* to the clipping of the mapapp.
*
* @export
* @class DeepZoomMapProjection
* @extends {MapProjection}
*/
export class DeepZoomMapProjection extends MapProjection {
constructor(projection, tilesConfig, opts = {}) {
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]
}
}
}
super(projection, opts)
this.app = opts.app
}
}
+414
View File
@@ -0,0 +1,414 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>Maps</title>
<script src="../../3rdparty/highlight/highlight.pack.js"></script>
<link rel="stylesheet" href="../../../fonts/material-icon-font/material-icons.css">
<link rel='stylesheet' href='../../3rdparty/highlight/styles/vs2015.css'>
<link rel='stylesheet' href='../../../css/doctest.css'>
<script src="../../../dist/iwmlib.3rdparty.js"></script>
<script src="../../../dist/iwmlib.js"></script>
<script src="../../../dist/iwmlib.pixi.js"></script>
<style>
.inline-showcase {
display: flex
}
.map-example {
display: inline-block;
width: 256px;
margin: 5px;
}
</style>
</head>
<body onload='Doctest.run()'>
<h1>Maps</h1>
<p>Maps represent a geographical image in a PIXI.Application. Preferably in a MapApp to have more convenient methods
to
handle the maps.</p>
<h2>Cover Test</h2>
<p>Usually maps should cover the whole app, or a window inside the app. Therefore they support a cover option.
This option allows the
</p>
<canvas id="bigMap">
</canvas>
<script class="doctest" data-title="Cover Map" data-collapsible data-collapsed>
(function () {
// Configuration of the deepzoom map.
let osmConfig = {
"projection": "mercator",
"type": "deepzoom",
"tiles": {
"tileSize": 256,
"format": "png",
"overlap": 0,
"type": "map",
"height": 2048,
"width": 2048,
"path": "../assets/maps/osm",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
}
// We may use a focuspoint. This is the point the app
// sets in center on startup.
const Berlin = { x: 52.514961, y: 13.401366 }
let zoom = 3
const app = new MapApp({
view: bigMap,
root: "../",
width: 512,
height: 512,
coordsLogging: true,
focus: Berlin,
zoom
})
let osmMapProjection = new DeepZoomMapProjection(new Projection.Mercator(), osmConfig.tiles, {
app: app
})
let deepZoomMap = new DeepZoomMap(osmMapProjection, Object.assign({}, osmConfig.tiles, { app: app }), { cover: true })
app.setMap("deepzoom", deepZoomMap)
app.setup().run()
return app
})()
</script>
<script>
const Berlin = { x: 52.514961, y: 13.401366 }
function createDeepZoomMap(view, opts, mapOptipns = {}) {
let osmConfig = {
"projection": "mercator",
"type": "deepzoom",
"tiles": {
"tileSize": 256,
"format": "png",
"overlap": 0,
"type": "map",
"height": 2048,
"width": 2048,
"path": "../assets/maps/osm",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
}
opts = Object.assign({
root: "../",
width: 256,
height: 256,
coordsLogging: true,
focus: Berlin
}, opts, { view })
const app = new MapApp(opts)
let osmMapProjection = new DeepZoomMapProjection(new Projection.Mercator(), osmConfig.tiles, {
app: app
})
let deepZoomMap = new DeepZoomMap(osmMapProjection, Object.assign({}, osmConfig.tiles, { app: app }), Object.assign({ cover: false }, mapOptipns))
app.setMap("deepzoom", deepZoomMap)
app.setup().run()
return app
}
function drawLocations(locations, maplayer) {
let locationLayer = new GeoLayer(new PIXI.Container(), { name: "Location Overlay" })
for (key in locations) {
let position = new GeoPoint(locations[key], {
/**
* To style GeoGraphics, the fill has to be set in the
* onDraw callback.
*
* Note: GeoPoints are the only GeoGraphic with no
* initial geometry attached. These have to be drawn to
* the graphics object manually.
*/
onDraw: function () {
this.graphics.beginFill(0xFF0000)
this.graphics.drawCircle(0, 0, 5)
this.graphics.endFill()
}
})
locationLayer.addChild(position)
}
maplayer.addLayer(locationLayer)
}
</script>
<h2>Map Types</h2>
<div class="inline-showcase">
<div class="map-example">
<canvas id="imagemap_canvas"></canvas>
<h3>Image Map</h3>
<p>A map, that is represented by a single image.</p>
</div>
<div class="map-example">
<canvas id="dzi_canvas"></canvas>
<h3>Deep Zoom Map</h3>
<p>The map representation is a DeepZoomImage, that can be scaled much more deeply, without losing quality by
increasing
the amount of tiles, that represent the image.</p>
<p>
You should see a map of the world with a set of cities highlighted with dots. When pressing the button
all views should jump
to the same point.
</p>
</div>
</div>
<script class='doctest' data-collapsible="true" data-collapsed data-title="Image Map">
// Create app.
let imageApp = new MapApp({
view: imagemap_canvas,
focus: Berlin,
width: 256,
height: 256,
coordsLogging: true,
})
// Specify image path.
let europe = "../assets/maps/pixabay/europe.jpg"
// The sprites of the images need to be loaded before initialization by pixi.
// Therefore this loading step is required.
imageApp.loadSprites([
europe
], (sprites) => ready(sprites), { resolutionDependent: false })
// Called when all textures are loaded.
let ready = (sprites) => {
// Define the map projection for the map.
let europeData = new MapProjection(new Projection.Mercator(), {
clip: {
min: { x: 32.863294, y: -18.58 },
max: { x: 57.467973, y: 44.277158 }
}
})
// Create the map using the texture and the map projection.
// Optionally customize the map by supplying secific options.
let imageMap = new ImageMap(sprites.get(europe), europeData, {
cover: false
})
// Set the map inside the app.
imageApp.setMap("europe", imageMap)
}
// Setup and run the app.
imageApp.setup().run()
</script>
<script class='doctest' data-collapsible="true" data-collapsed data-title="Deepzoom Map">
(function () {
// Create or (more commonly) load the data file for the map.
let mapConfig = {
"projection": "mercator",
"type": "deepzoom",
"tiles": {
"tileSize": 256,
"format": "png",
"overlap": 0,
"type": "map",
"height": 1024,
"width": 1024,
"path": "../assets/maps/osm",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
}
// Create the app by providing the canvas element.
const app = new MapApp({
view: dzi_canvas,
root: "../",
width: 256,
height: 256,
coordsLogging: true,
focus: Berlin
})
// Create the deepzoomdata.
let osmMapProjection = new DeepZoomMapProjection(new Projection.Mercator(), mapConfig.tiles, {
app
})
// Create the app.
let deepZoomMap = new DeepZoomMap(osmMapProjection, Object.assign({}, mapConfig.tiles, { app }), { cover: false })
// Set the map in the app.
app.setMap("deepzoom", deepZoomMap)
// Setup and run the app.
app.setup().run()
})()
</script>
<h1>Movement</h1>
<p>
Its crucial to focus certain points in a map application. The following tests the behavioud of maps inside a
mapapp and maps
inside of windows.
</p>
</ul>
<h2>Map Frame</h2>
<p>
The map's frame specifies the stage of the map. When moving to the focus point normally, the map focuses in the
center, the
frame defines the new area, the map will be centered in. This is important when trying to mask a map.
</p>
<canvas id="canvasFrameTest"></canvas>
<div class="controls">
<button id="nextCapital">Next Capital</button>
<input type="number" name="0" id="animationTime" min="0" value="0.35">
<span id="msg"></span>
</div>
<script class="doctest">
(function () {
// Create the app.
let app = createDeepZoomMap(canvasFrameTest, {
width: 512
})
let capitals = {
abidjan: { x: 5.349470, y: -4.006472 },
berlin: { x: 52.525430, y: 13.385291 },
canberra: { x: -35.282025, y: 149.128648 },
capetown: { x: -33.925448, y: 18.416962 },
moscow: { x: 55.750892, y: 37.622799 },
washington: { x: 38.895650, y: -77.031407 },
rio: { x: -22.871400, y: -43.280490 },
tokio: { x: 35.696278, y: 139.731366 }
}
// For debugging purposes we draw a visual
// representation of the frame.
let frame = new PIXI.Graphics()
frame.beginFill(0xFFFFFF, 0.5)
const border = 4
frame.lineStyle(border, 0xff0000, 1)
frame.drawRect(border / 2, border / 2, app.width / 2, app.height / 2)
const focusWidth = 4
frame.endFill()
frame.drawCircle(app.width / 4 + border / 2, app.height / 4 + border / 2, 10)
// For demonstration the frame object is a scatter
// element.
new DisplayObjectScatter(frame, app.renderer)
frame.interactive = true
frame.scatter.position = { x: 50, y: 50 }
// We set the frame to the map.
// Note: when using multiple maps, you should update the
// frame on every mapCHange
app.mapLayer.map.setFrame(frame)
app.scene.addChild(frame)
drawLocations(capitals, app.mapLayer)
// Add change focus event.
let visiting = 0
nextCapital.addEventListener("click", () => {
let keys = Object.keys(capitals)
visiting = (++visiting) % keys.length
let key = keys[visiting]
app.map.moveTo(capitals[key], null, { animate: parseFloat(animationTime.value) })
msg.innerHTML = "Visiting " + key + "."
})
})()
</script>
<script>
// function point_ready(sprites) {
// let maps_0 = ["world", "world2"]
// let sprite = sprites.get(osmworld)
// let worldOSMMap = new ImageMap(sprite, worlOSMdData, {
// cover: false,
// onLoaded: () => {
// worldOSMMap.setFrame(frame)
// }
// })
// pointApp.setMap(maps_0[0], worldOSMMap)
// let world2Map = new ImageMap(sprites.get(world2), world2Data, {
// cover: false,
// startScale: 1,
// onLoaded: function () {
// this.setFrame(frame)
// }
// })
// pointApp.addMap(maps_0[1], world2Map)
// pointApp.pixiLayer.place(frame)
// placeCapitals(pointApp.mapLayer)
// let active_0 = 0
// swapBtn_0.addEventListener("click", () => {
// active_0++
// pointApp.selectMap(maps_0[active_0 % maps_0.length])
// })
// }
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

+163
View File
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en" class="dark-mode">
<head>
<meta charset="UTF-8" />
<title>MapViewport</title>
<script src="../../3rdparty/highlight/highlight.pack.js"></script>
<link rel="stylesheet" href="../../../fonts/material-icon-font/material-icons.css">
<link rel='stylesheet' href='../../3rdparty/highlight/styles/vs2015.css'>
<link rel='stylesheet' href='../../../css/doctest.css'>
<script src="../../../dist/iwmlib.3rdparty.js"></script>
<script src="../../../dist/iwmlib.js"></script>
<script src="../../../dist/iwmlib.pixi.js"></script>
<style>
.controls {
display: flex;
}
</style>
</head>
<body onload="Doctest.run()">
<h1>MapViewport</h1>
<p>
The MapViewport works under the hood of a map layer to track the informations about the current focus point and
zoom position.
This is important to maintain the same view when maps are changed.
</p>
<canvas id="canvas"></canvas>
<div id="mapControl"></div>
<div id="cityControl" class="controls"></div>
<p><strong>WHAT TO SEE:</strong> The map should focus Paris.</p>
<script>
let osmConfig = {
"projection": "mercator",
"type": "deepzoom",
"tiles": {
"tileSize": 256,
"format": "png",
"overlap": 0,
"type": "map",
"height": 1024,
"width": 1024,
"path": "../assets/maps/osm",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
}
let testConfig = {
"projection": "mercator",
"type": "deepzoom",
"tiles": {
"tileSize": 128,
"format": "jpg",
"overlap": 0,
"type": "map",
"height": 4096,
"width": 4096,
"path": "../assets/maps/test",
"urlTileTemplate": "{path}/{level}/{row}/{column}.{format}"
}
}
</script>
<script class="doctest">
let capitals = {
london: { x: 51.5, y: -0.083333 },
rome: { x: 41.9, y: 12.483333 },
madrid: { x: 40.4, y: -3.683333 },
paris: { x: 48.833986, y: 2.346989 }
}
// You may define a focus point ...
let focus = capitals.paris
// ... and a zoom level.
let zoom = 1
// Name has to be app (like all other PIXIApps).
const app = (window.app = new MapApp({
focus,
zoom,
view: canvas,
coordsLogging: true,
width: 512,
height: 512
}))
// As map an image of europe is used.
let europe = '../assets/maps/pixabay/europe.jpg'
//Preload all required sprites for the image map.
app.loadSprites([europe], sprites => ready(sprites), {
resolutionDependent: false
})
// The map projection object contains informations,
// how the displayed map has to be interpreted.
// e.g. which projection is used or how the
// image is clipped.
let europeData = new MapProjection(new Projection.Mercator(), {
clip: {
min: { x: 32.863294, y: -18.58 },
max: { x: 57.467973, y: 44.277158 }
}
})
function ready(sprites) {
const cover = true
// When resources are loaded, the ImageMap can be instantiated.
let imageMap = new ImageMap(sprites.get(europe), europeData, {
coordsLogging: true,
maxScale: 1,
cover
})
let testMapProjection = new DeepZoomMapProjection(new Projection.Mercator(), testConfig.tiles, {
app
})
let testMap = new DeepZoomMap(testMapProjection, Object.assign({}, testConfig.tiles, { app }), { cover })
app.addMap("test", testMap)
let osmMapProjection = new DeepZoomMapProjection(new Projection.Mercator(), osmConfig.tiles, {
app
})
let deepZoomMap = new DeepZoomMap(osmMapProjection, Object.assign({}, osmConfig.tiles, { app }), { cover })
app.addMap("osm", deepZoomMap)
// Finally apply the map to the MapApp
app.setMap('europe', imageMap)
// The app requires a map before beeing able to run.
// So start the app here.
app.setup().run()
for (let [key, val] of Object.entries(app.mapList.maps)) {
let mapBtn = document.createElement("button")
mapBtn.innerText = key
mapBtn.addEventListener("click", () => {
app.mapLayer.changeMap(val)
})
mapControl.appendChild(mapBtn)
}
}
for (let [key, val] of Object.entries(capitals)) {
let cityBtn = document.createElement("button")
cityBtn.innerText = key
cityBtn.addEventListener("click", () => {
app.mapLayer.map.moveTo(val)
})
cityControl.appendChild(cityBtn)
}
</script>
</body>
</html>
+177
View File
@@ -0,0 +1,177 @@
import { DeepZoomMap } from './map.js'
/**
* The MapViewport class is responsible for a consistent map view.
* It is aware of the current viewposition, the scale and viewport.
* It ensures, that maps can be changed, without the user noticing it.
*
*/
export default class MapViewport {
/**
*
* @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 MapViewport will prioritize a full scale app, than displaying the demanded zoom factor
*/
constructor({ 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.referenceHeight = 256
}
/**
* The current focus point as map coordinates.
*
* @member {CoordinatePoint}
* @readonly
* @memberof MapViewport
*/
get focus() {
return this._focus
}
/**
* The current zoom distance.
* On DeepZoomMaps this is equal to the zoom level.
*
* @member {number}
* @readonly
* @memberof MapViewport
*/
get zoom() {
return this._zoom
}
/**
* Applies the current view to a map.
*
*
* @param {GeoMap} map - Map the viewport should be applied to.
* @memberof MapViewport
*/
apply(map) {
map.moveTo(this._focus, this._zoom)
}
/**
* Update the focus point and zoom according to the map.
* This is commonly called when the map is transformed.
*
* @param {GeoMap} map - Map to update.
* @memberof MapViewport
*/
update(map) {
this.updateZoom(map)
this.updateFocusPoint(map)
}
/**
* Updates the focus point.
* This is automatically called when calling update.
*
* @param {GeoMap} map
* @memberof MapViewport
*/
updateFocusPoint(map) {
const frame = map.getFrame()
this._focus = this.coordinatesFromWindowPoint(map, frame.localCenter)
}
/**
* Updates the zoom.
* This is automatically called when calling update.
*
* @param {GeoMap} map
* @memberof MapViewport
*/
updateZoom(map) {
/**
* TODO: This relies on the fact, that all maps have the same tileSize,
* if a set would have a smaller tileSize. Improve that.
*/
if (map instanceof DeepZoomMap) this._zoom = map.floatingLevelForScale(map.image.scatter.scale)
else {
this._zoom = map.zoom
}
}
/**
* Transforms a position on the map to a position in the window.
*
* Inverse of windowPointToMapPoint.
*
* @param {GeoMap} map - Map to calculate the position on.
* @param {Point} point - Position on the map in pixels.
* @returns {Point} - Pixel position in the window.
* @memberof MapViewport
*/
mapPointToWindowPoint(map, point) {
let windowPoint = { x: 0, y: 0 }
if (map['image'] && map.image['parent']) {
let container = map.image.parent
windowPoint = new PIXI.Point(
map.scatter.position.x + map.scatter.scale * point.x,
map.scatter.position.y + map.scatter.scale * point.y
)
windowPoint = container.toGlobal(windowPoint)
} else {
console.error(this._noParentError)
}
return windowPoint
}
/**
* Transforms in the window to a position on the map.
*
* Inverse of mapPointToWindowPoint(map, point) {.
*
* @param {GeoMap} map - Map to calculate the position on.
* @param {Point} point -Pixel position in the window.
* @returns {Point} - Position on the map in pixels.
* @memberof MapViewport
*/
windowPointToMapPoint(map, point) {
let pointOnMap = { x: 0, y: 0 }
if (map['image'] && map.image['parent']) {
let offset = map.image.parent.toGlobal({ x: 0, y: 0 })
pointOnMap = new PIXI.Point(
(point.x - map.scatter.position.x - offset.x) / map.scatter.scale,
(point.y - map.scatter.position.y - offset.y) / map.scatter.scale
)
} else console.error(this._noParentError)
return pointOnMap
}
get _noParentError() {
return 'Cannot compute map point when map has no parent.'
}
/**
* 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(map, point) {
let position = {
x: point.x - map.scatter.position.x,
y: point.y - map.scatter.position.y
}
let normalized = {
x: position.x / (map.width * map.scatter.scale),
y: position.y / (map.height * map.scatter.scale)
}
let coordinates = map.mapProjection.toCoordinates(normalized)
return coordinates
}
}
+304
View File
@@ -0,0 +1,304 @@
<!DOCTYPE html>
<html lang="en" class="dark-mode">
<head>
<meta charset="UTF-8" />
<title>Overlay</title>
<script src="../../3rdparty/highlight/highlight.pack.js"></script>
<link rel="stylesheet" href="../../../fonts/material-icon-font/material-icons.css">
<link rel='stylesheet' href='../../3rdparty/highlight/styles/vs2015.css'>
<link rel='stylesheet' href='../../../css/doctest.css'>
<script src="../../../dist/iwmlib.3rdparty.js"></script>
<script src="../../../dist/iwmlib.js"></script>
<script src="../../../dist/iwmlib.pixi.js"></script>
<link rel="shortcut icon" type="image/x-icon" href="../../../assets/icons/map.png">
<style>
.inline-showcase {
display: flex;
}
.map-example {
display: inline-block;
width: 256px;
margin: 5px;
}
</style>
</head>
<body onload="Doctest.run()">
<h1 class="title">Overlay</h1>
<!-- <a href="../../../" class="Documentation"></a> -->
<p class="description">
The overlayclass creates a convenient way to create and design
complex map overlays.
</p>
<canvas id="view" class="center"> </canvas>
<script>
let app = new MapApp({
view,
width: 512,
height: 512,
coordsLogging: true
})
var osmworld = '../assets/maps/osm/0/0/0.png'
let worlOSMData = new MapProjection(new Projection.Mercator())
function setupMap(textures) {
// Create the map!
let map = new ImageMap(
new PIXI.Sprite(textures.get(osmworld)),
worlOSMData
)
// Setup the map in the mapapp.
app.setMap('osm', map)
app.setup().run()
}
</script>
<script class="doctest" data-collapsible data-collapsed data-title="Overlay Data">
/**
* To create an overlay you just need to create a JSON file.
* The styles defined in the styles are cascading, meaning that
* the styles can be defined on the top level, or directly at the
* single items. If the style is not defined in the item, the top-level style will be used.
* If the top-level style is not defined either, the default style is applied.
*
* The structure of an overlay file is as follows:
*
* {
* //COMMENT: Top-Level
*
* //COMMENT: Frequently used properties are:
* size: 500,
* color: 0xFF00FF,
* fillAlpha:0.5
*
* icon: 'path/to/icon.png'
* iconColor: 0xFFFFFF,
* "items": [
* //COMMENT: Items are defined in the items array.
* {
* location: {
* //COMMENT: Location must be defined.
* x: LAT_VALUE,
* y: LNG_VALUE
* }
* },{
* color: 0xFF0000,
* location: {
* //COMMENT: Location must be defined.
* x: LAT_VALUE,
* y: LNG_VALUE
* }
* }
* ]
* }
*/
let exampleOverlayJSON = {
icon: '../../../assets/icons/place.png',
iconColor: "0x35aaea",
iconAnchor: { x: 0.5, y: 1 },
size: 5,
scale: 0.2,
disabledColor: 0x000000,
disabledIconColor: 0xCCCCCC,
disabledScale: 0.5,
color: '0x3FA7EE',
items: [
{
name: 'Custom Icon',
fontWeight: "bold",
icon: '../../../assets/icons/beenhere.png',
iconColor: 0x00ff00,
iconAlpha: 0.5,
size: 0,
labelVerticalAlignment: "underneath",
label: 'Abidjan',
location: {
x: 5.34947,
y: -4.006472
},
information:
'Here a custom icon is used. It overrides the icon setting in the global section.'
},
{
name: 'Berlin',
label: "enabled",
disabledLabel: "disabled",
location: {
x: 52.52543,
y: 13.385291
},
information:
'... ist die Bundeshauptstadt der Bundesrepublik Deutschland.',
enabled: false
},
{
name: 'Canberra',
location: {
x: -35.282025,
y: 149.128648
},
information:
'... ist die Hauptstadt und achtgrößte Stadt Australiens.'
},
{
name: 'Kapstadt',
location: {
x: -33.925448,
y: 18.416962
},
information:
`This item adjusts it's size according to the map.`
},
{
name: 'Moskau',
location: {
x: 55.750892,
y: 37.622799
},
information:
'... die kosmopolitische Hauptstadt Russlands, liegt im Westen des Landes und wird von der Moskwa durchflossen.'
},
{
name: 'Washington, D.C.',
location: {
x: 38.89565,
y: -77.031407
},
information:
'... ist die Hauptstadt der USA und eine kompakte Stadt am Potomac River, die an die Bundesstaaten Maryland und Virginia grenzt.'
},
{
name: 'Rio de Janeiro',
location: {
x: -22.8714,
y: -43.28049
},
information:
'... ist eine ausgedehnte brasilianische Küstenmetropole. '
},
{
name: 'Tokio',
type: "factory",
label: "factory",
location: {
x: 35.696278,
y: 139.731366
},
information:
'... ist eine Global City in der Kantō-Region im Osten der japanischen Hauptinsel Honshū.'
}
]
}
</script>
<script class="doctest" data-collapsible data-title="Overlay">
let overlay = new Overlay(exampleOverlayJSON)
/**
* Textures should be loaded using the app's texture loader.
* With the findTexture method all required Textures within the
* overlays are loaded.
*/
let overlayTextures = overlay.findAllTextures()
let list = [osmworld].concat(overlayTextures)
app.loadTextures(list, textures => texturesLoaded(textures), {
resolutionDependent: false
})
function texturesLoaded(textures) {
/** When all textures are loaded .... */
setupMap(textures)
//Retrieve all overlay textures.
overlay.selectTextures(textures)
// Just some Helpers for the Popups.
let popup = null
let cleaner = null
const vanishingTime = 1000
// Factories must return a geographics object.
Overlay.createFactory("factory", (item) => {
// This is applied to every item in the overlay that has
// the type factory'
let geographics = new GeoPoint(item.location)
geographics.drawHandler.add((graphics) => {
graphics.beginFill(item.color, item.fillAlpha)
graphics.drawRect(0, 0, 10, 10)
})
let text = new PIXI.Text(item.name, {fontSize: 5})
geographics.graphics.addChild(text)
return geographics
})
/**
*
* The actual PIXI elements are created when the overlay.create() is called.
* This returns an GeoLayer which can be directly put onto the map's Maplayer (or any other GeoLayer).
*
* INFO: MapLayer and GeoLayer are specialized container for PIXI. They are meant to
* place PIXI Elements according to their world-coordinates (lat/lng position), instead
* of actual pixel values.
*
*/
let exampleOverlayGeoLayer = overlay.create()
// When placed on the mapLayer, the PIXI Graphic elements, that
// reside inside GeoGraphic Objects are placed automatically at the
// correct coordinates of the map.
app.mapLayer.addLayer(exampleOverlayGeoLayer)
// Just a helper function that clears the popups and removes
// a remaining cleaner timeout.
function clearPopup() {
if (popup) {
popup.parent.removeChild(popup)
popup = null
}
if (cleaner != null) {
clearTimeout(cleaner)
}
}
}
</script>
<script>
</script>
</body>
</html>
+775
View File
@@ -0,0 +1,775 @@
import { GeoLayer } from './geolayer.js'
import { GeoPoint, GeoMultiShape, GeoGraphics } 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
* @class Overlay
*
*
* @param {object} options
* @param {object} options.name Name of the item.
* @param {object} options.borderColor
* @param {object} options.borderThickness
* @param {object} options.enabled Determines if enabled, disabled when false.
* @param {object} options.labelScale
* @param {object} options.location
* @param {object} options.label
* @param {object} options.labelType
* @param {object} options.labelLocation
* @param {object} options.labelAlignment
* @param {object} options.labelVerticalAlignment
* @param {object} options.labelSpacing
* @param {object} options.labelTarget
* @param {object} options.geometry
* @param {object} options.information
* @param {object} options.informationPath
* @param {object} options.informationType
* @param {object} options.icon
* @param {object} options.iconAlpha
* @param {object} options.iconAnchor
* @param {object} options.iconColor
* @param {object} options.iconScale
* @param {object} options.iconOffset
* @param {object} options.items
* @param {object} options.color Color of te point.
* @param {object} options.fillAlpha FillAlpha of the point.
* @param {object} options.size Size of the point.
* @param {object} options.scale Determines the scale of the graphics.
* @param {object} options.rescale Rescales the graphics relative to the map, that they keep their original scale. Only works for the entire overlay atm. Therefore must be set in the global param eters and not inside the children.
* @param {object} options.popoverOffset
* @param {object} options.zoomVisibility
* @param {object} options.labelMultiLineAlign
* @param {object} options.labelBreakWords
* @param {object} options.labelDropShadow
* @param {object} options.labelDropShadowAlpha
* @param {object} options.labelDropShadowAngle
* @param {object} options.labelDropShadowBlur
* @param {object} options.labelDropShadowColor
* @param {object} options.labelDropShadowDistance
* @param {object} options.fontColor
* @param {object} options.fontFamily
* @param {object} options.fontSize
* @param {object} options.fontStyle
* @param {object} options.fontWeight
* @param {object} options.labelStroke
* @param {object} options.labelStrokeThickness
* @param {object} options.wordWrap
* @param {object} options.wordWrapWidth
* @param {object} options.labelFill
* @param {object} options.labelFillGradientType
* @param {object} options.labelFillGradientStops
* @param {object} options.fontVariant
* @param {object} options.labelLeading
* @param {object} options.letterSpacing
* @param {object} options.labelLineHeight
* @param {object} options.labelLineJoin
* @param {object} options.labelMiterLimit
* @param {object} options.labelPadding
* @param {object} options.labelTrim
* @param {object} options.textBaseline
* @param {object} options.labelWhiteSpace
*/
export default class Overlay {
constructor(options = {}) {
let defaultTextStyle = new PIXI.TextStyle()
let defaultOptions = {
type: 'auto',
name: 'unnamed',
borderColor: 0x000000,
borderThickness: 5,
enabled: true,
labelScale: 1,
location: null,
label: false,
labelType: null,
labelLocation: null,
labelAlignment: 'center',
labelVerticalAlignment: '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 },
zoomVisibility: { min: 0, max: Number.MAX_VALUE },
/**
* 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
}
defaultOptions = this.addDisabled(defaultOptions)
console.log('DISABLED VERTICALA.', defaultOptions.disabledLabelVerticalAlignment)
options = this.addDisabled(options)
options = Object.assign({}, defaultOptions, options)
Object.assign(this, options)
}
/**
* Some parameters shall not have a disabled option.
* These are defined here.
*
* @readonly
* @memberof Overlay
*/
get excludedDisableParameters() {
return ['items', 'rescale', 'name', 'type', 'enabled']
}
/**
* Copies the normal properties to the disabled version of the property,
* while conserving already set disabled properties.
*
* @param {*} options
* @returns {object} - Returns the modified options object.
* @memberof Overlay
*/
addDisabled(options) {
for (let [key, value] of Object.entries(options)) {
if (this.excludedDisableParameters.indexOf(key) == -1) {
let disabledProperty = this.toDisabledPropertyString(key)
if (options[disabledProperty] == undefined) {
options[disabledProperty] = value
}
}
}
return options
}
/**
* Textures need to be loaded by the app. Texture parameters return all parameters that need to be evaluated for textures.
* @readonly
* @member {array}
* @memberof Overlay
*/
get textureParameters() {
const textureParameters = ['icon']
textureParameters.forEach(textureParameter => {
textureParameters.push(this.toDisabledPropertyString(textureParameter))
})
return textureParameters
}
/**
* The required textures are saved inside the single items.
* Their texture string gets replaced by a texture object
* containing the path and the texture.
*
* @param {*} parameter
* @param {*} key
* @param {*} path
* @memberof Overlay
*/
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
})
}
/**
* Recursively calls a function on child items.
*
* @param {*} parameter
* @param {*} func
* @param {*} [obj=null]
* @memberof Overlay
*/
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,
cleanupItems = null
} = {}) {
console.log(this)
const name = this.name
? this.name[0].toUpperCase() + this.name.slice(1).toLowerCase() + ' Overlay'
: 'Unnamed Overlay'
let geoLayer = new GeoLayer(new PIXI.Container(), { name })
geoLayer.visibility = this.zoomVisibility
if (this.rescale) geoLayer.rescale = this.rescale
this.items.forEach(item => {
if (!excludeItems(item)) {
//Copies all values to a disabled state.
item = this.addDisabled(item)
if (adjustItems) {
adjustItems(item)
}
item.overlay = this
let graphics = this.createItem(item, informationCallback)
geoLayer.addChild(graphics)
if (cleanupItems) {
cleanupItems(item)
}
}
})
return geoLayer
}
/**
* Used to pick a property or disabled property using an enabled parameter.
*
* @param {*} item
* @param {*} property
* @param {boolean} [enabled=true]
* @returns {any} - Returns the picked item property.
* @memberof Overlay
*/
pickItemProperty(item, property, enabled = true) {
return enabled ? this.getItemProperty(item, property) : this.getDisabledItemProperty(item, property)
}
toDisabledPropertyString(propertyName) {
const prefix = 'disabled'
return prefix + propertyName[0].toUpperCase() + propertyName.slice(1)
}
/**
* Get's the disabled version of the property as long as it is not excluded.
*
* @param {*} item
* @param {*} property
* @returns {any} - Returns the picked item property.
* @memberof Overlay
*/
getDisabledItemProperty(item, property) {
if (this.excludedDisableParameters.indexOf(property) === -1) {
property = this.toDisabledPropertyString(property)
}
const propertyValue = this.getItemProperty(item, property)
return propertyValue
}
/**
* Tries to get the infromation from an overlay item.
* If the item does not contain the property, the overlay definition will be used.
* When the overlay definition is not set, the default value is used.
*
* @param {OverlayItem} item - Item to get the informations from.
* @param {string} property - Name of the property.
* @returns {any} - Returns the picked item property.
* @memberof Overlay
*/
getItemProperty(item, property) {
let propertyValue = null
const propertyExistsOnItem = item[property] !== undefined
const propertyExistsOnOverlay = this[property] !== undefined
if (property == 'disabledLabelVerticalAlignment') {
console.log(this)
console.log(this['disabledLabelVerticalAlignment'], propertyExistsOnItem, propertyExistsOnOverlay)
}
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
}
/**
* An overlay can have a reference to another file.
* That single overlays are more easy to maintain.
*
* @param {*} item
* @param {*} property
* @returns {any} - Returns the reference as object or the property.
* @memberof Overlay
*/
_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
}
}
})
})
}
/**
* Creates a geographic for each item.
* By default it uses the regular routine but you may
* specify a factory for the defined type.
*
* @param {*} item
* @param {*} [informationCallback=null]
* @returns {GeoGraphics} - Returns the created geographics.
* @memberof Overlay
*/
createItem(item, informationCallback = null) {
let geographics
let type = this.getItemProperty(item, 'type')
/**
* Use a factory to draw the items, if a type is specified.
*/
if (type != 'auto') {
let geographicsFactory = Overlay.requestFactory(type)
if (!geographicsFactory)
console.error(
'Invalid Overlay Mode: ' +
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)
}
let label = this._createLabel(item, geographics)
if (label != null && geographics != null) {
console.log(geographics)
geographics.graphics.addChild(label)
}
}
return geographics
}
/**
*
* Draws a label that is shown next to the item on the map.
*
* @param {*} item
* @returns {PIXI.Text} - Returns the created label or null.
* @memberof Overlay
*/
_createLabel(item) {
let enabled = this.getItemProperty(item, 'enabled')
let labelText = this.pickItemProperty(item, 'label', enabled)
let label = null
if (labelText) {
let textStyle = this._gatherFontStyle(item)
label = new PIXI.Text(labelText, textStyle)
let labelScale = this.pickItemProperty(item, 'labelScale', enabled)
label.scale.set(labelScale, labelScale)
let position = new PIXI.Point()
let align = this.pickItemProperty(item, 'labelAlignment', enabled)
if (align == 'left');
else if (align == 'center') position.set(label.position.x - label.width / 2, label.position.y)
else if (align == 'right') position.set(label.position.x - label.width, label.position.y)
else this._logPropertyNotImplemented('labelAlignment', align)
let verticalAlign = this.pickItemProperty(item, 'labelVerticalAlignment', enabled)
if (verticalAlign == 'underneath') {
let size = this.pickItemProperty(item, 'size', enabled)
let scale = this.pickItemProperty(item, 'scale', enabled)
let labelSpacing = this.pickItemProperty(item, 'labelSpacing', enabled)
position.y += size * scale + labelSpacing
} else if (verticalAlign == 'above') {
let size = this.pickItemProperty(item, 'size', enabled)
let scale = this.pickItemProperty(item, 'scale', enabled)
let labelSpacing = this.pickItemProperty(item, 'labelSpacing', enabled)
position.y -= size * scale + label.height + labelSpacing
} else this._logPropertyNotImplemented('labelVerticalAlignment', verticalAlign)
label.position.set(position.x, position.y)
}
return label
}
_gatherFontStyle(item) {
const enabled = this.getItemProperty(item, 'enabled')
return {
align: this.pickItemProperty(item, 'labelMultiLineAlign', enabled),
breakWords: this.pickItemProperty(item, 'labelBreakWords', enabled),
dropShadow: this.pickItemProperty(item, 'labelDropShadow', enabled),
dropShadowAlpha: this.pickItemProperty(item, 'labelDropShadowAlpha', enabled),
dropShadowAngle: this.pickItemProperty(item, 'labelDropShadowAngle', enabled),
dropShadowBlur: this.pickItemProperty(item, 'labelDropShadowBlur', enabled),
dropShadowColor: this.pickItemProperty(item, 'labelDropShadowColor', enabled),
dropShadowDistance: this.pickItemProperty(item, 'labelDropShadowDistance', enabled),
fontFamily: this.pickItemProperty(item, 'fontFamily', enabled),
fontSize: this.pickItemProperty(item, 'fontSize', enabled),
fontStyle: this.pickItemProperty(item, 'fontStyle', enabled),
fontWeight: this.pickItemProperty(item, 'fontWeight', enabled),
stroke: this.pickItemProperty(item, 'labelStroke', enabled),
strokeThickness: this.pickItemProperty(item, 'labelStrokeThickness', enabled),
wordWrap: this.pickItemProperty(item, 'wordWrap', enabled),
wordWrapWidth: this.pickItemProperty(item, 'wordWrapWidth', enabled),
fill: this.pickItemProperty(item, 'labelFill', enabled),
fillGradientType: this.pickItemProperty(item, 'labelFillGradientType', enabled),
fillGradientStops: this.pickItemProperty(item, 'labelFillGradientStops', enabled),
fontVariant: this.pickItemProperty(item, 'fontVariant', enabled),
leading: this.pickItemProperty(item, 'labelLeading', enabled),
letterSpacing: this.pickItemProperty(item, 'letterSpacing', enabled),
lineHeight: this.pickItemProperty(item, 'labelLineHeight', enabled),
lineJoin: this.pickItemProperty(item, 'labelLineJoin', enabled),
miterLimit: this.pickItemProperty(item, 'labelMiterLimit', enabled),
padding: this.pickItemProperty(item, 'labelPadding', enabled),
trim: this.pickItemProperty(item, 'labelTrim', enabled),
textBaseline: this.pickItemProperty(item, 'textBaseline', enabled),
whiteSpace: this.pickItemProperty(item, 'labelWhiteSpace', enabled)
}
}
_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 enabled = this.getItemProperty(item, 'enabled')
let icon = this.pickItemProperty(item, 'icon', enabled)
if (icon) {
if (icon.texture) {
let sprite = new PIXI.Sprite(icon.texture)
const iconAnchor = this.pickItemProperty(item, 'iconAnchor', enabled)
sprite.anchor.set(iconAnchor.x, iconAnchor.y)
const iconScale = this.pickItemProperty(item, 'iconScale', enabled)
if (iconScale) sprite.scale.set(iconScale, iconScale)
const iconOffset = this.pickItemProperty(item, 'iconOffset', enabled)
if (iconOffset && iconOffset.x != null && iconOffset.y != null)
sprite.position.set(iconOffset.x, iconOffset.y)
const iconColor = this.pickItemProperty(item, 'iconColor', enabled)
if (iconColor) sprite.tint = iconColor
const iconAlpha = this.pickItemProperty(item, 'iconAlpha', enabled)
if (iconAlpha) sprite.alpha = iconAlpha
geographics.graphics.addChild(sprite)
}
}
}
_createInformation(geographics, item, callback = null) {
if (item.information) {
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
})
if (geographics.map) {
const scale = 1 / geographics.map.scatter.scale
popup.scale.set(scale, scale)
}
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 = this.pickItemProperty(item, 'color', enabled)
const alpha = this.pickItemProperty(item, 'fillAlpha', enabled)
geographics.graphics.beginFill(color, alpha)
}
_drawPoint(item, informationCallback = null) {
const overlay = this
const enabled = this.getItemProperty(item, 'enabled')
let that = this
let geographic = new GeoPoint(item.location, {
onDraw: function() {
overlay._fill.call(overlay, this, item)
const size = that.pickItemProperty(item, 'size', enabled)
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 }
/**
* Creates a factory for a specific type.
*
* @param {string} type - Name of the factory. When overlay items match the type, the factory is applied.
* @param {function} geographicsFactory - A function that is called with every item. Can be used to draw the geographics individually.
*
* @static
* @memberof {Overlay}
*/
Overlay.createFactory = function(type, geographicsFactory) {
if (Overlay.Modes.hasOwnProperty(type))
console.warn(`The mode ${type} was already implemented and was overwritten!`)
Overlay.Modes[type] = 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
}
@@ -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) }
}
}
@@ -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 function in ' + this.name + '.')
}
toString() {
return 'Projection (abstract)'
}
get name() {
return this.toString()
}
get maxViewport() {
return { min: new PIXI.Point(0, 0), max: new PIXI.Point(1, 1) }
}
}
@@ -0,0 +1,375 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Projections</title>
<script src="../../../3rdparty/highlight/highlight.pack.js"></script>
<link rel="stylesheet" href="../../../../fonts/material-icon-font/material-icons.css">
<link rel='stylesheet' href='../../../3rdparty/highlight/styles/vs2015.css'>
<link rel='stylesheet' href='../../../../css/doctest.css'>
<script src="../../../../dist/iwmlib.3rdparty.js"></script>
<script src="../../../../dist/iwmlib.js"></script>
<script src="../../../../dist/iwmlib.pixi.js"></script>
<style>
section {
display: flex;
flex-direction: column;
}
.map-example {
display: inline-block;
width: 256px;
margin: 5px;
}
.map-wrapper {
position: relative;
/* inline-block create additional space around child elements. */
display: inline-flex;
margin: 100px;
}
.long {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background-color: yellowgreen;
}
.point {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: absolute;
width: 5px;
height: 5px;
background-color: red;
border-radius: 50%;
}
.small.point {
width: 2px;
height: 2px;
}
.medium.point {
width: 4px;
height: 4px;
opacity: 0.5;
}
.line {
position: absolute;
width: 100%;
left: 0;
border: 0.5px solid red;
}
</style>
</head>
<body onload="Doctest.run()">
<h1>Projections</h1>
<p>
Projections are used on the map projection to translate coordinates to pixelpositions. There are various
projections that can be used. All implemented ones are showcased here.
</p>
<section id="mercator">
<h2>Mercator Projection</h2>
<p>The most commonly used projection is the mercator projection.</p>
<div class="center">
<div id="mercatorMap" class="map-wrapper">
<img src="../../assets/maps/osm/0/0/0.png" alt="" />
</div>
</div>
<script>
window.examples = []
let boundaries = [
{ x: -90, y: -180 },
{ x: 90, y: 180 },
{ x: 0, y: 0 },
{ x: 90, y: -180 },
// Eastern Boundaries
{ x: -22, y: 180 },
{ x: -43, y: 180 },
{ x: -63, y: 180 },
{ x: -73, y: 180 },
{ x: -83, y: 180 },
{ x: -87, y: 180 },
{ x: 0, y: 180 },
{ x: 22, y: 180 },
{ x: 43, y: 180 },
{ x: 63, y: 180 },
{ x: 73, y: 180 },
{ x: 83, y: 180 },
{ x: 87, y: 180 },
{ x: -90, y: 180 },
// Western Boundaries
{ x: -22, y: -180 },
{ x: -43, y: -180 },
{ x: -63, y: -180 },
{ x: -73, y: -180 },
{ x: -83, y: -180 },
{ x: -87, y: -180 },
{ x: 0, y: -180 },
{ x: 22, y: -180 },
{ x: 43, y: -180 },
{ x: 63, y: -180 },
{ x: 73, y: -180 },
{ x: 83, y: -180 },
{ x: 87, y: -180 },
{ x: -90, y: -180 }
]
let capitals = {
abidjan: { x: 5, y: -5 },
canberra: { x: -35.312146, y: 149.121539 },
berlin: { x: 52.52543, y: 13.385291 },
capetown: { x: -33.925448, y: 18.416962 },
moscow: { x: 55.750892, y: 37.622799 },
washington: { x: 38.89565, y: -77.031407 },
rio: { x: -22.8714, y: -43.28049 },
tokio: { x: 35.696278, y: 139.731366 }
}
/**
*
*/
function createPointAtPoisition(position, className = '', style = {}) {
let point = document.createElement('div')
point.className = 'point ' + className
Object.assign(point.style, {
backgroundColor: "0xff0000"
}, style, {
left: position.x * 100 + '%',
top: position.y * 100 + '%'
})
return point
}
/**
* Draws a vertical line at the zero longitude of a map.
*/
function drawZeroLongitude(projection, parent) {
let element = document.createElement("div")
element.className = "long"
let position = projection.forward({ x: 0, y: 0 })
element.style.left = position.x * 100 + "%"
parent.appendChild(element)
}
/**
* Plots a point for a specific interval on the map. Distance defines that interval.
*/
function plotGridPoints(distance = 10, projection, parent) {
for (let lat = -90; lat <= 90; lat += distance) {
for (let lng = -180; lng <= 180; lng += distance) {
let color = "#0000ff"
let className = 'small'
if (lng == 180) {
color = '#ff0000'
lng = (lng == 180) ? 180 : lng
}
if (lng == -180) {
color = '#00ff00'
lng = (lng == 180) ? 180 : lng
className = "medium"
}
let relativePosition = projection.forward({ x: lat, y: lng })
let point = createPointAtPoisition(relativePosition, className, {
backgroundColor: color
})
robinsonMap.appendChild(point)
}
}
}
</script>
<script class="doctest" data-collapsible data-collapsed data-title="Test Data">
let mercatorTruth = {
abidjan: { x: 0.4861111111111111, y: 0.4860934491519468 },
canberra: { x: 0.9142264972222222, y: 0.6049626977761389 },
berlin: { x: 0.5371813638888889, y: 0.327930370913594 },
capetown: { x: 0.5511582277777778, y: 0.6002818272799257 },
moscow: { x: 0.604507775, y: 0.3126267957774014 },
washington: { x: 0.28602386944444447, y: 0.3825518808752881 },
rio: { x: 0.3797764166666667, y: 0.5652894059707362 },
tokio: { x: 0.8881426833333332, y: 0.39372656277794527 }
}
</script>
<script class="doctest">
; (function () {
//First we define the appropriate projection:
let mercatorProjection = new Projection.Mercator()
for (let [name, coordinates] of Object.entries(capitals)) {
// Every projection has a forward and a backward function.
// The forward function transforms coordinates in the form of
// {x: lat, y: lng} into relative screen coordinates in the form of {x,y}.
let relativePosition = mercatorProjection.forward(coordinates)
// Testcases for the forward transformation.
Doctest.expect(mercatorTruth[name], relativePosition)
// The backward function transforms relative positions on the screen
// to geographic coordinates.
let calculatedCoordinates = mercatorProjection.backward(relativePosition)
// Testcases for the backwards transformation.
Doctest.expectPointPrecision(coordinates, calculatedCoordinates)
let point = createPointAtPoisition(relativePosition)
mercatorMap.appendChild(point)
window.examples.push({ map: mercatorMap, projection: mercatorProjection })
}
})()
</script>
</section>
<section id="robinson">
<h2>Robinson Projection</h2>
<p>
The robinson projection is a more 'artistic' approach, as it follows a table instead of a strict
formula. It has severe distortions at the poles, but that distortion declines rapidly heading towards
the equator.
</p>
<div class="center">
<div id="robinsonMap" class="map-wrapper">
<img src="../../assets/maps/wikimedia-world-robinson/2000px-BlankMap-World.png" alt="" width="512" />
</div>
</div>
<script class="doctest" data-collapsible data-collapsed data-title="Test Data">
let robinsonTruth = {
"abidjan": {
"x": 0.45839166666666664,
"y": 0.469
},
"canberra": {
"x": 0.8637961558795233,
"y": 0.71892906228
},
"berlin": {
"x": 0.5080051098978837,
"y": 0.17622420010000006
},
"capetown": {
"x": 0.522127677557207,
"y": 0.7103377776
},
"moscow": {
"x": 0.5636501036295355,
"y": 0.15721735316000007
},
"washington": {
"x": 0.27607293856102144,
"y": 0.25892488299999994
},
"rio": {
"x": 0.35541500995592845,
"y": 0.6418026800000001
},
"tokio": {
"x": 0.8386571387301243,
"y": 0.27869700196
}
}
</script>
<script class="doctest">
; (function () {
// Create the robinson projection.
let robinsonProjection = new Projection.Robinson(10)
// Note: The center on this map is at approximately latitude 10,
// therefore the projection must be shifted.
/**
* Create a point for each capital.
*/
for (let [key, position] of Object.entries(capitals)) {
let relativePosition = robinsonProjection.forward(position)
// Run Test Cases
Doctest.expectPointPrecision(robinsonTruth[key], relativePosition, 0)
let point = createPointAtPoisition(relativePosition)
robinsonMap.appendChild(point)
}
window.examples.push({ projection: robinsonProjection, map: robinsonMap })
})()
</script>
</section>
<script>
// Put it in a self executing anonymous function to not pollute the window element.
(function () {
window.examples.forEach(({ map, projection }) => {
let display = document.createElement("p")
Object.assign(display.style, {
position: "absolute",
left: 0,
top: 0
})
map.parentNode.appendChild(display)
Object.assign(map.parentNode.style, {
position: "relative"
})
display.innerText = "Hover over Map to display coordinates."
map.addEventListener("mousemove", (event) => {
let mousePosition = { x: event.offsetX, y: event.offsetY }
let normalizedPosition = {
x: mousePosition.x / map.offsetWidth,
y: mousePosition.y / map.offsetHeight
}
let coordinates = projection.backward(normalizedPosition)
display.innerHTML = `<b>lat:</b> ${coordinates.x.toFixed(3)} <br><b>lng:</b> ${coordinates.y.toFixed(3)}`
})
})
})()
</script>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
import Projection from './projection'
/* */
export default class Robinson extends Projection {
constructor(lng = 0) {
super()
this.lng0 = lng
this.lengthOfParallel = [
1.0,
0.9986,
0.9954,
0.99,
0.9822,
0.973,
0.96,
0.9427,
0.9216,
0.8962,
0.8679,
0.835,
0.7986,
0.7597,
0.7186,
0.6732,
0.6213,
0.5722,
0.5322
]
this.distancesFromEquator = [
0.0,
0.062,
0.124,
0.186,
0.248,
0.31,
0.372,
0.434,
0.4958,
0.5571,
0.6176,
0.6769,
0.7346,
0.7903,
0.8435,
0.8936,
0.9394,
0.9761,
1.0
]
}
forward(coords) {
let { x: lat, y: lng } = coords
lng = this._adjustLng(lng)
// Get the required indices, the remainder in between low and hight as ratio
// and the sign of the found indices, as the tables are only in positive direction.
let { low, high, ratio, sign } = this._getInterpolationValues(lat, 90)
// Values that lie inbetween two indices are interpolated.
let y = this._interpolate(this.distancesFromEquator[low], this.distancesFromEquator[high], ratio)
// Reapply the sign to the vertical position.
y *= sign
// The center of the projection is in the center of the map. Therefore we shift the
// center to the top left corner.
y = 1 - (y + 1) / 2
// The lengthOfParallel table provides us with the corresponding scaling factor
// for a specific latitude. Inbetween values are interpolated as before.
let proportionalLength = this._interpolate(this.lengthOfParallel[low], this.lengthOfParallel[high], ratio)
//To normalize the value to a range from -1 to 1.
let x = (proportionalLength * lng) / 180
x = (x + 1) / 2
return { x, y }
}
backward(position) {
let { x, y } = position
y = 1 - 2 * y
let sign = Math.sign(y)
y = Math.abs(y)
let low = 0
let high = 0
for (let i = 0; i < this.distancesFromEquator.length - 1 && y > this.distancesFromEquator[i]; i++) {
low = i
high = i + 1
}
let lowDist = this.distancesFromEquator[low]
let highDist = this.distancesFromEquator[high]
let ratio = highDist - lowDist == 0 ? 0 : (y - lowDist) / (highDist - lowDist)
let lat = low * 5 + ratio * 5
let parallelLengthMin = this.lengthOfParallel[low]
let parallelLengthMax = this.lengthOfParallel[high]
let completeLength = parallelLengthMin + (parallelLengthMax - parallelLengthMin) * ratio
x = x * 2 - 1
let normalizedLength = x / completeLength
let lng = normalizedLength * 180
return { x: lat * sign, y: this._adjustLng(lng, true) }
}
_adjustLng(lng, inv = false) {
let moved = inv ? lng + this.lng0 : lng - this.lng0
// if (moved < -180) moved += 360
// if (moved > 180) moved -= 360
return moved
}
_interpolate(a, b, ratio) {
return a * (1 - ratio) + b * ratio
}
_getInterpolationValues(value, max) {
let sign = Math.sign(value)
value = Math.min(Math.abs(value), max)
// Note that min and max can be the same. Which is true
// when lat is dividable by 5. This also covers the edge cases 0 and 90.
let minIndex = Math.floor(value / 5)
let maxIndex = Math.ceil(value / 5)
let ratio = (value % 5) / 5
// console.log({ value, minIndex, maxIndex, ratio })
// console.log(this.lengthOfParallel.length)
return { low: minIndex, high: maxIndex, ratio, sign }
}
get name() {
return 'Robinson Projection'
}
get maxViewport() {
let min = new PIXI.Point(-90, -180)
let max = new PIXI.Point(90, 180)
max.x += this.lng0
min.x += this.lng0
console.log({ min, max })
return { min, max }
}
}
+151
View File
@@ -0,0 +1,151 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>Scatter</title>
<link rel='stylesheet' href='../../3rdparty/highlight/styles/default.css'>
<link rel='stylesheet' href='../../../css/doctest.css'>
<script src='../../../dist/iwmlib.3rdparty.js'></script>
<script src='../../../dist/iwmlib.pixi.js'></script>
<script src='../../../dist/iwmlib.js'></script>
<style>
.inline-showcase {
display: flex
}
.map-example {
display: inline-block;
width: 256px;
margin: 5px;
}
</style>
</head>
<body>
<h1>Scatter</h1>
<p>The Scatter in Tüsch specifies some classes to make them more suitable for Map applications.</p>
<h2>CoverScatter</h2>
<p>Usually maps should cover the whole app, or a window inside the app. Therefore they support a cover option. This option
allows prevents the scatter to move inside the window.
</p>
<p>
<h3>Legend</h3>
<ul>
<li>
<span style="color: deepskyblue">Bounds:</span> The bounds represent the bounding boxes of the elements.
</li>
<li>
<span style="color:red;">Polygon:</span> The polygon property of scatters describe their actual extends and are used for e.g. calculating
overlap with the scatter container.
</li>
<li>
<span style="color:greenyellow;">Boundaries:</span> You can set boundaries in the options in the form of
<code>opts = {..., boundaries: {min: {x:0,y:0},
max{x:1,y:1},...}}</code> these will be applied relatively to the image dimensions.</li>
</ul>
</p>
<canvas id="map_01">
</canvas>
<script>
let app = new PIXIApp({
view: map_01,
width: 512,
height: 512
})
app.sceneFactory = () => {
window.scatterContainer = new AdvancedScatterContainer(app.renderer, { showBounds: true, showPolygon: true, app })
return window.scatterContainer
}
app.setup().run()
/*
Create Rotated Container
*/
let pad = 128
let frame = new RigidContainer(app.width - 2 * pad, app.height - 2 * pad)
frame.position.set(pad, pad)
//Create Containers Scatter
new CompactScatter(frame, app.renderer, { translatable: false, scalable: false, rotatable: false})
app.scene.addChild(frame)
let oldCenter = new PIXI.Point()
oldCenter.copy(frame.scatter.center)
frame.scatter.rotation = Math.PI / 4
let offset = Points.subtract(frame.scatter.center, oldCenter)
frame.scatter.position = Points.subtract(frame.scatter.position, offset)
/*
Create Regular Rect
*/
let map = new PIXI.Graphics()
map.beginFill(0x00FFFF, 0.2)
map.drawRect(0, 0, 100, 100)
map.position.set(-10, -10)
map.interactive = true
new CoverScatter(map, app.renderer, { maxScale: Infinity,
autoBringToFront:false,
boundaries: {
min: { x: 0.2, y: 0.2 },
max: { x: 0.8, y: 0.8 }
},
alpha: 0.5,
cover: true,
debug:true
})
frame.addChild(map)
/*
Create Rotated Rect
*/
let advancedScatter = new PIXI.Graphics()
advancedScatter.interactive = true
new CoverScatter(advancedScatter, app.renderer, {
maxScale: Infinity,
autoBringToFront: false,
alpha: 0.3, cover:false,
debug:true
})
advancedScatter.beginFill(0x000000, 0.2)
advancedScatter.drawRect(0, 0, 200, 200)
app.scene.addChild(advancedScatter)
</script>
<h2>Note</h2>
<ul>
<li>
Cover behaviour is not yet working on rotated elements.
</li>
</ul>
</body>
</html>
+617
View File
@@ -0,0 +1,617 @@
/* ES Lint */
/* globals PIXI, requestAnimationFrame, performance, app*/
import { DisplayObjectScatter, ScatterContainer } from '../scatter.js'
import { Points } from '../../utils.js'
import { EventHandler } from './utils.js'
/**
* The AdvancedScatterContainer extends the ScatterContainer, but
* uses the findTargetNew instead of the findTarget method, which takes interactive elements
* into account, when calculating the target.
*/
export class AdvancedScatterContainer extends ScatterContainer {
applyToChildScattersRecursively(parent, root, applyFunc) {
for (let child of parent.children) {
if (child.scatter) {
applyFunc(child, root)
this.applyToChildScattersRecursively(child, root, applyFunc)
}
}
}
/**
* The update method was adjusted to draw bounds and polygpns
*/
update(dt) {
this.clear()
this.lineStyle(1, 0x0000ff)
if (this.showBounds) {
this.applyToChildScattersRecursively(this, this, (child, root) => {
let position = child.worldTransform.apply(new PIXI.Point(0, 0))
this.drawShape(child.scatter.bounds)
let center = {
x: child.scatter.width / 2 / child.scatter.scale,
y: child.scatter.height / 2 / child.scatter.scale
}
center = child.worldTransform.apply(center)
this.drawCircle(center.x, center.y, 4)
this.drawCircle(position.x, position.y, 4)
})
this.lineStyle(2, 0x0000ff)
this.drawShape(this.bounds)
}
if (this.showPolygon) {
this.lineStyle(2, 0xff0000)
for (let child of this.children) {
if (child.scatter) {
let polygon = child.scatter.polygon
let shape = new PIXI.Polygon(polygon.flatAbsolutePoints())
if (
shape.points[0] !== shape.points[shape.points.length - 2] ||
shape.points[1] !== shape.points[shape.points.length - 1]
) {
shape.points.push(shape.points[0], shape.points[1])
}
this.drawShape(shape)
}
}
}
if (this.showTouches) {
let current = this.delegate.interaction.current
for (let [key, point] of current.entries()) {
let local = this.mapPositionToPoint(point)
this.drawCircle(local.x, local.y, 12)
}
}
}
get width() {
return this.bounds.width
}
get height() {
return this.bounds.width
}
findTarget(event, local, global) {
// UO: still problematic. Does not find non interactive elements
// which are needed for some stylus applications
if (event.claimedByScatter) {
return null
}
this.hitScatter = null
let interactionManager = this.renderer.plugins.interaction
let displayObject = interactionManager.hitTest(local, this)
if (displayObject != null) {
if (displayObject.dontBlockScatter && displayObject.parent != null) {
displayObject = interactionManager.hitTest(local, displayObject.parent)
}
if (displayObject.scatter != null) this.hitScatter = displayObject.scatter
if (this.claimEvents) event.claimedByScatter = this.hitScatter
}
return this.hitScatter
}
}
export class RigidScatterContainer extends AdvancedScatterContainer {
constructor(width, height, renderer, opts) {
super(renderer, opts)
this._width = width
this._height = height
}
resize(width, height) {
this._width = width
this._height = height
}
get width() {
return this._width
}
get height() {
return this._height
}
_calculateBounds() {
this._bounds.clear()
let bounds = new PIXI.Bounds()
bounds.minX = this.parent.position.x
bounds.minY = this.parent.position.y
bounds.maxX = this.parent.position.x + this.width
bounds.maxY = this.parent.position.y + this.height
this._bounds.addBounds(bounds)
}
}
export class RigidContainer extends PIXI.Graphics {
constructor(width, height, { onResize = null } = {}) {
super()
if (!width || !height) {
console.log(
`Width (${width}) or height (${height}) not set! If this is intended use another PIXI.DisplayObject instead.`,
this
)
}
this._width = width
this._height = height
this.beginFill(0xffffff, 0.3)
this.drawRect(0, 0, this.width, this.height)
this.onResize = new EventHandler('resize', {
listeners: onResize ? [onResize] : []
})
}
/**
* Resizes the scattercontainer to the provided dimensions.
*
* @param {number} width - Target width of the resize.
* @param {number} height - Target height of the resize.
* @memberof RigidContainer
*/
resize(width, height) {
this._width = width
this._height = height
this._updateHitArea()
this.onResize.call(this, 'resize', { x: width, y: height })
}
_updateHitArea() {
// this.hitArea = new PIXI.Rectangle(0,0, this.width, this.height)
}
get width() {
return this._width
}
get height() {
return this._height
}
}
/**
* Specialization of the DisplayObjectScatter with special behavior needed for maps.
*/
class AdvancedScatter extends DisplayObjectScatter {
/**
* Animates the throw and ensures that the map is always visible. This
* is different from the standard behavior.
*
* @param {*} time
* @memberof AdvancedScatter
*/
animateThrow(time) {
// In rare cases animateThrow is called when the displayObject is already removed
if (this.displayObject.parent == null) {
return
}
//Moved this outside, as the time continously increases,
//when the object is not thrown.
if (this.velocity != null) {
let dt = this._throwDeltaTime()
// This seems to just correct the velocity when its calculated wrong.
// In what case can it get bigger? Velocity tends to always shrink, when no force is added.
let d = this._getThrowDelta(dt)
this._move(d)
this.onDragUpdate(d)
if (dt == 0 || this.needsAnimation()) {
requestAnimationFrame(this.animateThrow.bind(this))
return
} else {
if (this.isOutside()) {
requestAnimationFrame(this.animateThrow.bind(this))
return
}
}
}
this.onDragComplete()
}
get parent() {
// Returns the parent is a scatter get the parent scatter.
// If it's a scatter container, get the displayObject.
// Other cases are not allowed.
return this.displayObject.parent != null && this.displayObject.parent.scatter != null
? this.displayObject.parent.scatter
: this.displayObject.parent
}
_getThrowDelta(dt) {
this.velocity = this.nextVelocity(this.velocity)
if (this.velocity != null) {
return Points.multiplyScalar(this.velocity, dt)
}
return { x: 0, y: 0 }
}
freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) {
this.translatable = translatable
this.scalable = scalable
this.rotatable = rotatable
this.movableX = movableX
this.movableY = movableY
}
unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) {
this.freeze({
translatable,
scalable,
rotatable: false,
movableX,
movableY
})
}
}
export class SubmapScatter extends DisplayObjectScatter {
constructor(displayObject, renderer, opts = {}) {
/*
Currently the submaps are only working on one scale to
avoid recalculations of the shown map.
Therfore we force the scatter to not be scaleable.
*/
Object.assign(opts, {
minScale: 1,
maxScale: 1,
startScale: 1,
overdoScaling: 1,
scalable: false,
resizable: true
})
super(displayObject, renderer, opts)
this.onResize = opts.onResize
}
/**
* Adds mouse wheel resizing to the submap.
*
* @param {MouseEvent} event - Event containing the mouse data.
* @memberof SubmapScatter
*/
onMouseWheel(event) {
super.onMouseWheel(event)
if (this.onResize) {
const zoomFactor = 0.001
let zoom = 1 + zoomFactor * event.deltaY
let anchor = {
x: event.offsetX,
y: event.offsetY
}
this.onResize(anchor, zoom)
}
}
/**
* Overwrites the gesture to get access to the interaction pivot and scaling information.
* This calls the resize method to resize the submap.
*
* @private
* @param {interaction} interaction - Interaction element containing various informations about the touch interaction.
* @memberof SubmapScatter
*/
gesture(interaction) {
super.gesture(interaction)
if (this.onResize) {
let delta = interaction.delta()
this.onResize(delta.about, delta.zoom)
}
}
get width() {
return this.displayObject.width * this.displayObject.scale.x
}
get height() {
return this.displayObject.height * this.displayObject.scale.y
}
freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) {
this.translatable = translatable
this.scalable = scalable
this.rotatable = rotatable
this.movableX = movableX
this.movableY = movableY
}
unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) {
this.freeze({
translatable,
scalable,
rotatable: false,
movableX,
movableY
})
}
onZoomed(about) {
super.onZoomed(about)
}
}
/**
* TODO
* A scatter which width does not increase when child elements are added.
*/
export class CompactScatter extends AdvancedScatter {}
/**
* A problem with PIXI Objects is, that the bounding box changes
* with child elements. Therefore the sized scatter has a rigid size, that is not
* changed by elements, but only with their width and height property.
*/
export class CoverScatter extends AdvancedScatter {
constructor(a, b, opts) {
super(a, b, opts)
opts = Object.assign(
{
debug: false,
cover: true,
boundaries: {
min: { x: 0, y: 0 },
max: { x: 1, y: 1 }
}
},
opts
)
this.debug = opts.debug
this.cover = opts.cover
this._boundaries = opts.boundaries
if (this.debug) {
this.debugGraphics = new PIXI.Graphics()
this.displayObject.addChild(this.debugGraphics)
this.debugGraphics.lineStyle(0.2, 0x00ff00)
this.debugGraphics.drawRect(
this.width * this.boundaries.min.x,
this.height * this.boundaries.min.y,
this.width * (this.boundaries.max.x - this.boundaries.min.x),
this.height * (this.boundaries.max.y - this.boundaries.min.y)
)
this.debugGraphics.endFill()
}
if (this.cover) {
// The reference to the element handler needs to be stored,
// that we can remove it later on.
this._applyInitialCover = this._applyInitialCover.bind(this)
this.displayObject.on('added', this._applyInitialCover)
this._applyInitialCover()
}
}
_applyInitialCover() {
if (this.debug) console.log('ApplyInitialCover: ', parent)
if (this.displayObject.parent)
this.forceCover(this.displayObject.parent.width, this.displayObject.parent.height)
}
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)
if (scale < minCoverScale) {
scale = minCoverScale
}
}
this.scale = scale
}
}
requestFocus(point) {
let allowedMovement = this.outsideBoundaries(Points.subtract(point, this.position))
this.transform(allowedMovement, 1, 0, { x: 0, y: 0 })
}
}
/**
* The MapObjectScatter somewhat inverts the behaviour of
* regular throwables. Regular throwables should bounce of the wall, when they
* overlap to a certain amount. Maps on the otherhand are intended to overlap with the
* scatter container heavily to hide irrelevant parts of the map without revealing
* any background.
*
* @class
* @extends DisplayObjectScatter
*/
export class MapObjectScatter extends CoverScatter {
/**
* x=null, y=null,
minScale=0.1,
maxScale=1.0,
startScale=1.0,
autoBringToFront=true,
translatable=true, scalable=true, rotatable=true, resizable=false,
movableX=true,
movableY=true,
throwVisibility=44,
throwDamping = 0.95,
autoThrow=true,
rotationDegrees=null,
rotation=null,
onTransform = null }
*/
constructor(displayObject, renderer, opts = {}) {
opts = Object.assign(
{
debug: false,
cover: false,
startScale: 1,
boundaries: {
min: { x: 0, y: 0 },
max: { x: 1, y: 1 }
},
autoBringToFront: false
},
opts
)
super(displayObject, renderer, opts)
// if (!renderer) {
// console.error('Renderer was not set!')
// return
// }
this.cover = opts.cover
}
moveTo() {
super.moveTo(...arguments)
}
lock() {
this.rotatable = false
this.moveable = false
this.scaleable = false
}
unlock() {
this.rotatable = true
this.moveable = true
this.scaleable = true
}
getWorldScatter() {
return this.parent
}
}
+134
View File
@@ -0,0 +1,134 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>Utils</title>
<link rel='stylesheet' href='../iwmlib/lib/3rdparty/highlight/styles/default.css'>
<link rel='stylesheet' href='../iwmlib/lib/../css/doctest.css'>
<script src='../iwmlib/lib/3rdparty/highlight/highlight.pack.js'></script>
<script src='../iwmlib/lib/3rdparty/all.js'></script>
<script src='../iwmlib/lib/all.js'></script>
<script src='../iwmlib/lib/pixi/all.js'></script>
<script src="all.js"></script>
<style>
body {
position: relative;
}
</style>
</head>
<body onload="Doctest.run()">
<h1>Utils</h1>
<p>Some utility functionalities for the Tuesch.</p>
<h2>Event Handler</h2>
<p>
The event handler class generalizes a common design principle in javascript. When an event occurs, that is of relevance for
other objects, then this event should be sent out to all objects, that are interested in that event. Objects interested
in that event, subscribe the event handler and get notified via a callback function.
</p>
<script class="doctest">
let result = []
function a() { result.push("a") }
function b() { result.push("b") }
function c() { result.push("c") }
function d() { result.push("d") }
function e() { result.push("e") }
function f() { result.push("f") }
//Init and call.
let eventHandler = new EventHandler("manual", { listeners: [a, b] })
eventHandler.call()
Doctest.expect(result.join(","), ["a", "b"].join(","))
result = []
// Add single
eventHandler.add(c)
eventHandler.call()
Doctest.expect(result.join(","), ["a", "b", "c"].join(","))
result = []
// Add multiple
eventHandler.addMultiple(d, e, f)
eventHandler.call()
Doctest.expect(result.join(","), ["a", "b", "c", "d", "e", "f"].join(","))
result = []
//Remove inbetween
eventHandler.remove(c)
eventHandler.call()
Doctest.expect(result.join(","), ["a", "b", "d", "e", "f"].join(","))
result = []
// Remove first
eventHandler.remove(a)
eventHandler.call()
Doctest.expect(result.join(","), ["b", "d", "e", "f"].join(","))
result = []
// Remove all remaining elements.
eventHandler.empty()
eventHandler.call()
Doctest.expect(result.join(","), [].join(","))
result = []
</script>
<h2>DomUtils</h2>
Utility functions that help handling the DOM.
<h3>positionOnElement(element, position)</h3>
Function that returns the global position for a normalized position.
<div id="positionOnElementBox" class="box" style="width: 512px;height:256px;border: 1px solid black; box-sizing: border-box; transform: rotate(30deg); margin:100px;"></div>
<script class="doctest">
let target = document.getElementById("positionOnElementBox")
window.addEventListener("load", () => {
let positions = [
[0, 0],
[0, 1],
[1, 0],
[0.5,0.5],
[0.2, 0.2],
[0.2, 0.8],
[0.8, 0.2],
[0.8, 0.8],
]
positions.forEach(position => {
position = { x: position[0], y: position[1] }
let transformedPosition = DomUtils.positionOnElement(target, position)
let dot = document.createElement("div");
const size = 10;
Object.assign(dot.style, {
width: size + "px",
height: size + "px",
top: target.offsetTop + target.offsetHeight / 2 + transformedPosition.y -size/2+ "px",
left: target.offsetLeft + target.offsetWidth / 2 + transformedPosition.x -size/2 + "px",
position: "absolute",
backgroundColor: "green",
borderRadius: "50%"
})
document.body.appendChild(dot)
})
})
</script>
</body>
</html>
+513
View File
@@ -0,0 +1,513 @@
import { Points } from '../../utils.js'
export class ObjectUtils {
/**
* Finds the previous key in an object.
*
* @param {string} targetKey - The target key, of whom the predecessor is wanted.
*/
static findPredecessor(targetKey, obj) {
// Initialize pointers ...
let prev = null
let first = null
// ... and a hit flag.
let hit = false
//Iterate over all available items.
for (let key in obj)
if (obj.hasOwnProperty(key)) {
if (!hit) {
//Assign both values,
//while not hit
prev = first
first = key
//When hit, set flag.
if (key == targetKey) {
hit = true
if (prev) return prev //If it was not hit on the first item, we can return prev.
}
} else {
//Otherwise, when first item was hit, we iterate over the entire object.
first = key
}
}
return first
}
/**
* Finds the successing key in an object.
*
* @param {string} targetKey - The key of whom the successor key should be found.
* @param {object} obj - The object, that is scanned for the successor.
*/
static findSuccessor(targetKey, obj) {
let first = null
let next = false
for (let key in obj)
if (obj.hasOwnProperty(key)) {
if (!first) first = key
if (next) {
return key
}
if (targetKey == key) {
next = true
}
}
return first
}
static fromPath(obj, path, separator = '.') {
let arr = path.split(separator)
let result = obj
for (let i = 0; i < arr.length; i++) {
if (result[arr[i]] !== null) {
result = result[arr[i]]
} else return null
}
return result
}
}
export class PathUtils {
static fixTrailingSlash(url) {
return url.replace(/\/?$/, '/')
}
}
export class TimeUtils {
static minutesToMs(minutes) {
return TimeUtils.minutesToSeconds(minutes) * 1000
}
static minutesToSeconds(minutes) {
return minutes * 60
}
}
export class StringUtils {
/* Used from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript */
static capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
}
export class GeometryUtils {
static RandomRange(min, max) {
return GeometryUtils.toRadians(Math.random() * (max - min) + min)
}
static RandomSign() {
return Math.sign(Math.random() - 0.5)
}
static toDegrees(radians) {
return (radians * 180) / Math.PI
}
static toRadians(degrees) {
return (degrees / 180) * Math.PI
}
/*
Recursion is buggy.
*/
static subdivide(polygon, factor = 0.5, n = 0) {
console.log(polygon.length, factor)
let points = []
for (let i = 0; i < polygon.length; i++) {
let prev = i - 1 < 0 ? polygon[polygon.length - 1] : polygon[i - 1]
let next = i + 1 < polygon.length ? polygon[i + 1] : polygon[0]
let current = polygon[i]
points.push(GeometryUtils.linearInterpolation(prev, current, factor))
points.push(GeometryUtils.linearInterpolation(current, next, 1 - factor))
}
if (n > 0) {
n--
points = GeometryUtils.subdivide(points, n)
}
return points
}
static linearInterpolation(first, second, n) {
let point = new PIXI.Point()
point.x = first.x * n + second.x * (1 - n)
point.y = first.y * n + second.y * (1 - n)
return point
}
}
export class Matrix {
constructor(a, b, d, e, c = 0, f = 0) {
Object.assign(this, {
a,
b,
c,
d,
e,
f,
g: 0,
h: 0,
i: 1
})
}
static Transpose(m) {
let first = ['b', 'c', 'f']
let second = ['d', 'g', 'h']
for (let i = 0; i < first.length; i++) {
let tmp = m[first[i]]
first[i] = second[i]
second[i] = tmp
}
}
static Rotation(deg) {
let rad = (2 * Math.PI * deg) / 180
return new Matrix(Math.cos(rad), -Math.sin(rad), Math.sin(rad), Math.cos(rad))
}
static Transform(x, y) {
return new Matrix(1, 0, 0, 1, x, y)
}
static MultiplyPoint(m, p) {
x = p.x * m.a + m.b * p.y + m.c * 1
y = p.x * m.d + p.y * m.e + m.f * 1
return {
x,
y
}
}
}
export class DomUtils {
static getCenter(element) {
let x = (element.clientLeft + element.clientWidth) / 2
let y = (element.clientTop + element.clientHeight) / 2
return { x, y }
}
static getTransformedPosition(element) {
let mat = this.getCSSMatrix(element)
return { x: mat[4], y: mat[5] }
}
static getCSSMatrix(element) {
let matrix = window.getComputedStyle(element).transform
if (matrix == 'none') {
return [1, 0, 0, 1, 0, 0]
} else {
const pre = 'matrix('
let values = matrix.substring(pre.length, matrix.length - 1).split(',')
for (let i in values) {
values[i] = parseFloat(values[i])
}
return values
}
}
static positionOnElement(element, position) {
const matrix = this.getCSSMatrix(element)
const inX = element.offsetWidth * (position.x - 0.5)
const inY = element.offsetHeight * (position.y - 0.5)
let outX = matrix[0] * inX + matrix[2] * inY + matrix[4]
let outY = matrix[1] * inX + matrix[3] * inY + matrix[5]
let out = {
x: outX,
y: outY
}
return out
}
static applyTransform(target, transformedPosition, size = 0) {
return {
x: target.offsetLeft + target.offsetWidth / 2 + transformedPosition.x - size / 2,
y: target.offsetTop + target.offsetHeight / 2 + transformedPosition.y - size / 2
}
}
}
export class Vector {
static length(vector) {
return Math.sqrt(vector.x * vector.x + vector.y * vector.y)
}
static normalize(vector) {
return Points.multiplyScalar(vector, 1 / this.length(vector))
}
static scaleTo(vector, length) {
let normalized = this.normalize(vector)
return Points.multiplyScalar(normalized, length)
}
}
export class PIXIUtils {
/*
* Transform a pixi text to it's actual screensize,
* ignoring it's local transforms
*/
static toScreenFontSize(pixiText, fontSize = null) {
pixiText._recursivePostUpdateTransform();
let normalizedScale = {
x: pixiText.scale.x / pixiText.transform.worldTransform.a,
y: pixiText.scale.x / pixiText.transform.worldTransform.d
};
pixiText.scale = { x: normalizedScale.x, y: normalizedScale.y };
if (fontSize) pixiText.style.fontSize = fontSize;
}
static saveFill(graphics) {
return {
fill: graphics.fill.color,
alpha: graphics.fill.alpha
}
}
}
/**
* The EventHandler class is used to take care of a event based design
* pattern. Callbacks can subscribe to an event and these unknown sources
* get notified whenever the event changes.
*
* @export
* @class EventHandler
*/
export class EventHandler {
/**
* Creates an instance of EventHandler.
* @param {any} name
* @param {any} [{
* listeners = [] - With the listnerers parameter the user can specify a function, array of functions or null (no function - useful when used in constructor with optional parameter).
* }={}]
* @memberof EventHandler
*/
constructor(name, { listeners = [] } = {}) {
this.name = name
this.listeners = []
this.onces = []
/**
* One may initialize the eventListener using a parameter
* that is either passed or null.
*/
if (listeners == null) {
// Null is a valid value as the EventHandler assumes no listener is passed on purpose.
// This is useful, when a default parameter is passed as null.
} else if (Array.isArray(listeners)) this.listeners = listeners
else if (typeof listeners == 'function') {
this.listeners = []
this.add(listeners)
} else {
console.warn(
"The provided 'listeners' is neither an Array of functions, nor a function. No eventcallback was added!",
listeners,
this
)
}
}
addMultiple(...callbacks) {
for (let callback of callbacks) {
this.listeners.push(callback)
}
}
add(callback) {
this.listeners.push(callback)
return callback
}
once(callback) {
this.onces.push(callback)
}
remove(func) {
for (const [idx, listener] of this.listeners.entries()) {
if (listener === func) {
this.listeners.splice(idx, 1)
return true
}
}
return false
}
empty() {
this.listeners = []
}
call(context, ...args) {
if (context == null) {
this.listeners.forEach(listener => listener(...args))
this.onces.forEach(listener => listener(...args))
} else {
this.listeners.forEach(listener => listener.call(context, ...args))
this.onces.forEach(listener => listener.call(context, ...args))
}
this.onces = []
}
get length() {
return this.listeners.length + this.onces.length
}
}
export class Dom {
/**
* Popups should be displayed right over the text.
* Normally we would expect the popup to appear right over
* the center of the text. A problem in HTML is, that it's hard
* to determine the position of a text link, when it has a line-break
* in it.
*
* This function solves this problem in the (so far) only possible way.
*
* 1. It removes the link from the dom tree.
* 2. It adds an empty copy A of the link to the dom tree. (Copy is important, as the same styles have to be applied.)
* 3. The contents of the link are added one by one to A.
* 4. If the resulting boundingRect is bigger than the previous one, a line break is detected.
* 5. The old line is tested, if the point was inside that boundingBox. If so save that bounding box (Goto: 7), else:
* 6. Saves the content to a preceding clone B. And repeats from 3.
* 7. Replace A with the initial content
* 8. Return the found BoundingBox. If none found. Return the last bounding box.
*/
static getTextHitRect(link, point) {
// We cannot use it as it produces axis aligned bounding boxes
/* if (true) {
let rects = link.getClientRects()
let target = null
for (let [idx, rect] of Object.entries(rects)) {
target = rect
if (Rect.contains(rect, point))
break
}
return target
} else {*/
let processedText = link.cloneNode(true)
let content = processedText.innerHTML
let words = content.split(/ /g)
processedText.innerHTML = ''
event.target.innerHTML = ''
// let lineRect = event.target.getBoundingClientRect()
let local = Points.fromPageToNode(event.target.parentNode, point)
console.log(local)
let target = event.target
let height = 0
while (words.length > 0) {
let word = words.pop()
target.innerHTML += word + ' '
if (target.height != height) {
// New line was reached.
console.log('NEW LINE WAS REACHED!')
}
}
return {
top: 0,
left: 0,
right: 100,
bottom: 100,
width: 100,
height: 100
}
// let total = words.length
// while (words.length > 0) {
// let lastRect = lineRect
// let lastContent = event.target.innerHTML
// let added = words.length == total ? "" : " "
// added += words.shift()
// event.target.innerHTML += added
// lineRect = event.target.getBoundingClientRect()
// // When new line or last line:
// if (lineRect.height != lastRect.height) {
// //Reconstructure last line.
// event.target.innerHTML = lastContent
// //Create Rect from last line.
// lineRect = event.target.getBoundingClientRect()
// rects.push(lineRect)
// //Copy last line content to processed text.
// processedText.innerHTML += lastContent
// // Create content of new line.
// event.target.innerHTML = added
// if (!processedText.parentNode) event.target.parentNode.insertBefore(processedText, event.target)
// if (Rect.contains(lineRect, point)) {
// break
// }
// }
// if (words.length == 0) {
// lineRect = event.target.getBoundingClientRect()
// processedText.innerHTML += event.target.innerHTML
// rects.push(lineRect)
// }
// }
// event.target.innerHTML = content
// if (processedText.parentNode) processedText.parentNode.removeChild(processedText)
// return lineRect
// }
}
static printDomRect(rect, { color = 'red', pad = 0, parent = document.body }) {
let element = document.createElement('div')
Object.assign(element.style, {
padding: pad + 'px',
borderColor: color,
borderWidth: '2px',
borderStyle: 'solid',
top: rect.top + 'px',
left: rect.left + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
position: 'absolute',
zIndex: 10000,
opacity: 0.3
})
parent.appendChild(element)
}
static printDomPoint(point, { color = 'red', parent = document.body }) {
let element = document.createElement('div')
Object.assign(element.style, {
position: 'absolute',
top: point.y + 'px',
left: point.x + 'px',
width: '10px',
height: '10px',
backgroundColor: color,
zIndex: 10000
})
parent.appendChild(element)
}
}