iwmlib/lib/pixi/maps/overlay.js

583 lines
22 KiB
JavaScript

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
}