import { GeoLayer } from './geolayer.js' import { GeoPoint, GeoMultiShape } from './geographics.js' import GeoJson from './geojson.js' import Popup from '../popup.js' /** * The Overlay-Class shall provide and easy to use frameworkd * of marking points of interest on a map, using shapes, geometries, * items or text. * * It's highly configurable and adaptable to a variety of scenarios. * */ export default class Overlay { /** *Creates an instance of Overlay. * @param {*} [{ * name = "unnamed", {string} * connector = false, * connectorType = "line", * location = null, * label = false, * labelType = null, * labelLocation = null, * labelAlignment = "center", * labelVerticalAlign = "underneath", * labelTarget = null, * shape = "geometry", {string} - Defines the shape to be used: geometry, circle, rectangle * geometry = null, * geometryType = "point", * geometryFallback = "circle", * informationPopup = "infocard", * * }={}] * @memberof Overlay */ constructor(opts = {}) { let defaultTextStyle = new PIXI.TextStyle() Object.assign( this, { type: 'auto', name: 'unnamed', borderColor: 0x000000, borderThickness: 5, disabledAlpha: 1, disabledColor: 0xaa1111, disabledBorderColor: 0x000000, enabled: true, labelScale: 1, location: null, label: false, labelType: null, labelLocation: null, labelAlignment: 'center', labelVerticalAlign: 'underneath', labelSpacing: 10, labelTarget: null, geometry: null, information: '', informationPath: null, informationType: 'popup', icon: null, iconAlpha: 1, iconAnchor: { x: 0.5, y: 0.5 }, iconColor: 0xffffff, iconScale: 1, iconOffset: { x: 0, y: 0 }, items: [], color: 0xff00ff, fillAlpha: 1, size: 50, scale: 1, rescale: false, popoverOffset: { x: 0, y: 0 }, 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 }, opts ) } get textureParameters() { return ['icon'] } setTexture(parameter, key, path) { let obj = (this[parameter] = {}) obj._key = key obj._path = path obj._texture = null obj.initialize = function(texture) { if (!texture) console.error('Could not initialize texture. Invalid value provided: ', texture) this._texture = texture }.bind(obj) obj.__defineGetter__('texture', function() { if (!obj._texture) { console.error('Texture was not yet initialized! ', obj) } else { return obj._texture } }) obj.__defineGetter__('key', () => { return obj._key }) obj.__defineGetter__('path', () => { return obj._path }) } apply(parameter, func, obj = null) { if (obj == null) return if (Array.isArray(obj)) { obj.forEach(child => { if (typeof child == 'object') this.apply(parameter, func, child) }) } else { for (let [key, val] of Object.entries(obj)) { if (val == null) continue if (key == parameter) { func.call(obj, val) } if (typeof val == 'object') { this.apply(parameter, func, val) } } } } /** * Creates an overlay from the current Object. * * @param {settings} object - Object to configure the creation process. * @param {settings.excludeItems} function - You may provide a function, that excludes elements selectively. The function must return true for items to exclude them. * @param {settings.adjustItems} function - You may adjust the created PIXI items, using the adjustItems function. * @returns {GeoLayer} - Returns the created GeoLayer. * @memberof Overlay */ create({ excludeItems = item => { return false }, informationCallback = null, adjustItems = null, cleanupItems = null } = {}) { const name = this.name[0].toUpperCase() + this.name.slice(1).toLowerCase() + ' 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)) { if (adjustItems) { adjustItems(item) } item.overlay = this let graphics = this.createItem(item, informationCallback) geoLayer.addChild(graphics) if (cleanupItems) { cleanupItems(item) } } }) return geoLayer } getItemProperty(item, property) { let propertyValue = null const propertyExistsOnItem = item[property] !== undefined const propertyExistsOnOverlay = this[property] !== undefined if (propertyExistsOnItem) propertyValue = item[property] else if (propertyExistsOnOverlay) propertyValue = this[property] else { console.error(`Invalid property: ${property} on`, item) } propertyValue = this._resolveReference(item, propertyValue) return propertyValue } _isReference(property) { if (property && typeof property == 'object' && property.hasOwnProperty('@property')) return true else return false } _resolveReference(item, property) { if (this._isReference(property)) { let referencedProperty = property['@property'] return this.getItemProperty(item, referencedProperty) } else { return property } } findAllTextures() { let textures = [] this.textureParameters.forEach(parameter => { if (this.hasOwnProperty(parameter) && this[parameter] != null) { textures.push(this[parameter]) } this.items.forEach(item => { if (item.hasOwnProperty(parameter) && item[parameter] != null) { textures.push(item[parameter]) } }) }) return textures } selectTextures(textures) { this.textureParameters.forEach(parameter => { if (this.hasOwnProperty(parameter) && this[parameter]) { let path = this[parameter] const texture = textures.get(path) if (!texture) console.error('Texture could not be selected: ', path) this[parameter] = { texture, path } } this.items.forEach((item, idx) => { if (this.items[idx].hasOwnProperty(parameter) && this.items[idx][parameter]) { const path = this.items[idx][parameter] const texture = textures.get(path) this.items[idx][parameter] = { path, texture } } }) }) } createItem(item, informationCallback = null) { let geographics /** * Use a factory to draw the items, if a type is specified. */ if (this.type != 'auto') { let geographicsFactory = Overlay.requestFactory(this.type) if (!geographicsFactory) console.error( 'Invalid Overlay Mode: ' + this.type + '. Fallback to auto mode. Overlaymodes must be registeres beforehand. Valid modes are: ' + Overlay.listFactories().join(',') + '.' ) else { let actualItem = Object.assign({}, this, item) geographics = geographicsFactory(actualItem) } } /** * Otherwise draw the default geographics. */ if (!geographics) { if (item.geometry == null || item.geometry == undefined) { if (item.location) { geographics = this._drawPoint(item, informationCallback) const scale = this.getItemProperty(item, 'scale') geographics.graphics.scale.set(scale, scale) } else console.warn("Item could not be drawn. Specify a 'location' or a 'geometry'.", item) } else { geographics = this._drawGeometry(item) this._drawRoutine(geographics, item, informationCallback) } this._drawLabel(item, geographics) } return geographics } _drawLabel(item, geographics) { let label = this.getItemProperty(item, 'label') if (label) { let textStyle = this._gatherFontStyle(item) let text = new PIXI.Text(label, textStyle) let labelScale = this.getItemProperty(item, 'labelScale') text.scale.set(labelScale, labelScale) let position = new PIXI.Point() let align = this.getItemProperty(item, 'labelAlignment') if (align == 'left'); else if (align == 'center') position.set(text.position.x - text.width / 2, text.position.y) else if (align == 'right') position.set(text.position.x - text.width, text.position.y) else this._logPropertyNotImplemented('labelAlignment', align) let verticalAlign = this.getItemProperty(item, 'labelVerticalAlignment') if (verticalAlign == 'underneath') { let size = this.getItemProperty(item, 'size') let scale = this.getItemProperty(item, 'scale') let labelSpacing = this.getItemProperty(item, 'labelSpacing') position.y += size * scale + labelSpacing } else if (verticalAlign == 'above') { let size = this.getItemProperty(item, 'size') let scale = this.getItemProperty(item, 'scale') let labelSpacing = this.getItemProperty(item, 'labelSpacing') position.y -= size * scale + text.height + labelSpacing } else this._logPropertyNotImplemented('labelVerticalAlignment', verticalAlign) text.position.set(position.x, position.y) geographics.graphics.addChild(text) } } _gatherFontStyle(item) { return { align: this.getItemProperty(item, 'labelMultiLineAlign'), breakWords: this.getItemProperty(item, 'labelBreakWords'), dropShadow: this.getItemProperty(item, 'labelDropShadow'), dropShadowAlpha: this.getItemProperty(item, 'labelDropShadowAlpha'), dropShadowAngle: this.getItemProperty(item, 'labelDropShadowAngle'), dropShadowBlur: this.getItemProperty(item, 'labelDropShadowBlur'), dropShadowColor: this.getItemProperty(item, 'labelDropShadowColor'), dropShadowDistance: this.getItemProperty(item, 'labelDropShadowDistance'), fontFamily: this.getItemProperty(item, 'fontFamily'), fontSize: this.getItemProperty(item, 'fontSize'), fontStyle: this.getItemProperty(item, 'fontStyle'), fontWeight: this.getItemProperty(item, 'fontWeight'), stroke: this.getItemProperty(item, 'labelStroke'), strokeThickness: this.getItemProperty(item, 'labelStrokeThickness'), wordWrap: this.getItemProperty(item, 'wordWrap'), wordWrapWidth: this.getItemProperty(item, 'wordWrapWidth'), fill: this.getItemProperty(item, 'labelFill'), fillGradientType: this.getItemProperty(item, 'labelFillGradientType'), fillGradientStops: this.getItemProperty(item, 'labelFillGradientStops'), fontVariant: this.getItemProperty(item, 'fontVariant'), leading: this.getItemProperty(item, 'labelLeading'), letterSpacing: this.getItemProperty(item, 'letterSpacing'), lineHeight: this.getItemProperty(item, 'labelLineHeight'), lineJoin: this.getItemProperty(item, 'labelLineJoin'), miterLimit: this.getItemProperty(item, 'labelMiterLimit'), padding: this.getItemProperty(item, 'labelPadding'), trim: this.getItemProperty(item, 'labelTrim'), textBaseline: this.getItemProperty(item, 'textBaseline'), whiteSpace: this.getItemProperty(item, 'labelWhiteSpace') } } _drawGeometry(item) { let multiPolygonArray = [] if (Array.isArray(item.geometry)) item.geometry.forEach(geometry => { let polygon = this._drawPolygon(geometry) if (polygon != null && polygon != []) multiPolygonArray.push(polygon) }) else { let polygon = this._drawPolygon(item.geometry) if (polygon != null && polygon != []) multiPolygonArray.push(polygon) } const overlay = this return new GeoMultiShape(multiPolygonArray, { onDraw: function() { overlay._fill.call(overlay, this, item) } }) } _drawPolygon(geometry) { let polygon = [] if (geometry.type == 'Polygon') { if (GeoJson.validateCoordinates(geometry.type, geometry.coordinates)) { polygon = GeoJson.convert(geometry.type, geometry.coordinates) } else console.error(`Could not validate coordinates for type '${geometry.type}'.`, geometry.coordinates) } else { console.warn(`Geometry of type ${geometry.type} is not implemented yet, hence it was ignored.`) } return polygon } _drawRoutine(geographics, item, informationCallback = null) { this._createIcon(geographics, item) const enabled = this.getItemProperty(item, 'enabled') if (enabled) this._createInformation(geographics, item, informationCallback) } _createIcon(geographics, item) { let icon = this.getItemProperty(item, 'icon') if (icon) { if (icon.texture) { let sprite = new PIXI.Sprite(icon.texture) const iconAnchor = this.getItemProperty(item, 'iconAnchor') sprite.anchor.set(iconAnchor.x, iconAnchor.y) const iconScale = this.getItemProperty(item, 'iconScale') if (iconScale) sprite.scale.set(iconScale, iconScale) const iconOffset = this.getItemProperty(item, 'iconOffset') if (iconOffset && iconOffset.x != null && iconOffset.y != null) sprite.position.set(iconOffset.x, iconOffset.y) const iconColor = this.getItemProperty(item, 'iconColor') if (iconColor) sprite.tint = iconColor const iconAlpha = this.getItemProperty(item, 'iconAlpha') if (iconAlpha) sprite.alpha = iconAlpha geographics.graphics.addChild(sprite) } } } _createInformation(geographics, item, callback = null) { if (item.information) { /** * SO: The overlay class is quite convenient. * But managing the information creation solely inside this * class restricts us massively. * * Maybe a restructuring would be good, that we can handle * the creation of information with a callback and can adapt * to any occuring situation. * * e.g. * * overlay.informationHandler((geo, type, item)=>{ * switch(type){ * case "popup": * createPopup(item.information) * break; * * .... * } * }) */ geographics.graphics.interactive = true const informationType = this.getItemProperty(item, 'informationType') if (callback) callback.call(this, geographics, informationType, item) else { this.defaulInformationCallback(geographics, informationType, item) } } } defaulInformationCallback(geographics, informationType, item) { // Make the items interactive. geographics.graphics.interactive = true geographics.graphics.on('pointerdown', () => { if (!geographics.active) { geographics.active = true // Create and place the popup. let popup = new Popup({ header: item.name, content: item.information, maxWidth: 350 }) 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 = enabled ? this.getItemProperty(item, 'color') : this.getItemProperty(item, 'disabledColor') let alpha = enabled ? this.getItemProperty(item, 'fillAlpha') : this.getItemProperty(item, 'disabledAlpha') geographics.graphics.beginFill(color, alpha) } _drawPoint(item, informationCallback = null) { const overlay = this let geographic = new GeoPoint(item.location, { onDraw: function() { overlay._fill.call(overlay, this, item) const size = overlay.getItemProperty(item, 'size') ? overlay.getItemProperty(item, 'size') : 0 this.graphics.drawCircle(0, 0, size) overlay._drawRoutine.call(overlay, this, item, informationCallback) } }) return geographic } _logPropertyNotImplemented(property, value) { console.error(`Value '${value}' is not yet implemented in property '${property}'.`) } } Overlay.Modes = { auto: null } Overlay.createFactory = function(name, geographicsFactory) { if (Overlay.Modes.hasOwnProperty(name)) console.warn(`The mode ${name} was already implemented and was overwritten!`) Overlay.Modes[name] = geographicsFactory } Overlay.requestFactory = function(name) { return Overlay.Modes[name] } Overlay.listFactories = function() { let modes = [] for (let key of Object.keys(Overlay.Modes)) { modes.push(key) } return modes }