ol-ext
Version:
A set of cool extensions for OpenLayers (ol) in node modules structure
417 lines (407 loc) • 14.5 kB
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