Added the maps-module to the iwmlib.
Migrated a ot of the content from the tuesch to the iwmlib. This is before the decoupeling of the layers.
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
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 },
|
||||
|
||||
/**
|
||||
* 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({ name })
|
||||
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.place(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
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user