/* 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} 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