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 } 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 } get map() { let map = null if (this.mapLayer) { map = this.mapLayer.map } return map } 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 { 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.} 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.} polygon - An array of PIXI.Points for drawing a polygon. * @param {Array.} [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 } } }