417 lines
14 KiB
JavaScript
417 lines
14 KiB
JavaScript
/* Copyright (c) 2016 Jean-Marc VIGLINO,
|
|
released under the CeCILL-B license (French BSD license)
|
|
(http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
|
|
*/
|
|
|
|
import {unByKey as ol_Observable_unByKey} from 'ol/Observable.js'
|
|
import ol_layer_Image from 'ol/layer/Image.js'
|
|
import ol_source_ImageCanvas from 'ol/source/ImageCanvas.js'
|
|
import ol_style_Style from 'ol/style/Style.js'
|
|
import ol_style_Fill from 'ol/style/Fill.js'
|
|
import {asString as ol_color_asString} from 'ol/color.js'
|
|
|
|
import ol_ext_element from '../util/element.js'
|
|
import ol_Overlay_Popup from './Popup.js'
|
|
import {ol_coordinate_dist2d} from "../geom/GeomUtils.js";
|
|
|
|
/**
|
|
* A popup element to be displayed over the map and attached to a single map
|
|
* location. The popup are customized using CSS.
|
|
*
|
|
* @constructor
|
|
* @extends {ol_Overlay_Popup}
|
|
* @fires show
|
|
* @fires hide
|
|
* @param {} options Extend Overlay options
|
|
* @param {String} options.popupClass the a class of the overlay to style the popup.
|
|
* @param {ol.style.Style} options.style a style to style the link on the map.
|
|
* @param {number} options.minScale min scale for the popup, default .5
|
|
* @param {number} options.maxScale max scale for the popup, default 2
|
|
* @param {bool} options.closeBox popup has a close box, default false.
|
|
* @param {function|undefined} options.onclose: callback function when popup is closed
|
|
* @param {function|undefined} options.onshow callback function when popup is shown
|
|
* @param {Number|Array<number>} options.offsetBox an offset box
|
|
* @param {ol.OverlayPositioning | string | undefined} options.positioning
|
|
* the 'auto' positioning: the popup choose its positioning to stay on the map.
|
|
* @param {string} options.hook popup is hooked on the 'map' (and move with it) or on the 'viewport', default viewport.
|
|
* @api stable
|
|
*/
|
|
var ol_Overlay_FixedPopup = class olOverlayFixedPopup extends ol_Overlay_Popup {
|
|
constructor(options) {
|
|
options.anchor = false
|
|
options.positioning = options.positioning || 'center-center'
|
|
options.className = (options.className || '') + ' ol-fixPopup'
|
|
super(options)
|
|
|
|
this.set('minScale', options.minScale || .5)
|
|
this.set('maxScale', options.maxScale || 2)
|
|
this.set('hook', options.hook || 'viewport')
|
|
|
|
// Canvas for drawing inks
|
|
var canvas = document.createElement('canvas')
|
|
|
|
this._coord = undefined;
|
|
this._overlay = new ol_layer_Image({
|
|
source: new ol_source_ImageCanvas({
|
|
canvasFunction: function (extent, res, ratio, size) {
|
|
canvas.width = size[0]
|
|
canvas.height = size[1]
|
|
return canvas
|
|
}
|
|
})
|
|
})
|
|
this._style = options.style || new ol_style_Style({
|
|
fill: new ol_style_Fill({ color: [102, 153, 255] })
|
|
})
|
|
|
|
this._overlay.on(['postcompose', 'postrender'], function (e) {
|
|
if (this.getVisible() && this._pixel) {
|
|
var map = this.getMap()
|
|
var position = this.getPosition()
|
|
var pixel = map.getPixelFromCoordinate(position)
|
|
var r1 = this.element.getBoundingClientRect()
|
|
var r2 = this.getMap().getTargetElement().getBoundingClientRect()
|
|
var pixel2 = [r1.left - r2.left + r1.width / 2, r1.top - r2.top + r1.height / 2]
|
|
e.context.save()
|
|
var tr = e.inversePixelTransform
|
|
if (tr) {
|
|
e.context.transform(tr[0], tr[1], tr[2], tr[3], tr[4], tr[5])
|
|
} else {
|
|
// ol ~ v5.3.0
|
|
e.context.scale(e.frameState.pixelRatio, e.frameState.pixelRatio)
|
|
}
|
|
e.context.beginPath()
|
|
e.context.moveTo(pixel[0], pixel[1])
|
|
if (Math.abs(pixel2[0] - pixel[0]) > Math.abs(pixel2[1] - pixel[1])) {
|
|
e.context.lineTo(pixel2[0], pixel2[1] - 8)
|
|
e.context.lineTo(pixel2[0], pixel2[1] + 8)
|
|
} else {
|
|
e.context.lineTo(pixel2[0] - 8, pixel2[1])
|
|
e.context.lineTo(pixel2[0] + 8, pixel2[1])
|
|
}
|
|
e.context.moveTo(pixel[0], pixel[1])
|
|
if (this._style.getFill()) {
|
|
e.context.fillStyle = ol_color_asString(this._style.getFill().getColor())
|
|
e.context.fill()
|
|
}
|
|
if (this._style.getStroke()) {
|
|
e.context.strokeStyle = ol_color_asString(this._style.getStroke().getColor())
|
|
e.context.lineWidth = this._style.getStroke().getWidth()
|
|
e.context.stroke()
|
|
}
|
|
e.context.restore()
|
|
}
|
|
}.bind(this))
|
|
var update = function () {
|
|
this.setPixelPosition()
|
|
}.bind(this)
|
|
this.on(['hide', 'show'], function () {
|
|
setTimeout(update)
|
|
}.bind(this))
|
|
|
|
// Get events centroid
|
|
function centroid(pevents) {
|
|
var clientX = 0
|
|
var clientY = 0
|
|
var length = 0
|
|
for (var i in pevents) {
|
|
clientX += pevents[i].clientX
|
|
clientY += pevents[i].clientY
|
|
length++
|
|
}
|
|
return [clientX / length, clientY / length]
|
|
}
|
|
// Get events angle
|
|
function angle() {
|
|
var p1, p2, v = Object.keys(pointerEvents)
|
|
if (v.length < 2)
|
|
return false
|
|
p1 = pointerEvents[v[0]]
|
|
p2 = pointerEvents[v[1]]
|
|
var v1 = [p2.clientX - p1.clientX, p2.clientY - p1.clientY]
|
|
p1 = pointerEvents2[v[0]]
|
|
p2 = pointerEvents2[v[1]]
|
|
var v2 = [p2.clientX - p1.clientX, p2.clientY - p1.clientY]
|
|
var d1 = Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1])
|
|
var d2 = Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1])
|
|
var a = Math.acos((v1[0] * v2[0] + v1[1] * v2[1]) / (d1 * d2)) * 360 / Math.PI
|
|
if (v1[0] * v2[1] - v1[1] * v2[0] < 0)
|
|
return -a
|
|
else
|
|
return a
|
|
}
|
|
// Get distance beetween events
|
|
function distance(pevents) {
|
|
var v = Object.keys(pevents)
|
|
if (v.length < 2)
|
|
return false
|
|
return ol_coordinate_dist2d([pevents[v[0]].clientX, pevents[v[0]].clientY], [pevents[v[1]].clientX, pevents[v[1]].clientY])
|
|
}
|
|
|
|
// Handle popup move
|
|
var pointerEvents = {}
|
|
var pointerEvents2 = {}
|
|
var pixelPosition = []
|
|
var distIni, rotIni, scaleIni, move
|
|
// down
|
|
this.element.addEventListener('pointerdown', function (e) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
// Reset events to this position
|
|
for (let i in pointerEvents) {
|
|
if (pointerEvents2[i]) {
|
|
pointerEvents[i] = pointerEvents2[i]
|
|
}
|
|
}
|
|
pointerEvents[e.pointerId] = e
|
|
pixelPosition = this._pixel || this.getMap().getPixelFromCoordinate(this.getPosition())
|
|
rotIni = this.get('rotation') || 0
|
|
scaleIni = this.get('scale') || 1
|
|
distIni = distance(pointerEvents)
|
|
move = false
|
|
}.bind(this))
|
|
// Prevent click when move
|
|
this.element.addEventListener('click', function (e) {
|
|
if (move) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
}, true)
|
|
// up / cancel
|
|
var removePointer = function (e) {
|
|
if (pointerEvents[e.pointerId]) {
|
|
delete pointerEvents[e.pointerId]
|
|
e.preventDefault()
|
|
}
|
|
if (pointerEvents2[e.pointerId]) {
|
|
delete pointerEvents2[e.pointerId]
|
|
}
|
|
/* Simulate a second touch pointer * /
|
|
if (e.metaKey || e.ctrlKey) {
|
|
pointerEvents['touch'] = e;
|
|
pointerEvents2['touch'] = e;
|
|
} else {
|
|
delete pointerEvents['touch'];
|
|
delete pointerEvents2['touch'];
|
|
}
|
|
/**/
|
|
}.bind(this)
|
|
document.addEventListener('pointerup', removePointer)
|
|
document.addEventListener('pointercancel', removePointer)
|
|
// move
|
|
document.addEventListener('pointermove', function (e) {
|
|
if (pointerEvents[e.pointerId]) {
|
|
e.preventDefault()
|
|
pointerEvents2[e.pointerId] = e
|
|
var c1 = centroid(pointerEvents)
|
|
var c2 = centroid(pointerEvents2)
|
|
var dx = c2[0] - c1[0]
|
|
var dy = c2[1] - c1[1]
|
|
move = move || Math.abs(dx) > 3 || Math.abs(dy) > 3
|
|
var a = angle()
|
|
if (a) {
|
|
this.setRotation(rotIni + a * 1.5, false)
|
|
}
|
|
var d = distance(pointerEvents2)
|
|
if (d !== false && distIni) {
|
|
this.setScale(scaleIni * d / distIni, false)
|
|
distIni = scaleIni * d / this.get('scale')
|
|
}
|
|
this.setPixelPosition([pixelPosition[0] + dx, pixelPosition[1] + dy])
|
|
}
|
|
}.bind(this))
|
|
}
|
|
/**
|
|
* Set the map instance the control is associated with
|
|
* and add its controls associated to this map.
|
|
* @param {_ol_Map_} map The map instance.
|
|
*/
|
|
setMap(map) {
|
|
super.setMap(map)
|
|
this._overlay.setMap(this.getMap())
|
|
if (this._listener) {
|
|
ol_Observable_unByKey(this._listener)
|
|
}
|
|
if (map) {
|
|
// Force popup inside the viewport
|
|
this._listener = map.on('change:size', function () {
|
|
this.setPixelPosition()
|
|
}.bind(this))
|
|
}
|
|
}
|
|
/** Update pixel position
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
updatePixelPosition() {
|
|
var map = this.getMap()
|
|
var position = this.getPosition()
|
|
if (!map || !map.isRendered() || !position) {
|
|
this.setVisible(false)
|
|
return
|
|
}
|
|
var mapSize = map.getSize();
|
|
var pixel;
|
|
if (!this._pixel) {
|
|
pixel = map.getPixelFromCoordinate(this.getPosition());
|
|
this.updateRenderedPosition(pixel, mapSize);
|
|
this._coord = map.getCoordinateFromPixel(pixel)
|
|
this._pixel = pixel;
|
|
}
|
|
if (this._pixel && this.get('hook') === 'map') {
|
|
pixel = map.getPixelFromCoordinate(this._coord);
|
|
super.updateRenderedPosition(pixel, mapSize);
|
|
this._pixel = pixel;
|
|
}
|
|
}
|
|
/** updateRenderedPosition
|
|
* @private
|
|
*/
|
|
updateRenderedPosition(pixel, mapsize) {
|
|
super.updateRenderedPosition(pixel, mapsize)
|
|
this.setRotation()
|
|
this.setScale()
|
|
}
|
|
/** Set pixel position
|
|
* @param {ol.pixel} pix
|
|
* @param {string} position top/bottom/middle-left/right/center
|
|
*/
|
|
setPixelPosition(pix, position) {
|
|
var r, map = this.getMap()
|
|
var mapSize = map ? map.getSize() : [0, 0]
|
|
if (position) {
|
|
this.setPositioning(position)
|
|
r = ol_ext_element.offsetRect(this.element)
|
|
r.width = r.height = 0
|
|
if (/top/.test(position))
|
|
pix[1] += r.height / 2
|
|
else if (/bottom/.test(position))
|
|
pix[1] = mapSize[1] - r.height / 2 - pix[1]
|
|
else
|
|
pix[1] = mapSize[1] / 2 + pix[1]
|
|
if (/left/.test(position))
|
|
pix[0] += r.width / 2
|
|
else if (/right/.test(position))
|
|
pix[0] = mapSize[0] - r.width / 2 - pix[0]
|
|
else
|
|
pix[0] = mapSize[0] / 2 + pix[0]
|
|
}
|
|
if (pix) {
|
|
this._pixel = pix
|
|
this._coord = map.getCoordinateFromPixel(pix)
|
|
}
|
|
if (map && map.getTargetElement() && this._pixel) {
|
|
this.updateRenderedPosition(this._pixel, mapSize)
|
|
// Prevent outside
|
|
var outside = false
|
|
r = ol_ext_element.offsetRect(this.element)
|
|
var rmap = ol_ext_element.offsetRect(map.getTargetElement())
|
|
if (r.left < rmap.left) {
|
|
this._pixel[0] = this._pixel[0] + rmap.left - r.left
|
|
outside = true
|
|
} else if (r.left + r.width > rmap.left + rmap.width) {
|
|
this._pixel[0] = this._pixel[0] + rmap.left - r.left + rmap.width - r.width
|
|
outside = true
|
|
}
|
|
if (r.top < rmap.top) {
|
|
this._pixel[1] = this._pixel[1] + rmap.top - r.top
|
|
outside = true
|
|
} else if (r.top + r.height > rmap.top + rmap.height) {
|
|
this._pixel[1] = this._pixel[1] + rmap.top - r.top + rmap.height - r.height
|
|
outside = true
|
|
}
|
|
if (outside && this.get('hook') !== 'map') {
|
|
this.updateRenderedPosition(this._pixel, mapSize)
|
|
}
|
|
this._overlay.changed()
|
|
}
|
|
}
|
|
/** Set pixel position
|
|
* @returns {ol.pixel}
|
|
*/
|
|
getPixelPosition() {
|
|
return this._pixel
|
|
}
|
|
/**
|
|
* Get the coordinate in view of the popup
|
|
*/
|
|
getCoordinate() {
|
|
return this._coord
|
|
}
|
|
/**
|
|
* Set the position of the popup.
|
|
* @param {ol.Coordinate|undefined} position Position.
|
|
* @api stable
|
|
*/
|
|
setCoordinate(position) {
|
|
this._coord = position
|
|
this.setPixelPosition()
|
|
}
|
|
/**
|
|
* Set the hook
|
|
* @param {string} [hook] 'map' or 'viewport', default viewport
|
|
*/
|
|
setHook(hook) {
|
|
this.set('hook', hook || 'viewport');
|
|
this._coord = this.get('hook') === 'map' ? this.getMap().getCoordinateFromPixel(this._pixel) : null
|
|
this.setPixelPosition()
|
|
}
|
|
/**
|
|
* Set the CSS class of the popup.
|
|
* @param {string} c class name.
|
|
* @api stable
|
|
*/
|
|
setPopupClass(c) {
|
|
super.setPopupClass(c)
|
|
this.addPopupClass('ol-fixPopup')
|
|
}
|
|
/** Set poppup rotation
|
|
* @param {number} angle
|
|
* @param {booelan} update update popup, default true
|
|
* @api
|
|
*/
|
|
setRotation(angle, update) {
|
|
if (typeof (angle) === 'number')
|
|
this.set('rotation', angle)
|
|
if (update !== false) {
|
|
if (/rotate/.test(this.element.style.transform)) {
|
|
this.element.style.transform = this.element.style.transform.replace(/rotate\((-?[\d,.]+)deg\)/, 'rotate(' + (this.get('rotation') || 0) + 'deg)')
|
|
} else {
|
|
this.element.style.transform = this.element.style.transform + ' rotate(' + (this.get('rotation') || 0) + 'deg)'
|
|
}
|
|
}
|
|
}
|
|
/** Set poppup scale
|
|
* @param {number} scale
|
|
* @param {booelan} update update popup, default true
|
|
* @api
|
|
*/
|
|
setScale(scale, update) {
|
|
if (typeof (scale) === 'number')
|
|
this.set('scale', scale)
|
|
scale = Math.min(Math.max(this.get('minScale') || 0, this.get('scale') || 1), this.get('maxScale') || 2)
|
|
this.set('scale', scale)
|
|
if (update !== false) {
|
|
if (/scale/.test(this.element.style.transform)) {
|
|
this.element.style.transform = this.element.style.transform.replace(/scale\(([\d,.]+)\)/, 'scale(' + (scale) + ')')
|
|
} else {
|
|
this.element.style.transform = this.element.style.transform + ' scale(' + (scale) + ')'
|
|
}
|
|
}
|
|
}
|
|
/** Set link style
|
|
* @param {ol.style.Style} style
|
|
*/
|
|
setLinkStyle(style) {
|
|
this._style = style
|
|
this._overlay.changed()
|
|
}
|
|
/** Get link style
|
|
* @return {ol.style.Style} style
|
|
*/
|
|
getLinkStyle() {
|
|
return this._style
|
|
}
|
|
}
|
|
|
|
export default ol_Overlay_FixedPopup |