iwmlib/lib/pixi/maps/overlay.js

776 lines
28 KiB
JavaScript

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
}