iwmlib/lib/pixi/maps/geographics.js

746 lines
21 KiB
JavaScript

import { Points } from '../../utils.js'
import { EventHandler } from './utils.js'
import { FlagPolygon } from '../graphics/label.js'
import { DeepZoomMap } from './map.js'
/**
* GeoGraphics are graphical objects, that does not store the graphics information
* in screen space, but in geographical coordinates. Therefore GeoGraphics must be
* placed on GeoLayers to work properly.
*
* (Note: As GeoLayers are always children of a map layer. When the map is changed
* all GeoLayers are notified via the 'adaptTo(map)' method.)
*
* The geolayers forward this 'adaptTo' to all children that are GeoGraphics.
* Which adjust their so called 'point' data to the new map.
*
* @abstract
*/
export class GeoGraphics {
constructor(coordinates, { scale = 1, onDraw = null, onDrawEnd = null, debug = false } = {}) {
this.coordinates = coordinates
this.debug = debug
this.graphics = new PIXI.Graphics()
this.scale = scale
this.drawHandler = new EventHandler('onDraw', { listeners: onDraw })
this.drawEndHandler = new EventHandler('onDrawEnd', { listeners: onDrawEnd })
this._points = null
this._position = null
this.map = null
}
clone() {
console.error(`Call of abstract method clone(). Overwrite in subclass.`, this)
}
_cloneOptions() {
return {
debug: this.debug,
scale: this.scale
}
}
/**
* The _adaptCoordinates is called first by the adaptTo Method.
* Here all coordinates are transformed into point coordinates.
* This must be overloaded in subclass.
*
* @abstract
*/
_adaptCoordinates(map) {
console.error(`Call of abstract method _adaptCoordinates(map). Overwrite in subclass.`, this)
}
/**
* Gets all screen points in a single array.
*
* @abstract
* @returns {array} - Array of all points in the GeoGraphic.
*/
_getPoints() {
console.error(`Call of abstract method _getPoints(func). Overwrite in subclass.`, this)
}
/**
* Manipulates all points depending on a function.
* Mainly used to transform points to local space.
*
* @abstract
* @param {function} func
* @memberof GeoGraphics
*/
_manipulatePoints(func) {
console.error(`Call of abstract method _manipulatePoints(func). Overwrite in subclass.`, this)
}
/**
* The _draw method is called last on adaptation. It creates the GraphicData
* of the specified subclass. To manipulate the style of the graphic, hook an onDraw listener
* to the GeoGraphics object. It is called before the _draw and lets the user modify color and
* lineStyle of the drawn object.
*
* Note: It could also be used for more radical manipulations on the graphics object.
* But this should be used with care.
*
* @abstract
*/
_draw() {
console.error(`Call of abstract method _draw(). Overwrite in subclass.`, this)
}
/**
* Called by the containing geo layer, when the map changes.
*/
adaptTo(map) {
this.map = 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
}
// get map() {
// if (
// this.graphics.layer &&
// (this.graphics.layer instanceof GeoLayer || this.graphics.layer instanceof MapLayer)
// ) {
// return this.graphics.layer.map
// } else return null
// }
// get mapLayer() {
// if (
// this.graphics.layer &&
// (this.graphics.layer instanceof GeoLayer || this.graphics.layer instanceof MapLayer)
// ) {
// return this.graphics.layer.mapLayer
// } else return null
// }
/**
* Prepare draw is a private function, that prepares the graphics
* for the next draw call. It also fires the drawHandler.
*
* @private
* @memberof GeoGraphics
*/
_prepareDraw() {
this.graphics.clear()
/*
Set a fillcolor and a stroke style for
debugging. Can be overloaded using the onDraw
event function.
*/
if (this.debug) this.graphics.beginFill(0xff00ff)
}
_updatePosition() {
let points = this._getPoints()
this._position = GeoGraphics.calculateCenterOfMass(points)
this._manipulatePoints(point => {
point = Points.subtract(point, this._position)
return point
})
this.graphics.position = this._position
}
/**
* Calculates the center of Mass for a set of points.
*
* @static
* @param {Array} points - Array of points in the format {x: a, y:b}
* @returns {object} - Returns a point containing the center of mass of the polygon.
* @memberof GeoGraphics
*/
static calculateCenterOfMass(points) {
let com = new PIXI.Point()
points.forEach(p => {
let point = new PIXI.Point(p.x, p.y)
com = Points.add(com, point)
})
return Points.multiplyScalar(com, 1 / points.length)
}
}
/**
* Represents a single point on the Map.
*
* This GeoGraphics does not provide any visual representation.
* Draw the desired shape in the onDraw callback.
*/
export class GeoPoint extends GeoGraphics {
clone() {
return new GeoPoint(this.coordinates, this._cloneOptions())
}
_adaptCoordinates(map) {
let scale = 1
if (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() {}
}
export class GeoLine extends GeoGraphics {
/**
* @param {object} opts - Optional values
* @param {array} [opts.points=[]] - Initial points of the geo shape.
* @param {boolean} [closed=false] - Defines if the
*/
constructor(coordinates, { closed = false, size = 1, onDraw = null } = {}) {
super(coordinates, {
size,
onDraw
})
this._closed = closed
}
clone() {
return new GeoLine(this.coordinates, this._cloneOptions)
}
_cloneOptions() {
let options = super._cloneOptions()
Object.assign(options, {
closed: this.closed
})
}
/**
* Adds a point to the geo line.
*/
addPoint(coordinate) {
this.coordinates.push(coordinate)
}
_manipulatePoints(func) {
this.points.forEach((point, idx, array) => {
array[idx] = func(point)
})
}
_getPoints() {
return this.points
}
_adaptCoordinates(map) {
let points = []
this.coordinates.forEach(point => {
points.push(map.coordinatesToPoint(point))
})
return points
}
_prepareDraw() {
this.graphics.clear()
if (this.debug) this.graphics.lineStyle(0.5, 0xff00ff)
this.drawHandler.call(this)
}
_draw() {
/**
* This resets the fill.
*
* DISCUSS: SO: "I'm not sure how the line should be defined.
* On the one hand. The line is clearly intended to
* represent a line and not an area. On the other hand,
* why should the user be prevented from using a fill for the
* area within the line. But if he want's a fill, why don't take
* a Polygon in the first place?
*
* (But if it's a predefined GeoJSON object obtained through e.g. elasticsearch,
* then the user is not in full control of the object type and it may be a good
* addition to grant the user this additional design choice.)
*
* The opportunity to do so would result in additional conditions, when creating the
* GeoGraphics of an overlay."
* */
this.graphics.beginFill(0, 0)
if (this.points.length > 0) {
this.graphics.moveTo(this.points[0].x, this.points[0].y)
for (let i = 1; i < this.points.length; i++) {
this.graphics.lineTo(this.points[i].x, this.points[i].y)
}
if (this.closed) {
this.graphics.lineTo(this.points[0].x, this.points[0].y)
}
}
}
get closed() {
return this._closed
}
set closed(val) {
if (val != this._closed) {
this._closed = val
this.draw()
}
}
}
export class GeoShape extends GeoGraphics {
clone() {
return new GeoShape(this.coordinates, this._cloneOptions)
}
_manipulatePoints(func) {
this.constructor._manipulatePoints(this.points, func)
}
/**
* Mets the requirements for the _manipulatePointsMethod().
* Primarily used in subclasses to get the processing steps from
* their superclass.
*
* @static
* @protected
* @param {Array.<PIXI.Points>} points - The points array that shold be manipulated.
* @param {function} func - The function that changes the single point value. Has to return a new point.
* @memberof GeoShape
*/
static _manipulatePoints(points, func) {
points.forEach((pointArray, arrIdx) => {
pointArray.forEach((point, idx) => {
points[arrIdx][idx] = func(point)
})
})
}
_getPoints() {
return this.constructor._getPointsFrom(this.points)
}
/**
* Returns all points of a polygon array.
* Useful for when getting sub polygons in child class.
*/
static _getPointsFrom(shape) {
let concatArray = []
shape.forEach(array => {
concatArray = concatArray.concat(array)
})
return concatArray
}
_adaptCoordinates(map) {
let val = this.constructor._adaptPoint(this.coordinates, map)
return val
}
static _adaptPoint(coordinates, map) {
return coordinates.map(array => {
return array.map(point => {
return map.coordinatesToPoint(point)
})
})
}
_draw() {
this._drawFrom(this.points)
this.graphics.position = this.position
}
_drawFrom(shape) {
const { polygon, hole } = this.constructor._pointsToShape(shape)
this._drawShape(polygon, hole)
}
/**
* Separates the points array into it's two parts:
* - the solid polygon
* - a hole that is cut into the polygon (optional)
*
* @private
* @returns {object} - Returns an object containing the polygon and the hole in the form of {polygon: [...PIXI.Point], hole: [...PIXI.Point]}
*/
static _pointsToShape(points) {
let polygon = GeoShape._transformToPIXI(points[0])
let hole = points[1] ? GeoShape._transformToPIXI(points[1]) : []
return { polygon, hole }
}
/**
* Transform an array of poins into an array of PIXI.Points.
* Note: When creating PIXI.Polygons, for some reason the points
* need to be a PIXI.Points object (at least the first one).
*
* @param {array} points - Points in the form of {x:a,y:b}
* @returns An array of PIXI.Points
* @memberof GeoPolygon
*/
static _transformToPIXI(points = []) {
let polygon = []
points.forEach(point => {
polygon.push(new PIXI.Point(point.x, point.y))
})
return polygon
}
/**
* Draws a single shape onto the graphics object.
* Useful when being called from subclass.
*
* @protected
* @param {Array.<PIXI.Point>} polygon - An array of PIXI.Points for drawing a polygon.
* @param {Array.<PIXI.Point>} [hole=[]] - An Array of PIXI.Points for cutting a hole into the polygon
* @memberof GeoShape
*/
_drawShape(polygon, hole = []) {
// We save the fill specified in the onDraw event handler.
//
// Consider: Maybe it would be a good idea to add a 'onHoleDraw'
// callback to make the hole customizable. Maybe you want
// to fill it with a different color or an mediocre alpha value.
// then feel free to implement it.
let { fill, alpha } = PIXIUtils.saveFill(this.graphics)
/**
* This may seem redundant, but it's required
* to make the hole clickable.
*
* It was a bit confusing, so I made a CodePen
* for it: https://codepen.io/Ukmasmu/pen/WJEaoK
*/
if (hole.length > 0) {
this.graphics.beginFill(0x0000ff, 0.0000001)
this.graphics.drawPolygon(hole)
}
this.graphics.beginFill(fill, alpha)
this.graphics.drawPolygon(polygon)
if (hole.length > 0) {
this.graphics.beginHole()
this.graphics.drawPolygon(hole)
this.graphics.endHole()
}
}
}
/**
* The MultiGraphics makes use of the other GeoGraphics to
* create a single graphics element out of multiple different
* GeoGraphics.
*/
class GeoMultiGraphics extends GeoGraphics {
/**
* The _adaptCoordinates is called first by the adaptTo Method.
* Here all coordinates are transformed into point coordinates.
* This must be overloaded in subclass.
*
* @abstract
*/
_adaptCoordinates(map) {}
/**
* Gets all screen points in a single array.
*
* @abstract
* @returns {array} - Array of all points in the GeoGraphic.
*/
_getPoints() {
console.error(`Call of abstract method _getPoints(func). Overwrite in subclass.`, this)
}
/**
* Manipulates all points depending on a function.
* Mainly used to transform points to local space.
*
* @abstract
* @param {function} func
* @memberof GeoGraphics
*/
_manipulatePoints(func) {
console.error(`Call of abstract method _manipulatePoints(func). Overwrite in subclass.`, this)
}
/**
* The _draw method is called last on adaptation. It creates the GraphicData
* of the specified subclass. To manipulate the style of the graphic, hook an onDraw listener
* to the GeoGraphics object. It is called before the _draw and lets the user modify color and
* lineStyle of the drawn object.
*
* Note: It could also be used for more radical manipulations on the graphics object.
* But this should be used with care.
*
* @abstract
*/
_draw() {
console.error(`Call of abstract method _draw(). Overwrite in subclass.`, this)
}
}
export class GeoText extends GeoPoint {
constructor(coordinates, text, opts) {
super(coordinates, opts)
this.align = opts.align
this.textStyle = Object.assign(new PIXI.TextStyle(), opts.textStyle)
this._text = new PIXI.Text(text, this.textStyle)
//TODO: Make this more generic:
// We have 8 layers (12-20) for each map so this temporarily works fine.
// Outsource it to the map class.
//let textScale = Math.pow(2, 7)
// let textScale = 5
// this.text.scale.set(1 / textScale, 1 / textScale)
switch (this.align) {
case 'left':
break
case 'center':
this.text.position.x -= this.text.width / 2
break
case 'right':
this.text.position.x -= this.text.width
break
default:
console.error('Align type is not implemented: ' + this.align + ". Use 'left', 'right' or 'center'.")
}
this.text.position.y -= this.text.height / 2
this.graphics.addChild(this.text)
}
get text() {
return this._text
}
_draw() {
super._draw()
}
adaptTo(map) {
super.adaptTo(map)
}
}
/**
* A geotext with a notch at a certain Position.
*/
export class GeoFlagLabel extends GeoText {
constructor(coordinates, text, opts) {
super(
coordinates,
text,
Object.assign(opts, {
align: 'left'
})
)
if (!opts.flag) {
opts.flag = {}
}
const {
alpha = 1,
color = 0xffffff,
borderAlpha = 1,
borderColor = 0x000000,
borderThickness = 0,
fillNotch = false
} = opts
Object.assign(this, {
alpha,
color,
fillNotch,
borderAlpha,
borderColor,
borderThickness
})
this.flagOptions = Object.assign(
{
notchSize: 10,
notchWidth: null,
notchHeight: null,
padding: { x: 0, y: 0 },
originOffset: { x: 0, y: 0 }
},
opts.flag
)
this.border = this.borderThickness > 0
Object.assign(this.flagOptions, {
width: this.text.width + this.flagOptions.padding.x * 2,
height: this.text.height + this.flagOptions.padding.y * 2
})
this.flagPolygon = new FlagPolygon(this.flagOptions)
// hover effect
//--------------------
const duration = 0.2
this.graphics.interactive = true
this.graphics.on('pointerover', event => {
TweenLite.to(this.graphics, duration, { alpha: 0.6 })
})
this.graphics.on('pointerout', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
this.graphics.on('pointerupoutside', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
this.graphics.on('pointercancel', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
this.graphics.on('pointertap', event => {
TweenLite.to(this.graphics, duration, { alpha: 1 })
})
// this.graphics.on('pointerdown', event => {
// TweenLite.to(this.graphics, duration, { alpha: 0.45 })
// })
// this.graphics.on('pointerup', event => {
// TweenLite.to(this.graphics, duration, { alpha: 0.6 })
// })
}
_draw() {
super._draw()
this.graphics.beginFill(this.color, this.alpha)
this.graphics.drawPolygon(this.flagPolygon)
this.flagPolygon.placeText(this.text, this.flagOptions.padding)
if (this.border) {
const lineStyle = [this.borderThickness, this.borderColor, this.borderAlpha]
if (this.fillNotch) {
this.graphics.beginFill(this.borderColor)
this.graphics.drawPolygon(this.flagPolygon.notch)
this.graphics.endFill()
this.graphics.lineStyle(...lineStyle)
this.graphics.drawPolygon(this.flagPolygon.rect)
this.graphics.drawPolygon(this.flagPolygon.notch)
} else {
this.graphics.lineStyle(...lineStyle)
this.graphics.drawPolygon(this.flagPolygon)
}
}
}
}
export class GeoMultiShape extends GeoShape {
static _manipulatePoints(points, func) {
points.forEach(shape => {
GeoShape._manipulatePoints(shape, func)
})
}
static _getPointsFrom(multiShapeArray) {
let points = []
multiShapeArray.forEach(shape => {
points = points.concat(GeoShape._getPointsFrom(shape))
})
return points
}
static _adaptPoint(coordinates, map) {
let points = []
coordinates.forEach(shape => {
let adaptedPoint = GeoShape._adaptPoint(shape, map)
points.push(adaptedPoint)
})
return points
}
calculateLocation() {
let coms = []
this.coordinates.forEach(polygon => {
coms.push(GeoGraphics.calculateCenterOfMass(polygon))
})
return GeoGraphics.calculateCenterOfMass(coms)
}
_drawFrom(multiShape) {
multiShape.forEach(shape => {
super._drawFrom(shape)
})
}
}
class PIXIUtils {
/*
* Transform a pixi text to it's actual screensize,
* ignoring it's local transforms
*/
static toScreenFontSize(pixiText, fontSize = null) {
pixiText._recursivePostUpdateTransform()
let normalizedScale = {
x: pixiText.scale.x / pixiText.transform.worldTransform.a,
y: pixiText.scale.x / pixiText.transform.worldTransform.d
}
pixiText.scale = { x: normalizedScale.x, y: normalizedScale.y }
if (fontSize) pixiText.style.fontSize = fontSize
}
static saveFill(graphics) {
return {
fill: graphics.fill.color,
alpha: graphics.fill.alpha
}
}
}