2019-11-04 10:59:08 +01:00
|
|
|
import { MapObjectScatter } from './scatter.js'
|
|
|
|
import { DeepZoomImage, DeepZoomInfo } from '../deepzoom/image.js'
|
2019-12-11 16:45:26 +01:00
|
|
|
import { MapProjection, DeepZoomMapProjection } from './mapprojection.js'
|
2019-11-04 10:59:08 +01:00
|
|
|
import { Points } from '../../utils.js'
|
|
|
|
import { EventHandler } from './utils.js'
|
|
|
|
import Mercator from './projections/mercator.js'
|
|
|
|
|
|
|
|
/**
|
2019-12-11 16:45:26 +01:00
|
|
|
* The GeoMap class displays a map, that it gets from MapProjection object.
|
2019-11-04 10:59:08 +01:00
|
|
|
* It handles the current location on the map, the zoom factor, interactions and
|
|
|
|
* the viewport, the area of the map, the user can see and navigate to.
|
|
|
|
*
|
|
|
|
* You should not instantiate GeoMap on it's own. Use the implemented child classes
|
|
|
|
* or derive a new one from it.
|
|
|
|
*
|
|
|
|
* Note: The name Map is already in use. Therefore GeoMap was used instead.
|
|
|
|
*
|
|
|
|
|
|
|
|
* @abstract
|
|
|
|
* @class
|
|
|
|
* @see {@link maps.html}
|
|
|
|
*/
|
|
|
|
|
|
|
|
export class GeoMap {
|
|
|
|
/**
|
|
|
|
* Creates instance of GeoMap
|
|
|
|
*
|
|
|
|
* @constructor
|
2019-12-11 16:45:26 +01:00
|
|
|
* @param {MapProjection}[mapProjection={}] - The map projection describes how the map has to be interpreted by the Map class. E.g. what are the boundaries of the map?
|
2019-11-04 10:59:08 +01:00
|
|
|
* @param {object}[opts={}] - With the opts, the created MapObjectScatter can be adjusted.
|
|
|
|
* @param {boolean}[opts.cover=false] - Enables covering behaviour of a map object. Normally maps should cover the whole app.
|
|
|
|
*/
|
|
|
|
constructor(
|
2019-12-11 16:45:26 +01:00
|
|
|
mapProjection = {},
|
2019-11-04 10:59:08 +01:00
|
|
|
{
|
|
|
|
debug = true,
|
|
|
|
cover = true,
|
|
|
|
// Scatter Options
|
|
|
|
alpha = 1,
|
|
|
|
startScale = 1,
|
|
|
|
minScale = 0,
|
|
|
|
maxScale = Infinity,
|
|
|
|
translatable = true,
|
|
|
|
scalable = true,
|
|
|
|
rotatable = false, // Many functionalities are not supported when rotating the map. Mainly the cover mechanism.
|
2019-12-11 16:45:26 +01:00
|
|
|
viewport = mapProjection.maxViewport,
|
2019-11-04 10:59:08 +01:00
|
|
|
// Events
|
|
|
|
onLoad = null,
|
|
|
|
onTransform = null
|
|
|
|
} = {}
|
|
|
|
) {
|
|
|
|
this._id = GeoMap.counter++
|
|
|
|
this.onLoad = new EventHandler('loaded', { listeners: onLoad })
|
|
|
|
this.onTransform = new EventHandler('transform', { listeners: onTransform })
|
|
|
|
|
2019-11-25 18:04:11 +01:00
|
|
|
this._alpha = alpha
|
2019-12-11 16:45:26 +01:00
|
|
|
this._cover = cover
|
2019-11-04 10:59:08 +01:00
|
|
|
this.debug = debug
|
|
|
|
|
|
|
|
//TODO discuss if this is required here.
|
|
|
|
// Those are just scatter options and the information
|
|
|
|
// is redundant in the map class and the scatter.
|
|
|
|
this.startScale = startScale
|
|
|
|
this.minScale = minScale
|
|
|
|
this.maxScale = maxScale
|
|
|
|
this.rotatable = rotatable
|
|
|
|
this.translatable = translatable
|
|
|
|
this.scalable = scalable
|
|
|
|
this.viewport = viewport
|
|
|
|
|
2019-12-11 16:45:26 +01:00
|
|
|
this._mapProjection = mapProjection
|
2019-11-04 10:59:08 +01:00
|
|
|
this.overlays = {}
|
|
|
|
|
|
|
|
/**
|
2019-12-11 16:45:26 +01:00
|
|
|
* Adjust the viewport depending on the mapProjection clipping.
|
2019-11-04 10:59:08 +01:00
|
|
|
*/
|
2019-12-11 16:45:26 +01:00
|
|
|
if (this.mapProjection.clip) {
|
2019-11-04 10:59:08 +01:00
|
|
|
const vp = this.viewport
|
2019-12-11 16:45:26 +01:00
|
|
|
const cp = this.mapProjection.clip
|
2019-11-04 10:59:08 +01:00
|
|
|
let bounds = {
|
|
|
|
min: {
|
|
|
|
x: vp.min.x > cp.min.x ? vp.min.x : cp.min.x,
|
|
|
|
y: vp.min.y > cp.min.y ? vp.min.y : cp.min.y
|
|
|
|
},
|
|
|
|
max: {
|
|
|
|
x: vp.max.x < cp.max.x ? vp.max.x : cp.max.x,
|
|
|
|
y: vp.max.y < cp.max.y ? vp.max.y : cp.max.y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.viewport = bounds
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-11 16:45:26 +01:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if the scatter covers the container.
|
|
|
|
* @member {boolean}
|
|
|
|
* @readonly
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
|
|
|
get cover() {
|
|
|
|
return this._cover
|
|
|
|
}
|
|
|
|
|
2019-12-11 15:29:59 +01:00
|
|
|
/**
|
|
|
|
* Returns the image object used by the GeoMap.
|
|
|
|
*
|
|
|
|
* @readonly
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
|
|
|
get image() {
|
|
|
|
return this._image
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-12-11 16:45:26 +01:00
|
|
|
* The mapProjection of the map.
|
2019-12-11 15:29:59 +01:00
|
|
|
*
|
2019-12-11 16:45:26 +01:00
|
|
|
* @member {MapProjection}
|
2019-12-11 15:29:59 +01:00
|
|
|
* @readonly
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-12-11 16:45:26 +01:00
|
|
|
get mapProjection() {
|
|
|
|
return this._mapProjection
|
2019-12-11 15:29:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears all EventHandlers.
|
|
|
|
*
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-11-04 10:59:08 +01:00
|
|
|
flushHandlers() {
|
2019-12-09 18:15:28 +01:00
|
|
|
this.onLoad.empty()
|
2019-11-04 10:59:08 +01:00
|
|
|
this.onTransform.empty()
|
|
|
|
}
|
|
|
|
|
2019-12-09 18:15:28 +01:00
|
|
|
/**
|
|
|
|
* Locks all transformations on the map.
|
|
|
|
* Single parameters can be set if necessary. False means the value is locked, true means they can be modified.
|
|
|
|
*
|
|
|
|
* @public
|
|
|
|
* @param {object} [{ rotatable = false, translatable = false, movableX = false, movableY = false, scalable = false }={}]
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-11-04 10:59:08 +01:00
|
|
|
lock({ rotatable = false, translatable = false, movableX = false, movableY = false, scalable = false } = {}) {
|
|
|
|
if (this.image && this.image.scatter) {
|
|
|
|
this.image.scatter.translatable = rotatable
|
|
|
|
this.image.scatter.movableX = translatable
|
|
|
|
this.image.scatter.movableY = movableX
|
|
|
|
this.image.scatter.rotatable = movableY
|
|
|
|
this.image.scatter.scalable = scalable
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-09 18:15:28 +01:00
|
|
|
/**
|
|
|
|
* Unlocks all transformations on the map.
|
|
|
|
* Single parameters can be set if necessary. False means the value is locked, true means they can be modified.
|
|
|
|
*
|
|
|
|
* @public
|
|
|
|
* @param {object} [{ rotatable = false, translatable = false, movableX = false, movableY = false, scalable = false }={}]
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-11-04 10:59:08 +01:00
|
|
|
unlock({ rotatable = true, translatable = true, movableX = true, movableY = true, scalable = true } = {}) {
|
2019-12-09 18:15:28 +01:00
|
|
|
this.lock({ rotatable, translatable, movableX, movableY, scalable })
|
2019-11-04 10:59:08 +01:00
|
|
|
}
|
|
|
|
|
2019-12-09 18:15:28 +01:00
|
|
|
/**
|
|
|
|
* Unloads the image of the map.
|
|
|
|
*
|
|
|
|
* @public
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-11-04 10:59:08 +01:00
|
|
|
unload() {
|
2019-11-04 18:20:32 +01:00
|
|
|
if (this.image) {
|
2019-11-05 11:07:36 +01:00
|
|
|
if (this.image.parent) {
|
|
|
|
this.image.parent.removeChild(this.image)
|
|
|
|
}
|
2019-11-25 18:04:11 +01:00
|
|
|
|
|
|
|
if (this.scatter) {
|
|
|
|
this.scatter.killAnimation()
|
|
|
|
this.image.scatter = null
|
|
|
|
}
|
2019-11-04 18:20:32 +01:00
|
|
|
}
|
2019-11-04 10:59:08 +01:00
|
|
|
}
|
|
|
|
|
2019-12-09 18:15:28 +01:00
|
|
|
/**
|
|
|
|
* Removes the map, freeing all memory ba flushing handlers and removing the image.
|
|
|
|
*
|
|
|
|
* @public
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-11-25 18:04:11 +01:00
|
|
|
remove() {
|
|
|
|
if (this.image) this.image.mask = null
|
|
|
|
|
|
|
|
this.removeFrame()
|
|
|
|
this.onTransform.empty()
|
|
|
|
this.onLoad.empty()
|
|
|
|
|
|
|
|
this.unload()
|
|
|
|
}
|
|
|
|
|
2019-11-04 10:59:08 +01:00
|
|
|
/**
|
|
|
|
* Is called when the scatter object is transformed.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {ScatterEvent}[e] - Contains informations on how the element was transformed.
|
|
|
|
*/
|
|
|
|
transformed(e) {
|
|
|
|
this.onTransform.call(this, e)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wrapps the display object around a scatter object.
|
2019-12-11 15:29:59 +01:00
|
|
|
* creates the image.
|
2019-11-04 10:59:08 +01:00
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {DisplayObject} displayObject - Defines the display object that will be wrapped inside the scatter object.
|
|
|
|
* @param {PIXI.Container} container - Defines the frame in which the map will be displayed.
|
|
|
|
*/
|
|
|
|
load(image, renderer, frame = null, scatter = null) {
|
|
|
|
if (this.debug) console.log('Load image: ', image, frame)
|
|
|
|
|
2019-12-11 15:29:59 +01:00
|
|
|
this._image = image
|
2019-11-04 10:59:08 +01:00
|
|
|
if (frame) this.setFrame(frame)
|
|
|
|
|
|
|
|
let scatterOpts = Object.assign({
|
|
|
|
cover: this.cover,
|
|
|
|
scaleable: this.scaleable,
|
|
|
|
translatable: this.translatable,
|
|
|
|
rotatable: this.rotatable,
|
|
|
|
debug: this.debug,
|
|
|
|
startScale: this.startScale,
|
|
|
|
minScale: this.minScale,
|
|
|
|
maxScale: this.maxScale,
|
|
|
|
onTransform: this.transformed.bind(this)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.scatter = new MapObjectScatter(image, renderer, scatterOpts)
|
|
|
|
this.image.scatter = scatter == null ? this.scatter : scatter
|
|
|
|
|
|
|
|
this.onLoad.call(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates an identical copy of the current map.
|
|
|
|
*/
|
|
|
|
clone() {
|
|
|
|
console.error('Method clone() is abstract. Overload it in subclass', this)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* Moves the 'camera' to another location on the map.
|
|
|
|
*
|
|
|
|
* @abstract
|
|
|
|
* @param {object} coordinates - Target coordinates in form {x: latitude, y: longitude}
|
|
|
|
* @param {number} scale - Target scale.
|
|
|
|
*/
|
|
|
|
moveTo(coordinates, zoom = null, { animate = false } = {}) {
|
|
|
|
console.error(this, 'The method moveTo(coordinates, zoom, opts) has not been defined in child class.')
|
|
|
|
}
|
|
|
|
|
|
|
|
zoomTo(zoomFactor) {
|
|
|
|
console.error('Method zoomTo() is abstract. Overload it in subclass', this, zoomFactor)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reloads the textures of the image, when it was unloaded and is required again.
|
|
|
|
*
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
|
|
|
reloadImage() {
|
|
|
|
console.error(`Call of abstract method reloadImage(). Overwrite in subclass.`, this)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unloads the textures of the image, when not in use.
|
|
|
|
*
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
|
|
|
unloadImage() {
|
|
|
|
console.error(`Call of abstract method unloadImage(). Overwrite in subclass.`, this)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transforms a point at pixel position {x,y}
|
|
|
|
* to a coordinate with latitude and longitude.
|
|
|
|
*
|
|
|
|
*
|
2019-12-09 18:15:28 +01:00
|
|
|
* @param {object} point - Point in form of {x, y}.
|
2019-11-04 10:59:08 +01:00
|
|
|
* @returns {object} - Coordinates on the map in form of {x: latitude, y: longitude}.
|
|
|
|
*/
|
|
|
|
coordinatesFromPoint(point) {
|
2019-12-11 16:45:26 +01:00
|
|
|
let coords = this.mapProjection.toCoordinates(this.toRelativePosition(point))
|
2019-11-04 10:59:08 +01:00
|
|
|
return coords
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transform coordinates in the map into pixel positions on the deep zoom image.
|
|
|
|
*
|
|
|
|
* @param {object} coords - Coordinates of a map position in form {x: latitude, y: longitude}.
|
2019-12-09 18:15:28 +01:00
|
|
|
* @return {Point} - Returns a image position in form of {x: x, y: y}.
|
2019-11-04 10:59:08 +01:00
|
|
|
*/
|
|
|
|
coordinatesToPoint(coordinates) {
|
2019-12-11 16:45:26 +01:00
|
|
|
return this.toAbsolutePixelCoordinates(this.mapProjection.toPixel(coordinates))
|
2019-11-04 10:59:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
toRelativePosition(point) {
|
|
|
|
let _point = new PIXI.Point()
|
|
|
|
|
|
|
|
_point.x = point.x / this.width
|
|
|
|
_point.y = point.y / this.height
|
|
|
|
|
|
|
|
return _point
|
|
|
|
}
|
|
|
|
|
|
|
|
toAbsolutePixelCoordinates(point) {
|
|
|
|
let _point = new PIXI.Point()
|
|
|
|
_point.x = point.x * this.width
|
|
|
|
_point.y = point.y * this.height
|
|
|
|
|
|
|
|
return _point
|
|
|
|
}
|
|
|
|
|
|
|
|
get width() {
|
|
|
|
return this.image.scatter.width / this.image.scatter.scale
|
|
|
|
}
|
|
|
|
|
|
|
|
get height() {
|
|
|
|
return this.image.scatter.height / this.image.scatter.scale
|
|
|
|
}
|
|
|
|
|
|
|
|
get distance() {
|
|
|
|
console.error('Overload get distance in subclass.')
|
|
|
|
}
|
|
|
|
|
2019-11-25 18:04:11 +01:00
|
|
|
get alpha() {
|
|
|
|
return this._alpha
|
2019-11-20 15:59:10 +01:00
|
|
|
}
|
|
|
|
|
2019-11-04 10:59:08 +01:00
|
|
|
/**
|
|
|
|
* Returns a screen point for a coordinate.
|
|
|
|
*/
|
|
|
|
positionInWindow(coordinates) {
|
|
|
|
let pos = this.coordinatesToPoint(coordinates)
|
|
|
|
|
|
|
|
let framePos = {
|
|
|
|
x: this.image.position.x + pos.x * this.image.scale.x,
|
|
|
|
y: this.image.position.y + pos.y * this.image.scale.y
|
|
|
|
}
|
|
|
|
|
|
|
|
return framePos
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the coordinates for a screen point.
|
|
|
|
*/
|
|
|
|
coordinatesInWindow(point) {
|
|
|
|
let mapPos = {
|
|
|
|
x: (point.x - this.image.position.x) / this.image.scale.x,
|
|
|
|
y: (point.y - this.image.position.y) / this.image.scale.y
|
|
|
|
}
|
|
|
|
|
|
|
|
let coords = this.coordinatesFromPoint(mapPos)
|
|
|
|
return coords
|
|
|
|
}
|
|
|
|
|
2019-11-25 18:04:11 +01:00
|
|
|
removeFrame() {
|
|
|
|
this.frame = null
|
|
|
|
}
|
|
|
|
|
2019-12-11 15:29:59 +01:00
|
|
|
/**
|
|
|
|
* The complete Triforce, or one or more components of the Triforce.
|
|
|
|
* @typedef {Object} Frame
|
|
|
|
* @property {number} x - X position of the frame.
|
|
|
|
* @property {number} y - Y position of the frame.
|
|
|
|
* @property {number} width - Width of the frame.
|
|
|
|
* @property {number} height - Height od the frame.
|
|
|
|
* @property {Point} localCenter - Local center of the map.
|
|
|
|
* @property {Point} center - Global center of the map.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the frame if the map.
|
|
|
|
*
|
|
|
|
* Frame is the display in which the map is shown.
|
|
|
|
* Normally it's the app, but it can be another element,
|
|
|
|
* for example when in a submap.
|
|
|
|
*
|
|
|
|
* @param {Frame} frame
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-11-04 10:59:08 +01:00
|
|
|
setFrame(frame) {
|
|
|
|
this.frame = frame
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-12-11 15:29:59 +01:00
|
|
|
* Gets the frame if the map.
|
|
|
|
*
|
2019-11-04 10:59:08 +01:00
|
|
|
* Frame is the display in which the map is shown.
|
|
|
|
* Normally it's the app, but it can be another element,
|
|
|
|
* for example when in a submap.
|
2019-12-11 15:29:59 +01:00
|
|
|
*
|
|
|
|
* @returns {Frame} - Returns the frame of the map.
|
|
|
|
* @memberof GeoMap
|
2019-11-04 10:59:08 +01:00
|
|
|
*/
|
|
|
|
getFrame() {
|
|
|
|
let frame = {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: 0,
|
|
|
|
height: 0
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.frame) {
|
|
|
|
//The app does not have an x and y attribute.
|
|
|
|
//Therefore we need the additional check.
|
|
|
|
frame = {
|
|
|
|
x: this.frame.x ? this.frame.x : 0,
|
|
|
|
y: this.frame.y ? this.frame.y : 0,
|
|
|
|
width: this.frame.width,
|
|
|
|
height: this.frame.height
|
|
|
|
}
|
|
|
|
} else if (this.image.parent) {
|
|
|
|
let parent = this.image.parent
|
|
|
|
|
|
|
|
for (let key of Object.keys(frame)) {
|
|
|
|
frame[key] = parent[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
frame.localCenter = {
|
|
|
|
x: frame.width / 2,
|
|
|
|
y: frame.height / 2
|
|
|
|
}
|
|
|
|
|
|
|
|
frame.center = {
|
|
|
|
x: frame.x + frame.localCenter.x,
|
|
|
|
y: frame.y + frame.localCenter.y
|
|
|
|
}
|
|
|
|
|
|
|
|
return frame
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves all maps from a json object.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @param {object} json - The parsed map data object.
|
|
|
|
* @returns {object} - Returns an object with the names as keys and the GeoMaps as value.
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
2019-11-20 15:59:10 +01:00
|
|
|
static allFromJson(json, root = './') {
|
2019-11-04 10:59:08 +01:00
|
|
|
let error = { message: '' }
|
|
|
|
let maps = {}
|
|
|
|
if (GeoMap._validateJson(json, error)) {
|
|
|
|
for (let [mapname, data] of Object.entries(json)) {
|
2019-11-20 15:59:10 +01:00
|
|
|
data.tiles.path = root + data.tiles.path
|
2019-11-04 10:59:08 +01:00
|
|
|
maps[mapname] = GeoMap._createMap(data)
|
2019-11-11 12:34:43 +01:00
|
|
|
maps[mapname].name = mapname
|
2019-11-04 10:59:08 +01:00
|
|
|
}
|
|
|
|
} else console.error('Could not validate JSON: ' + error.message)
|
|
|
|
|
|
|
|
return maps
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*Extracts a single map from a JSON map file.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @param {string} map - Name of the map.
|
|
|
|
* @param {object} json - Parsed json object.
|
|
|
|
* @returns {GeoMap} - Returns a geomap, if the map was in the data and valid. Otherwise false.
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
|
|
|
static mapFromJson(map, json) {
|
|
|
|
if (json[map]) {
|
|
|
|
const data = json[map]
|
|
|
|
if (this._validJsonMap(data)) return GeoMap._createMap(data)
|
|
|
|
else console.error('Map was not in a valid format.')
|
|
|
|
} else console.error('Map was not in data.')
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates if the map data contains valid data
|
|
|
|
* for creating the maps.
|
|
|
|
*
|
|
|
|
* @static
|
2019-12-11 15:29:59 +01:00
|
|
|
* @private
|
2019-11-04 10:59:08 +01:00
|
|
|
* @param {object} json - The object containing multiple map data sets.
|
|
|
|
* @param {error-object} error - An object that contains an parameter message: {message = ""}. This is faking a call by reference.
|
|
|
|
* @returns {boolean} - True if all sets were valid. False otherwise.
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
|
|
|
static _validateJson(json, error) {
|
|
|
|
let isValid = true
|
|
|
|
|
|
|
|
if (Object.keys(json).length == 0) {
|
|
|
|
isValid = false
|
|
|
|
error.message += 'The provided JSON object did not contain any items.'
|
|
|
|
}
|
|
|
|
for (let [name, data] of Object.entries(json)) {
|
|
|
|
if (!GeoMap._validJsonMap(data)) {
|
|
|
|
error.message += `${name} was not valid. `
|
|
|
|
isValid = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (error.message != '') error.message = 'Maps could not be validated: ' + error.message
|
|
|
|
|
|
|
|
return isValid
|
|
|
|
}
|
|
|
|
|
|
|
|
static changePathsInJson(json, path) {
|
|
|
|
for (let key of Object.keys(json)) {
|
|
|
|
json[key].icon = path + json[key].icon
|
|
|
|
json[key].tiles.path = path + json[key].tiles.path
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*Validates of a single data set contains the valid data for creating a map.
|
|
|
|
*
|
|
|
|
* @static
|
2019-12-11 15:29:59 +01:00
|
|
|
* @private
|
2019-11-04 10:59:08 +01:00
|
|
|
* @param {object} json - The object containing a single set of map data.
|
|
|
|
* @returns {boolean} - True if valid, otherwise false.
|
|
|
|
* @memberof GeoMap
|
|
|
|
*/
|
|
|
|
static _validJsonMap(json) {
|
|
|
|
let isJsonValid = true
|
|
|
|
const required = ['projection', 'type', 'icon']
|
|
|
|
for (const requirement of Object.values(required)) {
|
|
|
|
if (!json.hasOwnProperty(requirement)) {
|
|
|
|
isJsonValid = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isJsonValid && json.type.toLowerCase() == 'deepzoom') {
|
|
|
|
if (!json.hasOwnProperty('tiles')) isJsonValid = false
|
|
|
|
}
|
|
|
|
|
|
|
|
return isJsonValid
|
|
|
|
}
|
|
|
|
|
|
|
|
static _createMap(data) {
|
|
|
|
switch (data.type.toLowerCase()) {
|
|
|
|
case 'deepzoom':
|
|
|
|
return GeoMap._createDeepZoomMap(data)
|
|
|
|
default:
|
|
|
|
console.error(`Datatype is invalid or not implemented yet: ${data.type}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static _createDeepZoomMap(data) {
|
|
|
|
const projection = GeoMap._getProjectionByName(data.projection)
|
|
|
|
const tilesConfig = data.tiles
|
|
|
|
const options = data.options
|
|
|
|
|
2019-12-11 16:45:26 +01:00
|
|
|
const mapProjection = new DeepZoomMapProjection(projection, tilesConfig)
|
|
|
|
return new DeepZoomMap(mapProjection, tilesConfig, options)
|
2019-11-04 10:59:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static _getProjectionByName(projection) {
|
|
|
|
switch (projection.toLowerCase()) {
|
|
|
|
case 'mercator':
|
|
|
|
return new Mercator()
|
|
|
|
default:
|
|
|
|
console.error(`Projection is invalid or not implemented yet ${projection}.`)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
GeoMap.counter = 0
|
|
|
|
|
|
|
|
/**
|
2019-12-11 15:29:59 +01:00
|
|
|
* The DeepZoomMap class extends the GeoMap to create
|
2019-11-04 10:59:08 +01:00
|
|
|
* maps as deepzoom images from maptiles.
|
|
|
|
*
|
2019-12-11 15:29:59 +01:00
|
|
|
* @export
|
|
|
|
* @class DeepZoomMap
|
|
|
|
* @extends {GeoMap}
|
2019-11-04 10:59:08 +01:00
|
|
|
*/
|
|
|
|
export class DeepZoomMap extends GeoMap {
|
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @param {object} tilesConfig - The tiles config object, that defines at what path and in which format the tiles are.
|
2019-12-11 16:45:26 +01:00
|
|
|
* @param {MapProjection} mapProjection - A MapProjection object, that contains informations of how the given map has to be interpreted.
|
2019-11-04 10:59:08 +01:00
|
|
|
* @param {object} opts - Additional options to specify the behaviour of the deep zoom image.
|
|
|
|
*/
|
2019-12-11 16:45:26 +01:00
|
|
|
constructor(mapProjection, tilesConfig, opts = {}) {
|
2019-11-04 10:59:08 +01:00
|
|
|
opts = Object.assign(
|
|
|
|
{
|
|
|
|
maxScale: Math.min(tilesConfig.width, tilesConfig.height) / tilesConfig.tileSize,
|
2019-12-11 16:45:26 +01:00
|
|
|
minScale: mapProjection.getMinScale,
|
2019-11-04 10:59:08 +01:00
|
|
|
highResolution: true,
|
|
|
|
debug: false
|
|
|
|
},
|
|
|
|
opts
|
|
|
|
)
|
2019-12-11 16:45:26 +01:00
|
|
|
super(mapProjection, opts)
|
2019-11-04 10:59:08 +01:00
|
|
|
|
|
|
|
this.tilesConfig = tilesConfig
|
2019-12-11 16:45:26 +01:00
|
|
|
this._verifyMapProjection()
|
2019-11-04 10:59:08 +01:00
|
|
|
}
|
|
|
|
|
2019-12-11 16:45:26 +01:00
|
|
|
_verifyMapProjection() {
|
|
|
|
if (!(this.mapProjection instanceof MapProjection)) {
|
|
|
|
console.error('Use the MapProjection object for creating maps!')
|
2019-11-04 10:59:08 +01:00
|
|
|
} else {
|
2019-12-11 16:45:26 +01:00
|
|
|
if (!(this.mapProjection instanceof DeepZoomMapProjection)) {
|
|
|
|
console.error('Use the DeepZoomMapProjection object.')
|
2019-11-04 10:59:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the DeepZoomImage.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
load(container = null, scatter = null) {
|
2019-12-11 16:45:26 +01:00
|
|
|
if (!this.mapProjection.app) console.error('App was not set in the mapProjection.')
|
2019-11-04 10:59:08 +01:00
|
|
|
this.info = new DeepZoomInfo(this.tilesConfig)
|
|
|
|
let image = new DeepZoomImage(this.info, {
|
2019-12-11 16:45:26 +01:00
|
|
|
app: this.mapProjection.app,
|
2019-11-04 10:59:08 +01:00
|
|
|
alpha: this.alpha,
|
|
|
|
debug: this.debug,
|
|
|
|
world: scatter == null ? scatter : scatter.getWorldScatter()
|
|
|
|
})
|
|
|
|
|
|
|
|
super.load(image, container, scatter)
|
|
|
|
|
|
|
|
if (this.debug) console.log('Loaded image: ', image, 'With options: ', this.info)
|
|
|
|
}
|
|
|
|
|
|
|
|
unloadImage() {
|
|
|
|
if (this.image) {
|
|
|
|
this.image.deactivate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
reloadImage() {
|
|
|
|
this.image.activate()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event function that is invoked by the deep zoom image, when it has been transformed.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {ScatterEvent} e
|
|
|
|
*/
|
|
|
|
transformed(e) {
|
|
|
|
this.image.transformed(e)
|
|
|
|
super.transformed(e)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Jumps to a certain point on the map and adjusts the size of the
|
|
|
|
* map depending on the distance (zoom).
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param {*} coordinates
|
|
|
|
* @param {*} [zoom=null]
|
|
|
|
* @param {*} [{ animate = 0 }={}]
|
|
|
|
* @memberof DeepZoomMap
|
|
|
|
*/
|
|
|
|
moveTo(coordinates, zoom = null, { animate = 0 } = {}) {
|
|
|
|
if (this.image.scatter == null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
//TODO: This is jaggy on submaps. Make it more smooth. Severin Opel 08-05-2018
|
|
|
|
if (zoom) {
|
|
|
|
/**
|
|
|
|
* Zoom's the view to a specified distance in DZI-Levels.
|
|
|
|
* The input level is clipped to allowed levels.
|
|
|
|
* */
|
|
|
|
let targetScale = this.scaleForFloatingLevel(zoom)
|
|
|
|
|
|
|
|
// Don't scale beyond boundaries.
|
|
|
|
if (this.maxScale) targetScale = Math.min(this.maxScale, targetScale)
|
|
|
|
if (this.minScale) targetScale = Math.max(targetScale, this.minScale)
|
|
|
|
|
|
|
|
this.image.scatter.requestScale(targetScale)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (coordinates) {
|
|
|
|
//Get pixel position of lat lang values
|
|
|
|
let point = this.coordinatesToPoint(coordinates)
|
|
|
|
|
|
|
|
let containerCenter
|
|
|
|
if (this.frame) {
|
2019-11-20 15:59:10 +01:00
|
|
|
containerCenter = this.getFrame().localCenter
|
2019-11-04 10:59:08 +01:00
|
|
|
} else {
|
|
|
|
containerCenter = {
|
|
|
|
x: this.image.parent.width / 2,
|
|
|
|
y: this.image.parent.height / 2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Calculate the delta depending on image position and center the
|
|
|
|
// desired position in the frame.
|
|
|
|
let delta = {
|
|
|
|
x: containerCenter.x - point.x * this.image.scatter.scale - this.scatter.position.x,
|
|
|
|
y: containerCenter.y - point.y * this.image.scatter.scale - this.scatter.position.y
|
|
|
|
}
|
|
|
|
|
|
|
|
if (animate > 0) {
|
|
|
|
let last = 0
|
|
|
|
TweenMax.to({ x: 0, y: 0 }, animate, {
|
|
|
|
ease: Power0.easeNone,
|
|
|
|
x: delta.x,
|
|
|
|
y: delta.y,
|
|
|
|
onUpdateParams: ['{self}'],
|
|
|
|
onUpdate: tween => {
|
|
|
|
let step = tween.ratio - last
|
|
|
|
last = tween.ratio
|
|
|
|
this.image.scatter.transform(Points.multiplyScalar(delta, step), /*ratio*/ 1, 0, { x: 0, y: 0 })
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.image.scatter.transform(delta, /*ratio*/ 1, 0, { x: 0, y: 0 })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// return this.image.scatter.position
|
|
|
|
}
|
|
|
|
|
|
|
|
scaleForFloatingLevel(level) {
|
|
|
|
level = Math.max(0, level - this.image.info.baseLevel)
|
|
|
|
return Math.pow(2, level) / this.image.resolution
|
|
|
|
}
|
|
|
|
|
|
|
|
floatingLevelForScale(scale) {
|
|
|
|
let level = Math.log2(scale * this.image.resolution)
|
|
|
|
let newLevel = this.image.info.baseLevel + Math.max(level, 0)
|
|
|
|
return Math.min(newLevel, this.image.info.maxLoadableLevel)
|
|
|
|
}
|
|
|
|
|
|
|
|
get distance() {
|
|
|
|
return this.floatingLevelForScale(this.scatter.scale)
|
|
|
|
}
|
|
|
|
|
|
|
|
clone(container, scatter = null) {
|
2019-12-11 16:45:26 +01:00
|
|
|
const map = new DeepZoomMap(this.mapProjection, this.tilesConfig, {
|
2019-11-04 10:59:08 +01:00
|
|
|
alpha: this.alpha,
|
2019-12-11 16:45:26 +01:00
|
|
|
cover: this.image.scatter.cover,
|
2019-11-04 10:59:08 +01:00
|
|
|
debug: this.debug,
|
|
|
|
startScale: this.startScale,
|
|
|
|
minScale: this.minScale,
|
|
|
|
maxScale: this.maxScale,
|
|
|
|
rotatable: this.rotatable,
|
|
|
|
translatable: this.translatable,
|
|
|
|
scalable: this.scalable,
|
|
|
|
viewport: this.viewport
|
|
|
|
})
|
|
|
|
|
|
|
|
return map
|
|
|
|
}
|
|
|
|
|
|
|
|
tint() {
|
|
|
|
let color = DeepZoomMap.tintcolors[DeepZoomMap.tintcolor++ % DeepZoomMap.tintcolors.length]
|
2019-11-20 15:59:10 +01:00
|
|
|
|
|
|
|
this._forEachTile(tile => {
|
|
|
|
tile.tint = color
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
_forEachTile(callback) {
|
2019-11-04 10:59:08 +01:00
|
|
|
this.image.children[0].children.forEach(tiles => {
|
2019-11-20 15:59:10 +01:00
|
|
|
tiles.children.forEach(callback)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
setAlpha(alpha) {
|
|
|
|
this._forEachTile(tile => {
|
|
|
|
tile.alpha = alpha
|
2019-11-04 10:59:08 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// /** Returns the tile layer level that corresponds to the given scale.
|
|
|
|
// * @param {number} scale - the scale factor
|
|
|
|
// **/
|
|
|
|
// levelForScale(scale) {
|
|
|
|
// let level = Math.round(Math.log2(scale * this.resolution)) // Math.floor(Math.log2(event.scale))+1
|
|
|
|
// let newLevel = this.info.baseLevel + Math.max(level, 0)
|
|
|
|
// return Math.min(newLevel, this.info.maxLoadableLevel)
|
|
|
|
// }
|
|
|
|
|
|
|
|
// /**
|
|
|
|
// * Returns the scale factor that correspond to a given level.
|
|
|
|
// * @param {number} level - the requested level
|
|
|
|
// * @returns {number} - Returns the scale level for the given level.
|
|
|
|
// */
|
|
|
|
// scaleForLevel(level){
|
|
|
|
// level = Math.max(0, level - this.info.baseLevel)
|
|
|
|
// return Math.pow(2, level) / this.resolution
|
|
|
|
// }
|
|
|
|
}
|
|
|
|
|
|
|
|
DeepZoomMap.tintcolors = [0xff0000, 0xff00ff, 0xffff00, 0x00ff00, 0x00ffff, 0x0000ff]
|
|
|
|
DeepZoomMap.tintcolor = 0
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ImageMap extends GeoMap to display simple images
|
|
|
|
* as maps.
|
2019-12-11 15:29:59 +01:00
|
|
|
*
|
|
|
|
* @export
|
|
|
|
* @class ImageMap
|
|
|
|
* @extends {GeoMap}
|
2019-11-04 10:59:08 +01:00
|
|
|
*/
|
|
|
|
export class ImageMap extends GeoMap {
|
2019-12-11 16:45:26 +01:00
|
|
|
constructor(sprite, mapProjection, opts = {}) {
|
|
|
|
super(mapProjection, opts)
|
|
|
|
if (this.debug) console.log('Construct Image Map', sprite, mapProjection, opts)
|
2019-11-04 10:59:08 +01:00
|
|
|
|
|
|
|
this.sprite = sprite
|
|
|
|
|
|
|
|
this.baseZoomHeight = opts.baseZoomHeight ? opts.baseZoomHeight : this.sprite.texture.height
|
|
|
|
}
|
|
|
|
|
|
|
|
load(container = null, scatter = null) {
|
|
|
|
super.load(this.sprite, container, scatter)
|
|
|
|
this.image.alpha = this.alpha
|
|
|
|
this.image.interactive = true
|
|
|
|
}
|
|
|
|
|
|
|
|
clone(container = null, scatter = null) {
|
2019-12-11 16:45:26 +01:00
|
|
|
const map = new ImageMap(new PIXI.Sprite(this.sprite.texture), this.mapProjection, {
|
2019-11-04 10:59:08 +01:00
|
|
|
alpha: this.alpha,
|
2019-12-11 16:45:26 +01:00
|
|
|
cover: this.image.cover,
|
2019-11-04 10:59:08 +01:00
|
|
|
debug: this.debug,
|
|
|
|
startScale: this.startScale,
|
|
|
|
minScale: this.minScale,
|
|
|
|
maxScale: this.maxScale,
|
|
|
|
rotatable: this.rotatable,
|
|
|
|
translatable: this.translatable,
|
|
|
|
scalable: this.scalable,
|
|
|
|
viewport: this.viewport
|
|
|
|
})
|
|
|
|
map.load(container, scatter)
|
|
|
|
|
|
|
|
return map
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param {*} coordinates
|
|
|
|
* @param {*} [zoom=null]
|
|
|
|
* @param {*} [{ animate = 0 }={}]
|
|
|
|
* @memberof ImageMap
|
|
|
|
*/
|
|
|
|
moveTo(coordinates, zoom = null, { animate = 0 } = {}) {
|
|
|
|
if (this.image.scatter == null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
//TODO: This is jaggy on submaps. Make it more smooth. Severin Opel 08-05-2018
|
|
|
|
if (zoom) {
|
|
|
|
/**
|
|
|
|
* Zoom's the view to a specified distance in DZI-Levels.
|
|
|
|
* The input level is clipped to allowed levels.
|
|
|
|
* */
|
|
|
|
|
|
|
|
let targetScale = zoom * this.zoomFactor
|
|
|
|
|
|
|
|
// Don't scale beyond boundaries.
|
|
|
|
if (this.maxScale) targetScale = Math.min(this.maxScale, targetScale)
|
|
|
|
|
|
|
|
if (this.minScale) targetScale = Math.max(targetScale, this.minScale)
|
|
|
|
|
|
|
|
this.image.scatter.requestScale(targetScale)
|
|
|
|
}
|
|
|
|
|
|
|
|
//coordinates = null
|
|
|
|
if (coordinates) {
|
|
|
|
//Get pixel position of lat lang values
|
|
|
|
let point = this.coordinatesToPoint(coordinates)
|
|
|
|
|
|
|
|
let containerCenter
|
|
|
|
if (this.frame) {
|
|
|
|
containerCenter = this.getFrame().localCenter
|
|
|
|
} else {
|
|
|
|
containerCenter = {
|
|
|
|
x: this.image.parent.width / 2,
|
|
|
|
y: this.image.parent.height / 2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Calculate the delta depending on image position and center the
|
|
|
|
// desired position in the frame.
|
|
|
|
|
|
|
|
let delta = {
|
|
|
|
x: containerCenter.x - point.x * this.image.scatter.scale - this.scatter.position.x,
|
|
|
|
y: containerCenter.y - point.y * this.image.scatter.scale - this.scatter.position.y
|
|
|
|
}
|
|
|
|
|
|
|
|
if (animate > 0) {
|
|
|
|
let last = 0
|
|
|
|
TweenMax.to({ x: 0, y: 0 }, animate, {
|
|
|
|
ease: Power0.easeNone,
|
|
|
|
x: delta.x,
|
|
|
|
y: delta.y,
|
|
|
|
onUpdateParams: ['{self}'],
|
|
|
|
onUpdate: tween => {
|
|
|
|
let step = tween.ratio - last
|
|
|
|
last = tween.ratio
|
|
|
|
this.image.scatter.transform(Points.multiplyScalar(delta, step), /*ratio*/ 1, 0, { x: 0, y: 0 })
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.image.scatter.transform(delta, /*ratio*/ 1, 0, { x: 0, y: 0 })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get zoom() {
|
|
|
|
return this.scatter.scale / this.zoomFactor
|
|
|
|
}
|
|
|
|
|
|
|
|
get zoomFactor() {
|
|
|
|
let factor = this.baseZoomHeight / this.sprite.texture.height
|
|
|
|
return factor
|
|
|
|
}
|
|
|
|
}
|