581 lines
21 KiB
JavaScript
581 lines
21 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
|
|
} = {}) {
|
|
const name = this.name[0].toUpperCase() + this.name.slice(1).toLowerCase() + ' Overlay'
|
|
let geoLayer = new GeoLayer(new PIXI.Container(), { name })
|
|
|
|
console.log(this)
|
|
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)
|
|
}
|
|
})
|
|
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
|
|
}
|