/** * ol-ext - A set of cool extensions for OpenLayers (ol) in node modules structure * @description ol3,openlayers,popup,menu,symbol,renderer,filter,canvas,interaction,split,statistic,charts,pie,LayerSwitcher,toolbar,animation * @version v4.0.9 * @author Jean-Marc Viglino * @see https://github.com/Viglino/ol-ext#, * @license BSD-3-Clause */ /*global ol*/ if (window.ol) { /** @namespace ol.ext */ if (!ol.ext) ol.ext = {}; /** @namespace ol.legend */ if (!ol.legend) ol.legend = {}; /** @namespace ol.particule */ if (!ol.particule) ol.particule = {}; /** @namespace ol.ext.imageLoader */ if (!ol.ext.imageLoader) ol.ext.imageLoader = {}; /** @namespace ol.ext.input */ if (!ol.ext.input) ol.ext.input = {}; /* Version */ if (!ol.util) { ol.util = { VERSION: ol.VERSION || '5.3.0' }; } else if (!ol.util.VERSION) { ol.util.VERSION = ol.VERSION || '6.1.0' } } /** Inherit the prototype methods from one constructor into another. * replace deprecated ol method * * @param {!Function} childCtor Child constructor. * @param {!Function} parentCtor Parent constructor. * @function module:ol.inherits * @api */ ol.ext.inherits = function(child,parent) { child.prototype = Object.create(parent.prototype); child.prototype.constructor = child; }; // Compatibilty with ol > 5 to be removed when v6 is out if (window.ol) { if (!ol.inherits) ol.inherits = ol.ext.inherits; } /* IE Polyfill */ // NodeList.forEach if (window.NodeList && !NodeList.prototype.forEach) { NodeList.prototype.forEach = Array.prototype.forEach; } // Element.remove if (window.Element && !Element.prototype.remove) { Element.prototype.remove = function() { if (this.parentNode) this.parentNode.removeChild(this); } } /* End Polyfill */ /** Ajax request * @fires success * @fires error * @param {*} options * @param {string} options.auth Authorisation as btoa("username:password"); * @param {string} options.dataType The type of data that you're expecting back from the server, default JSON */ ol.ext.Ajax = class olextAjax extends ol.Object { constructor(options) { options = options || {}; super(); this._auth = options.auth; this.set('dataType', options.dataType || 'JSON'); } /** Helper for get * @param {*} options * @param {string} options.url * @param {string} options.auth Authorisation as btoa("username:password"); * @param {string} options.dataType The type of data that you're expecting back from the server, default JSON * @param {string} options.success * @param {string} options.error * @param {*} options.options get options */ static get(options) { var ajax = new ol.ext.Ajax(options); if (options.success) ajax.on('success', function (e) { options.success(e.response, e); }); if (options.error) ajax.on('error', function (e) { options.error(e); }); ajax.send(options.url, options.data, options.options); } /** Helper to get cors header * @param {string} url * @param {string} callback */ static getCORS(url, callback) { var request = new XMLHttpRequest(); request.open('GET', url, true); request.send(); request.onreadystatechange = function () { if (this.readyState == this.HEADERS_RECEIVED) { callback(request.getResponseHeader('Access-Control-Allow-Origin')); } }; } /** Send an ajax request (GET) * @fires success * @fires error * @param {string} url * @param {*} data Data to send to the server as key / value * @param {*} options a set of options that are returned in the * @param {boolean} options.abort false to prevent aborting the current request, default true */ send(url, data, options) { options = options || {}; var self = this; // Url var encode = (options.encode !== false); if (encode) url = encodeURI(url); // Parameters var parameters = ''; for (var index in data) { if (data.hasOwnProperty(index) && data[index] !== undefined) { parameters += (parameters ? '&' : '?') + index + '=' + (encode ? encodeURIComponent(data[index]) : data[index]); } } // Abort previous request if (this._request && options.abort !== false) { this._request.abort(); } // New request var ajax = this._request = new XMLHttpRequest(); ajax.open('GET', url + parameters, true); if (options.timeout) ajax.timeout = options.timeout; if (this._auth) { ajax.setRequestHeader("Authorization", "Basic " + this._auth); } // Load complete this.dispatchEvent({ type: 'loadstart' }); ajax.onload = function () { self._request = null; self.dispatchEvent({ type: 'loadend' }); if (this.status >= 200 && this.status < 400) { var response; // Decode response try { switch (self.get('dataType')) { case 'JSON': { response = JSON.parse(this.response); break; } default: { response = this.response; } } } catch (e) { // Error self.dispatchEvent({ type: 'error', status: 0, statusText: 'parsererror', error: e, options: options, jqXHR: this }); return; } // Success //console.log('response',response) self.dispatchEvent({ type: 'success', response: response, status: this.status, statusText: this.statusText, options: options, jqXHR: this }); } else { self.dispatchEvent({ type: 'error', status: this.status, statusText: this.statusText, options: options, jqXHR: this }); } }; // Oops ajax.ontimeout = function () { self._request = null; self.dispatchEvent({ type: 'loadend' }); self.dispatchEvent({ type: 'error', status: this.status, statusText: 'Timeout', options: options, jqXHR: this }); }; ajax.onerror = function () { self._request = null; self.dispatchEvent({ type: 'loadend' }); self.dispatchEvent({ type: 'error', status: this.status, statusText: this.statusText, options: options, jqXHR: this }); }; // GO! ajax.send(); } } /** SVG filter * @param {*} options * @param {ol.ext.SVGOperation} option.operation * @param {string} option.id filter id, only to use if you want to adress the filter directly or var the lib create one, if none create a unique id * @param {string} option.color color interpolation filters, linear or sRGB */ ol.ext.SVGFilter = class olextSVGFilter extends ol.Object { constructor(options) { options = options || {}; super(); if (!ol.ext.SVGFilter.prototype.svg) { ol.ext.SVGFilter.prototype.svg = document.createElementNS(this.NS, 'svg'); ol.ext.SVGFilter.prototype.svg.setAttribute('version', '1.1'); ol.ext.SVGFilter.prototype.svg.setAttribute('width', 0); ol.ext.SVGFilter.prototype.svg.setAttribute('height', 0); ol.ext.SVGFilter.prototype.svg.style.position = 'absolute'; /* Firefox doesn't process hidden svg ol.ext.SVGFilter.prototype.svg.style.display = 'none'; */ document.body.appendChild(ol.ext.SVGFilter.prototype.svg); } this.element = document.createElementNS(this.NS, 'filter'); this._id = options.id || '_ol_SVGFilter_' + (ol.ext.SVGFilter.prototype._id++); this.element.setAttribute('id', this._id); if (options.color) this.element.setAttribute('color-interpolation-filters', options.color); if (options.operation) this.addOperation(options.operation); ol.ext.SVGFilter.prototype.svg.appendChild(this.element); } /** Get filter ID * @return {string} */ getId() { return this._id; } /** Remove from DOM */ remove() { this.element.remove(); } /** Add a new operation * @param {ol.ext.SVGOperation} operation */ addOperation(operation) { if (operation instanceof Array) { operation.forEach(function (o) { this.addOperation(o); }.bind(this)); } else { if (!(operation instanceof ol.ext.SVGOperation)) operation = new ol.ext.SVGOperation(operation); this.element.appendChild(operation.geElement()); } } /** Add a grayscale operation * @param {number} value */ grayscale(value) { this.addOperation({ feoperation: 'feColorMatrix', type: 'saturate', values: value || 0 }); } /** Add a luminanceToAlpha operation * @param {*} options * @param {number} options.gamma enhance gamma, default 0 */ luminanceToAlpha(options) { options = options || {}; this.addOperation({ feoperation: 'feColorMatrix', type: 'luminanceToAlpha' }); if (options.gamma) { this.addOperation({ feoperation: 'feComponentTransfer', operations: [{ feoperation: 'feFuncA', type: 'gamma', amplitude: options.gamma, exponent: 1, offset: 0 }] }); } } applyTo(img) { var canvas = document.createElement('CANVAS'); canvas.width = img.naturalWidth || img.width; canvas.height = img.naturalHeight || img.height; canvas.getContext('2d').filter = 'url(#' + this.getId() + ')'; canvas.getContext('2d').drawImage(img, 0, 0); return canvas; } } ol.ext.SVGFilter.prototype.NS = 'http://www.w3.org/2000/svg'; ol.ext.SVGFilter.prototype.svg = null; ol.ext.SVGFilter.prototype._id = 0; /** * @typedef {Object} svgOperation * @property {string} attributes.feoperation filter primitive tag name * @property {Array} attributes.operations a list of operations */ /** SVG filter * @param {string | svgOperation} attributes the fe operation or a list of operations */ ol.ext.SVGOperation = class olextSVGOperation extends ol.Object { constructor(attributes) { if (typeof (attributes) === 'string') attributes = { feoperation: attributes }; super(); if (!attributes || !attributes.feoperation) { console.error('[SVGOperation]: no operation defined.'); return; } this._name = attributes.feoperation; this.element = document.createElementNS(ol.ext.SVGOperation.NS || 'http://www.w3.org/2000/svg', this._name); this.setProperties(attributes); if (attributes.operations instanceof Array) { this.appendChild(attributes.operations); } } /** Get filter name * @return {string} */ getName() { return this._name; } /** Set Filter attribute * @param {*} attributes */ set(k, val) { if (!/^feoperation$|^operations$/.test(k)) { super.set(k, val); this.element.setAttribute(k, val); } } /** Set Filter attributes * @param {*} attributes */ setProperties(attributes) { attributes = attributes || {}; for (var k in attributes) { this.set(k, attributes[k]); } } /** Get SVG element * @return {Element} */ geElement() { return this.element; } /** Append a new operation * @param {ol.ext.SVGOperation} operation */ appendChild(operation) { if (operation instanceof Array) { operation.forEach(function (o) { this.appendChild(o); }.bind(this)); } else { if (!(operation instanceof ol.ext.SVGOperation)) { operation = new ol.ext.SVGOperation(operation); } this.element.appendChild(operation.geElement()); } } } /** Text file reader (chunk by chunk, line by line). * Large files are read in chunks and returned line by line * to handle read progress and prevent memory leaks. * @param {Object} options * @param {File} [options.file] * @param {number} [options.chunkSize=1E6] */ ol.ext.TextStreamReader = function(options) { options = options || {}; this.setChunkSize(options.chunkSize); this.setFile(options.file); this.reader_ = new FileReader(); }; /** Set file to read * @param {File} file */ ol.ext.TextStreamReader.prototype.setFile = function(file) { this.file_ = file; this.fileSize_ = (this.file_.size - 1); this.rewind(); } /** Sets the file position indicator to the beginning of the file stream. */ ol.ext.TextStreamReader.prototype.rewind = function() { this.chunk_ = 0; this.residue_ = ''; }; /** Set reader chunk size * @param {number} [chunkSize=1E6] */ ol.ext.TextStreamReader.prototype.setChunkSize = function(s) { this.chunkSize_ = s || 1E6; }; /** Get progress * @return {number} progress [0,1] */ ol.ext.TextStreamReader.prototype.getProgress = function() { return this.chunk_ / this.fileSize_; }; /** Read a text file line by line from the start * @param {function} getLine a function that gets the current line as argument. Return false to stop reading * @param {function} [progress] a function that gets the progress on each chunk (beetween 0,1) and a boolean set to true on end */ ol.ext.TextStreamReader.prototype.readLines = function(getLine, progress) { this.rewind(); this.readChunk(function(lines) { // getLine by line for (var i=0; i. * @param {function} [progress] a function that gets the progress (beetween 0,1) and a boolean set to true on end of file */ ol.ext.TextStreamReader.prototype.readChunk = function(getLines) { // Parse chunk line by line this.reader_.onload = function(e) { // Get lines var lines = e.target.result.replace(/\r/g,'').split('\n') lines[0] = this.residue_ + lines[0] || ''; // next this.chunk_ += this.chunkSize_; // more to read? if (this.chunk_ < this.fileSize_) { this.residue_ = lines.pop(); } else { this.residue_ = ''; } // Get lines getLines(lines); }.bind(this) // Read next chunk this.nexChunk_(); }; /** Read next chunk * @private */ ol.ext.TextStreamReader.prototype.nexChunk_ = function() { if (this.chunk_ < this.fileSize_) { var blob = this.file_.slice(this.chunk_, this.chunk_ + this.chunkSize_); this.reader_.readAsText(blob); return true; } return false; }; // Prevent overwrite if (ol.View.prototype.flyTo) { console.warn('[OL-EXT] ol/View~View.flyTo redefinition') } /** Destination * @typedef {Object} viewTourDestinations * @property {string} [type=flyto] animation type (flyTo, moveTo), default flyTo * @property {number} [duration=2000] animation duration * @property {ol.coordinate} [center=] destination coordinate, default current center * @property {number} [zoom] destination zoom, default current zoom * @property {number} [zoomAt=-2] zoom to fly to, default min (current zoom, zoom) -2 * @property {function} [easing] easing function used during the animation, defaults ol/easing~inAndOut * @property {number} [rotation] The rotation of the view at the end of the animation * @property {anchor} [anchor] Optional anchor to remain fixed during a rotation or resolution animation. */ /** FlyTo animation * @param {viewTourDestinations} options * @param {function} done callback function called at the end of an animation, called with true if the animation completed */ ol.View.prototype.flyTo = function(options, done) { options = options || {}; // Start new anim this.cancelAnimations(); var callback = (typeof(done) === 'function' ? done : function(){}); // Fly to destination var duration = options.duration || 2000; var zoomAt = options.zoomAt || (Math.min(options.zoom||100, this.getZoom())-2); var zoomTo = options.zoom || this.getZoom(); var coord = options.center || this.getCenter(); // Move to this.animate ({ center: coord, duration: duration, easing: options.easing, anchor: options.anchor, rotation: options.rotation }); // Zoom to this.animate ({ zoom: zoomAt, duration: duration/2, easing: options.easing, anchor: options.anchor },{ zoom: zoomTo, duration: duration/2, easing: options.easing, anchor: options.anchor }, callback); }; /** Start a tour on the map * @param {Array|Array} destinations an array of destinations or an array of [x,y,zoom,destinationType] * @param {Object} options * @param {number} [options.delay=750] delay between 2 destination * @param {string} [options.type] animation type (flyTo, moveTo) to use if not defined in destinations * @param {function} [options.easing] easing function used during the animation if not defined in destinations * @param {function} [options.done] callback function called at the end of an animation, called with true if the tour completed * @param {function} [options.step] callback function called when a destination is reached with the step index as param */ ol.View.prototype.takeTour = function(destinations, options) { options = options || {}; var index = -1; var next = function(more) { if (more) { var dest = destinations[++index]; if (typeof(options.step) === 'function') options.step(index, destinations); if (dest) { if (dest instanceof Array) dest = { center: [dest[0],dest[1]], zoom: dest[2], type: dest[3] }; var delay = index === 0 ? 0 : (options.delay || 750); if (!dest.easing) dest.easing = options.easing; if (!dest.type) dest.type = options.type; setTimeout(function () { switch(dest.type) { case 'moveTo': { this.animate(dest, next); break; } case 'flightTo': default: { this.flyTo(dest, next); break; } } }.bind(this), delay); } else { if (typeof(options.done)==='function') options.done(true); } } else { if (typeof(options.done)==='function') options.done(false); } }.bind(this) next(true); }; /** Worker helper to create a worker from code * @constructor * @param {function} mainFn main worker function * @param {object} options * @param {function} [options.onMessage] a callback function to get worker result */ ol.ext.Worker = class olextWorker { constructor(mainFn, options) { // Convert to function var mainStr = mainFn.toString().replace(/^.*\(/, 'function('); var lib = ''; for (var i in options.lib) { lib += '\nvar ' + i + ' = ' + options.lib[i].toString().replace(/^.*\(/, 'function(') + ';'; } // Code var lines = ['var mainFn = ' + mainStr + lib + ` self.addEventListener("message", function(event) { var result = mainFn(event); self.postMessage(result); });`]; // console.log(lines[0]) this.code_ = URL.createObjectURL(new Blob(lines, { type: 'text/javascript' })); this.onMessage_ = options.onMessage; this.start(); } /** Terminate current worker and start a new one */ start() { if (this.worker) this.worker.terminate(); this.worker = new Worker(this.code_); this.worker.addEventListener('message', function (e) { this.onMessage_(e.data); }.bind(this)); } /** Terminate a worker */ terminate() { this.worker.terminate(); } /** Post a new message to the worker * @param {object} message * @param {boolean} [restart=false] stop the worker and restart a new one */ postMessage(message, restart) { if (restart) this.start(); this.worker.postMessage(message); } /** Set onMessage callback * @param {function} fn a callback function to get worker result */ onMessage(fn) { this.onMessage_ = fn; } } /** Converts an RGB color value to HSL. * returns hsl as array h:[0,360], s:[0,100], l:[0,100] * @param {ol/color~Color|string} rgb * @param {number} [round=100] * @returns {Array} hsl as h:[0,360], s:[0,100], l:[0,100] */ ol.color.toHSL = function(rgb, round) { if (round===undefined) round = 100; if (!Array.isArray(rgb)) rgb = ol.color.asArray(rgb); var r = rgb[0] / 255; var g = rgb[1] / 255; var b = rgb[2] / 255; var max = Math.max(r, g, b); var min = Math.min(r, g, b); var h, s, l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { var d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } } var hsl = [ Math.round(h*60*round)/round, Math.round(s*100*round)/round, Math.round(l*100*round)/round ]; if (rgb.length>3) hsl[3] = rgb[3]; return hsl; } /** Converts an HSL color value to RGB. * @param {Array} hsl as h:[0,360], s:[0,100], l:[0,100] * @param {number} [round=1000] * @returns {Array} rgb */ ol.color.fromHSL = function(hsl, round) { if (round===undefined) round = 1000 var h = hsl[0] / 360; var s = hsl[1] / 100; var l = hsl[2] / 100; var r, g, b; if (s == 0) { r = g = b = l; // achromatic } else { var hue2rgb = function(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } var rgb = [ Math.round(r * 255*round) / round, Math.round(g * 255*round) / round, Math.round(b * 255*round) / round ]; if (hsl.length>3) rgb[3] = hsl[3]; return rgb; } /** Converts an HSL color value to RGB. * @param {ol/color~Color|string} rgb * @param {number} [round=100] * @returns {Array} hsl as h:[0,360], s:[0,100], l:[0,100] */ ol.color.toHSV = function(rgb, round) { if (round===undefined) round = 100; if (!Array.isArray(rgb)) rgb = ol.color.asArray(rgb); var r = rgb[0] / 255; var g = rgb[1] / 255; var b = rgb[2] / 255; var max = Math.max(r, g, b); var min = Math.min(r, g, b); var h, s, v = max; var d = max - min; s = max == 0 ? 0 : d / max; if (max == min) { h = 0; // achromatic } else { switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } } var hsv = [ Math.round(h*60*round)/round, Math.round(s*100*round)/round, Math.round(v*100*round)/round ]; if (rgb.length>3) hsv[3] = rgb[3]; return hsv; } /** Converts an HSV color value to RGB. * @param {Array} hsl as h:[0,360], s:[0,100], l:[0,100] * @param {number} [round=1000] * @returns {Array} rgb */ ol.color.fromHSV = function(hsv, round) { if (round===undefined) round = 1000 var h = hsv[0] / 360; var s = hsv[1] / 100; var v = hsv[2] / 100; var r, g, b; var i = Math.floor(h * 6); var f = h * 6 - i; var p = v * (1 - s); var q = v * (1 - f * s); var t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: r = v, g = t, b = p; break; case 1: r = q, g = v, b = p; break; case 2: r = p, g = v, b = t; break; case 3: r = p, g = q, b = v; break; case 4: r = t, g = p, b = v; break; case 5: r = v, g = p, b = q; break; } var rgb = [ Math.round(r * 255*round) / round, Math.round(g * 255*round) / round, Math.round(b * 255*round) / round ]; if (hsv.length>3) rgb[3] = hsv[3]; return rgb; } /** Converts an HSL color value to RGB. * @param {ol/color~Color|string} rgb * @returns {string} */ ol.color.toHexa = function(rgb) { return '#' + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1); } /** Vanilla JS helper to manipulate DOM without jQuery * @see https://github.com/nefe/You-Dont-Need-jQuery * @see https://plainjs.com/javascript/ * @see http://youmightnotneedjquery.com/ */ /** @namespace ol.ext.element */ ol.ext.element = {}; /** * Create an element * @param {string} tagName The element tag, use 'TEXT' to create a text node * @param {*} options * @param {string} options.className className The element class name * @param {Element} options.parent Parent to append the element as child * @param {Element|string} [options.html] Content of the element (if text is not set) * @param {string} [options.text] Text content (if html is not set) * @param {Element|string} [options.options] when tagName = SELECT a list of options as key:value to add to the select * @param {string} options.* Any other attribut to add to the element */ ol.ext.element.create = function (tagName, options) { options = options || {}; var elt; // Create text node if (tagName === 'TEXT') { elt = document.createTextNode(options.html||''); if (options.parent) options.parent.appendChild(elt); } else { // Other element elt = document.createElement(tagName); if (/button/i.test(tagName)) elt.setAttribute('type', 'button'); for (var attr in options) { switch (attr) { case 'className': { if (options.className && options.className.trim) elt.setAttribute('class', options.className.trim()); break; } case 'text': { elt.innerText = options.text; break; } case 'html': { if (options.html instanceof Element) elt.appendChild(options.html) else if (options.html!==undefined) elt.innerHTML = options.html; break; } case 'parent': { if (options.parent) options.parent.appendChild(elt); break; } case 'options': { if (/select/i.test(tagName)) { for (var i in options.options) { ol.ext.element.create('OPTION', { html: i, value: options.options[i], parent: elt }) } } break; } case 'style': { ol.ext.element.setStyle(elt, options.style); break; } case 'change': case 'click': { ol.ext.element.addListener(elt, attr, options[attr]); break; } case 'on': { for (var e in options.on) { ol.ext.element.addListener(elt, e, options.on[e]); } break; } case 'checked': { elt.checked = !!options.checked; break; } default: { elt.setAttribute(attr, options[attr]); break; } } } } return elt; }; /** Create a toggle switch input * @param {*} options * @param {string|Element} options.html * @param {string|Element} options.after * @param {boolean} options.checked * @param {*} [options.on] a list of actions * @param {function} [options.click] * @param {function} [options.change] * @param {Element} options.parent */ ol.ext.element.createSwitch = function (options) { var input = ol.ext.element.create('INPUT', { type: 'checkbox', on: options.on, click: options.click, change: options.change, parent: options.parent }); var opt = Object.assign ({ input: input }, options || {}); new ol.ext.input.Switch(opt); return input; }; /** Create a toggle switch input * @param {*} options * @param {string|Element} options.html * @param {string|Element} options.after * @param {string} [options.name] input name * @param {string} [options.type=checkbox] input type: radio or checkbox * @param {string} options.value input value * @param {*} [options.on] a list of actions * @param {function} [options.click] * @param {function} [options.change] * @param {Element} options.parent */ ol.ext.element.createCheck = function (options) { var input = ol.ext.element.create('INPUT', { name: options.name, type: (options.type==='radio' ? 'radio' : 'checkbox'), on: options.on, parent: options.parent }); var opt = Object.assign ({ input: input }, options || {}); if (options.type === 'radio') { new ol.ext.input.Radio(opt); } else { new ol.ext.input.Checkbox(opt); } return input; }; /** Set inner html or append a child element to an element * @param {Element} element * @param {Element|string} html Content of the element */ ol.ext.element.setHTML = function(element, html) { if (html instanceof Element) element.appendChild(html) else if (html!==undefined) element.innerHTML = html; }; /** Append text into an elemnt * @param {Element} element * @param {string} text text content */ ol.ext.element.appendText = function(element, text) { element.appendChild(document.createTextNode(text||'')); }; /** * Add a set of event listener to an element * @param {Element} element * @param {string|Array} eventType * @param {function} fn */ ol.ext.element.addListener = function (element, eventType, fn, useCapture ) { if (typeof eventType === 'string') eventType = eventType.split(' '); eventType.forEach(function(e) { element.addEventListener(e, fn, useCapture); }); }; /** * Add a set of event listener to an element * @param {Element} element * @param {string|Array} eventType * @param {function} fn */ ol.ext.element.removeListener = function (element, eventType, fn) { if (typeof eventType === 'string') eventType = eventType.split(' '); eventType.forEach(function(e) { element.removeEventListener(e, fn); }); }; /** * Show an element * @param {Element} element */ ol.ext.element.show = function (element) { element.style.display = ''; }; /** * Hide an element * @param {Element} element */ ol.ext.element.hide = function (element) { element.style.display = 'none'; }; /** * Test if an element is hihdden * @param {Element} element * @return {boolean} */ ol.ext.element.hidden = function (element) { return ol.ext.element.getStyle(element, 'display') === 'none'; }; /** * Toggle an element * @param {Element} element */ ol.ext.element.toggle = function (element) { element.style.display = (element.style.display==='none' ? '' : 'none'); }; /** Set style of an element * @param {DOMElement} el the element * @param {*} st list of style */ ol.ext.element.setStyle = function(el, st) { for (var s in st) { switch (s) { case 'top': case 'left': case 'bottom': case 'right': case 'minWidth': case 'maxWidth': case 'width': case 'height': { if (typeof(st[s]) === 'number') { el.style[s] = st[s]+'px'; } else { el.style[s] = st[s]; } break; } default: { el.style[s] = st[s]; } } } }; /** * Get style propertie of an element * @param {DOMElement} el the element * @param {string} styleProp Propertie name * @return {*} style value */ ol.ext.element.getStyle = function(el, styleProp) { var value, defaultView = (el.ownerDocument || document).defaultView; // W3C standard way: if (defaultView && defaultView.getComputedStyle) { // sanitize property name to css notation // (hypen separated words eg. font-Size) styleProp = styleProp.replace(/([A-Z])/g, "-$1").toLowerCase(); value = defaultView.getComputedStyle(el, null).getPropertyValue(styleProp); } else if (el.currentStyle) { // IE // sanitize property name to camelCase styleProp = styleProp.replace(/-(\w)/g, function(str, letter) { return letter.toUpperCase(); }); value = el.currentStyle[styleProp]; // convert other units to pixels on IE if (/^\d+(em|pt|%|ex)?$/i.test(value)) { return (function(value) { var oldLeft = el.style.left, oldRsLeft = el.runtimeStyle.left; el.runtimeStyle.left = el.currentStyle.left; el.style.left = value || 0; value = el.style.pixelLeft + "px"; el.style.left = oldLeft; el.runtimeStyle.left = oldRsLeft; return value; })(value); } } if (/px$/.test(value)) return parseInt(value); return value; }; /** Get outerHeight of an elemen * @param {DOMElement} elt * @return {number} */ ol.ext.element.outerHeight = function(elt) { return elt.offsetHeight + ol.ext.element.getStyle(elt, 'marginBottom') }; /** Get outerWidth of an elemen * @param {DOMElement} elt * @return {number} */ ol.ext.element.outerWidth = function(elt) { return elt.offsetWidth + ol.ext.element.getStyle(elt, 'marginLeft') }; /** Get element offset rect * @param {DOMElement} elt * @return {*} */ ol.ext.element.offsetRect = function(elt) { var rect = elt.getBoundingClientRect(); return { top: rect.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0), left: rect.left + (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0), height: rect.height || (rect.bottom - rect.top), width: rect.width || (rect.right - rect.left) } }; /** Get element offset * @param {ELement} elt * @returns {Object} top/left offset */ ol.ext.element.getFixedOffset = function(elt) { var offset = { left:0, top:0 }; var getOffset = function(parent) { if (!parent) return offset; // Check position when transform if (ol.ext.element.getStyle(parent, 'position') === 'absolute' && ol.ext.element.getStyle(parent, 'transform') !== "none") { var r = parent.getBoundingClientRect(); offset.left += r.left; offset.top += r.top; return offset; } return getOffset(parent.offsetParent) } return getOffset(elt.offsetParent) }; /** Get element offset rect * @param {DOMElement} elt * @param {boolean} fixed get fixed position * @return {Object} */ ol.ext.element.positionRect = function(elt, fixed) { var gleft = 0; var gtop = 0; var getRect = function( parent ) { if (parent) { gleft += parent.offsetLeft; gtop += parent.offsetTop; return getRect(parent.offsetParent); } else { var r = { top: elt.offsetTop + gtop, left: elt.offsetLeft + gleft }; if (fixed) { r.top -= (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0); r.left -= (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0); } r.bottom = r.top + elt.offsetHeight; r.right = r.top + elt.offsetWidth; return r; } }; return getRect(elt.offsetParent); } /** Make a div scrollable without scrollbar. * On touch devices the default behavior is preserved * @param {DOMElement} elt * @param {*} options * @param {function} [options.onmove] a function that takes a boolean indicating that the div is scrolling * @param {boolean} [options.vertical=false] * @param {boolean} [options.animate=true] add kinetic to scroll * @param {boolean} [options.mousewheel=false] enable mousewheel to scroll * @param {boolean} [options.minibar=false] add a mini scrollbar to the parent element (only vertical scrolling) * @returns {Object} an object with a refresh function */ ol.ext.element.scrollDiv = function(elt, options) { options = options || {}; var pos = false; var speed = 0; var d, dt = 0; var onmove = (typeof(options.onmove) === 'function' ? options.onmove : function(){}); //var page = options.vertical ? 'pageY' : 'pageX'; var page = options.vertical ? 'screenY' : 'screenX'; var scroll = options.vertical ? 'scrollTop' : 'scrollLeft'; var moving = false; // Factor scale content / container var scale, isbar; // Update the minibar var updateCounter = 0; var updateMinibar = function() { if (scrollbar) { updateCounter++; setTimeout(updateMinibarDelay); } } var updateMinibarDelay = function() { if (scrollbar) { updateCounter--; // Prevent multi call if (updateCounter) return; // Container height var pheight = elt.clientHeight; // Content height var height = elt.scrollHeight; // Set scrollbar value scale = pheight / height; scrollbar.style.height = scale * 100 + '%'; scrollbar.style.top = (elt.scrollTop / height * 100) + '%'; scrollContainer.style.height = pheight + 'px'; // No scroll if (pheight > height - .5) scrollContainer.classList.add('ol-100pc'); else scrollContainer.classList.remove('ol-100pc'); } } // Handle pointer down var onPointerDown = function(e) { // Prevent scroll if (e.target.classList.contains('ol-noscroll')) return; // Start scrolling moving = false; pos = e[page]; dt = new Date(); elt.classList.add('ol-move'); // Prevent elt dragging e.preventDefault(); // Listen scroll window.addEventListener('pointermove', onPointerMove); ol.ext.element.addListener(window, ['pointerup','pointercancel'], onPointerUp); } // Register scroll var onPointerMove = function(e) { if (pos !== false) { var delta = (isbar ? -1/scale : 1) * (pos - e[page]); moving = moving || Math.round(delta) elt[scroll] += delta; d = new Date(); if (d-dt) { speed = (speed + delta / (d - dt))/2; } pos = e[page]; dt = d; // Tell we are moving if (delta) onmove(true); } else { moving = true; } }; // Animate scroll var animate = function(to) { var step = (to>0) ? Math.min(100, to/2) : Math.max(-100, to/2); to -= step; elt[scroll] += step; if (-1 < to && to < 1) { if (moving) setTimeout(function() { elt.classList.remove('ol-move'); }); else elt.classList.remove('ol-move'); moving = false; onmove(false); } else { setTimeout(function() { animate(to); }, 40); } } // Initialize scroll container for minibar var scrollContainer, scrollbar; if (options.vertical && options.minibar) { var init = function(b) { // only once elt.removeEventListener('pointermove', init); elt.parentNode.classList.add('ol-miniscroll'); scrollbar = ol.ext.element.create('DIV'); scrollContainer = ol.ext.element.create('DIV', { className: 'ol-scroll', html: scrollbar }); elt.parentNode.insertBefore(scrollContainer, elt); // Move scrollbar scrollbar.addEventListener('pointerdown', function(e) { isbar = true; onPointerDown(e) }); // Handle mousewheel if (options.mousewheel) { ol.ext.element.addListener(scrollContainer, ['mousewheel', 'DOMMouseScroll', 'onmousewheel'], function(e) { onMouseWheel(e) } ); ol.ext.element.addListener(scrollbar, ['mousewheel', 'DOMMouseScroll', 'onmousewheel'], function(e) { onMouseWheel(e) } ); } // Update on enter elt.parentNode.addEventListener('pointerenter', updateMinibar); // Update on resize window.addEventListener('resize', updateMinibar); // Update if (b!==false) updateMinibar(); }; // Allready inserted in the DOM if (elt.parentNode) init(false); // or wait when ready else elt.addEventListener('pointermove', init); // Update on scroll elt.addEventListener('scroll', function() { updateMinibar(); }); } // Enable scroll elt.style['touch-action'] = 'none'; elt.style['overflow'] = 'hidden'; elt.classList.add('ol-scrolldiv'); // Start scrolling ol.ext.element.addListener(elt, ['pointerdown'], function(e) { isbar = false; onPointerDown(e) }); // Prevet click when moving... elt.addEventListener('click', function(e) { if (elt.classList.contains('ol-move')) { e.preventDefault(); e.stopPropagation(); } }, true); // Stop scrolling var onPointerUp = function(e) { dt = new Date() - dt; if (dt>100 || isbar) { // User stop: no speed speed = 0; } else if (dt>0) { // Calculate new speed speed = ((speed||0) + (pos - e[page]) / dt) / 2; } animate(options.animate===false ? 0 : speed*200); pos = false; speed = 0; dt = 0; // Add class to handle click (on iframe / double-click) if (!elt.classList.contains('ol-move')) { elt.classList.add('ol-hasClick') setTimeout(function() { elt.classList.remove('ol-hasClick'); }, 500); } else { elt.classList.remove('ol-hasClick'); } isbar = false; window.removeEventListener('pointermove', onPointerMove) ol.ext.element.removeListener(window, ['pointerup','pointercancel'], onPointerUp); }; // Handle mousewheel var onMouseWheel = function(e) { var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail))); elt.classList.add('ol-move'); elt[scroll] -= delta*30; elt.classList.remove('ol-move'); return false; } if (options.mousewheel) { // && !elt.classList.contains('ol-touch')) { ol.ext.element.addListener(elt, ['mousewheel', 'DOMMouseScroll', 'onmousewheel'], onMouseWheel ); } return { refresh: updateMinibar } }; /** Dispatch an event to an Element * @param {string} eventName * @param {Element} element */ ol.ext.element.dispatchEvent = function (eventName, element) { var event; try { event = new CustomEvent(eventName); } catch(e) { // Try customevent on IE event = document.createEvent("CustomEvent"); event.initCustomEvent(eventName, true, true, {}); } element.dispatchEvent(event); }; /** Get a canvas overlay for a map (non rotated, on top of the map) * @param {ol.Map} map * @return {canvas} */ ol.ext.getMapCanvas = function(map) { if (!map) return null; var canvas = map.getViewport().getElementsByClassName('ol-fixedoverlay')[0]; if (!canvas) { if (map.getViewport().querySelector('.ol-layers')) { // Add a fixed canvas layer on top of the map canvas = document.createElement('canvas'); canvas.className = 'ol-fixedoverlay'; map.getViewport().querySelector('.ol-layers').after(canvas); // Clear before new compose map.on('precompose', function (e){ canvas.width = map.getSize()[0] * e.frameState.pixelRatio; canvas.height = map.getSize()[1] * e.frameState.pixelRatio; }); } else { canvas = map.getViewport().querySelector('canvas'); } } return canvas; }; ol.ext.olVersion = ol.util.VERSION.split('.'); ol.ext.olVersion = parseInt(ol.ext.olVersion[0])*100 + parseInt(ol.ext.olVersion[1]); /** Get style to use in a VectorContext * @param {} e * @param {ol.style.Style} s * @return {ol.style.Style} */ ol.ext.getVectorContextStyle = function(e, s) { var ratio = e.frameState.pixelRatio; // Bug with Icon images if (ol.ext.olVersion > 605 && ratio !== 1 && (s.getImage() instanceof ol.style.Icon)) { s = s.clone(); var img = s.getImage(); img.setScale(img.getScale()*ratio); /* BUG anchor don't use ratio */ var anchor = img.getAnchor(); if (img.setDisplacement) { var disp = img.getDisplacement(); if (disp) { disp[0] -= anchor[0]/ratio; disp[1] += anchor[1]/ratio; img.setAnchor([0,0]); } } else { if (anchor) { anchor[0] /= ratio; anchor[1] /= ratio; } } /**/ } return s; } /** Helper for loading BIL-32 (Band Interleaved by Line) image * @param {string} src * @param {function} onload a function that takes a Float32Array and a ol.size.Size (array size) * @param {function} onerror * @private */ ol.ext.imageLoader.loadBILImage = function(src, onload, onerror) { var size = [ parseInt(src.replace(/.*WIDTH=(\d*).*/i,'$1')), parseInt(src.replace(/.*HEIGHT=(\d*).*/i,'$1')) ]; var xhr = new XMLHttpRequest(); xhr.responseType = 'blob'; xhr.addEventListener('loadend', function () { var resp = this.response; if (resp !== undefined) { var reader = new FileReader(); // Get as array reader.addEventListener('loadend', (e) => { var data = new Float32Array(e.target.result); onload(data, size); }); // Start reading the blob reader.readAsArrayBuffer(resp); // tile.getImage().src = URL.createObjectURL(blob); } else { onerror(); } }); xhr.addEventListener('error', function () { onerror(); }); xhr.open('GET', src); xhr.send(); }; /** Helper for loading image * @param {string} src * @param {function} onload a function that takes a an image and a ol.size.Size * @param {function} onerror * @private */ ol.ext.imageLoader.loadImage = function(src, onload, onerror) { var xhr = new XMLHttpRequest(); xhr.responseType = 'blob'; xhr.addEventListener('loadend', function () { var resp = this.response; if (resp !== undefined) { var img = new Image(); img.onload = function() { onload(img, [img.naturalWidth, img.naturalHeight]); } img.src = URL.createObjectURL(resp); } else { onerror(); } }); xhr.addEventListener('error', function () { onerror(); }); xhr.open('GET', src); xhr.send(); }; /** Get a TileLoadFunction to transform tiles images * @param {function} setPixel a function that takes a Uint8ClampedArray and the pixel position to transform * @returns {function} an ol/Tile~LoadFunction */ ol.ext.imageLoader.pixelTransform = function(setPixel) { return function(tile, src) { ol.ext.imageLoader.loadImage( src, function(img, size) { var canvas = document.createElement('canvas'); canvas.width = size[0]; canvas.height = size[1]; var ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); var imgData = ctx.getImageData(0, 0, size[0], size[1]); var pixels = imgData.data; for (var i = 0; i < pixels.length; i += 4) { setPixel(pixels, i, size); } ctx.putImageData(imgData, 0, 0); tile.setImage(canvas); }, function() { tile.setState(3); } ); } }; /** Get a TileLoadFunction to transform tiles into grayscale images * @returns {function} an ol/Tile~LoadFunction */ ol.ext.imageLoader.grayscale = function() { return ol.ext.imageLoader.pixelTransform(function(pixels, i) { pixels[i] = pixels[i + 1] = pixels[i + 2] = parseInt(3*pixels[i] + 4*pixels[i + 1] + pixels[i + 2] >>> 3); }) }; /** Get a TileLoadFunction to turn color or a color range transparent * @param {ol.color.Color|Array} colors color or color range to turn transparent * @returns {function} an ol/Tile~LoadFunction */ ol.ext.imageLoader.transparent = function(colors) { var color1, color2; if (colors instanceof Array) { color1 = colors[0]; color2 = colors[1]; } var color = color1 = ol.color.asArray(color1); if (!color2) { return ol.ext.imageLoader.pixelTransform(function(pixels, i) { if (pixels[i]===color[0] && pixels[i+1]===color[1] && pixels[i+2]===color[2]) { pixels[i+3] = 0; } }) } else { color2 = ol.color.asArray(color2); color = [Math.min(color1[0], color2[0]), Math.min(color1[1], color2[1]), Math.min(color1[2], color2[2])]; color2 = [Math.max(color1[0], color2[0]), Math.max(color1[1], color2[1]), Math.max(color1[2], color2[2])]; return ol.ext.imageLoader.pixelTransform(function(pixels, i) { if (pixels[i]>=color1[0] && pixels[i]<=color2[0] && pixels[i+1]>=color[1] && pixels[i+1]<=color2[1] && pixels[i+2]>=color[2] && pixels[i+2]<=color2[2]) { pixels[i+3] = 0; } }) } }; /** Returns an Imageloader function to load an x-bil-32 image as sea level map * to use as a ol/Tile~LoadFunction or ol/Image~LoadFunction * @param { number } level * @param {*} options * @param { ol.Color } [options.color] fill color * @param { boolean } [options.opacity=true] smooth color on border * @param { number } [options.minValue=-Infinity] minimum level value * @returns {function} an ol/Tile~LoadFunction */ ol.ext.imageLoader.seaLevelMap = function(level, options) { options = options || {}; var h0 = Math.max(level + .01, .01); var c = options.color ? ol.color.asArray(options.color) : [135,203,249]; var min = typeof(options.minValue) === 'number' ? options.minValue : -Infinity; var opacity = options.opacity!==false; return ol.ext.imageLoader.elevationMap(function(h) { if (h < h0 && h > min) { return [c[0], c[1], c[2], opacity ? 255 * (h0-h) / h0 : 255]; } else { return [0,0,0,0]; } }) }; /** Shaded relief ? not/bad working yet... * @returns {function} an ol/Tile~LoadFunction * @private */ ol.ext.imageLoader.shadedRelief = function() { var sunElev = Math.PI / 4; var sunAzimuth = 2*Math.PI - Math.PI / 4; return function (tile, src) { ol.ext.imageLoader.loadBILImage( src, function(data, size) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var width = canvas.width = size[0]; var height = canvas.height = size[1]; var imgData = ctx.getImageData(0, 0, width, height); var pixels = imgData.data; function getIndexForCoordinates(x, y) { return x + y*width; } for (var x=0; x 0 ) { if ( sly > 0 ) azimuth = phi + 1.5*Math.PI; else if ( sly < 0 ) azimuth = 1.5*Math.PI - phi; else phi = 1.5*Math.PI; } else if ( slx < 0 ){ if ( sly < 0 ) azimuth = phi + .5*Math.PI; else if ( sly > 0 ) azimuth = .5*Math.PI - phi; else azimuth = .5*Math.PI; } else { if ( sly < 0 ) azimuth = Math.PI; else if ( sly > 0 ) azimuth = 0; } // get luminance var lum = Math.cos( azimuth - sunAzimuth )*Math.cos( Math.PI*.5 - Math.atan(sl0) )*Math.cos( sunElev ) + Math.sin( Math.PI*.5 - Math.atan(sl0) )*Math.sin( sunElev ); if (lum<0) lum = 0; lum = Math.sqrt(lum*.8 + .5); var p = getIndexForCoordinates(x,y) * 4; pixels[p] = pixels[p+1] = pixels[p+2] = 0; pixels[p+3] = 255 - lum*255; } ctx.putImageData(imgData, 0, 0); tile.setImage(canvas); }, function () { tile.setState(3); } ) }; }; /** Get a TileLoadFunction to load an x-bil-32 image as elevation map (ie. pixels colors codes elevations as terrain-RGB) * If getPixelColor is not define pixel store elevation as rgb, use {@link ol.ext.getElevationFromPixel} to get elevation from pixel * @param {function} [getPixelColor] a function that taket an elevation and return a color array [r,g,b,a], default store elevation as terrain-RGB * @returns {function} an ol/Tile~LoadFunction */ ol.ext.imageLoader.elevationMap = function(getPixelColor) { if (typeof(getPixelColor) !== 'function') getPixelColor = ol.ext.getPixelFromElevation; return function (tile, src) { ol.ext.imageLoader.loadBILImage( src, function(data, size) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = size[0]; canvas.height = size[1]; var imgData = ctx.getImageData(0, 0, size[0], size[1]); var pixels = imgData.data; for (var i=0; i -12000 m * - 2 digits (0.01 m) * @param {number} height elevation * @returns {Array} pixel value */ ol.ext.getPixelFromElevation = function(height) { var h = Math.round(height*100 + 1200000); var pixel = [ h >> 16, (h % 65536) >> 8, h % 256, 255 ]; return pixel; }; /** Convert pixel (terrain-RGB) to elevation * @see ol.ext.getPixelFromElevation * @param {Array} pixel the pixel value * @returns {number} elevation */ ol.ext.getElevationFromPixel = function(pixel) { // return -10000 + (pixel[0] * 65536 + pixel[1] * 256 + pixel[2]) * 0.01; return -12000 + ((pixel[0] << 16) + (pixel[1] << 8) + pixel[2]) * 0.01; }; /* See https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web https://evanw.github.io/lightgl.js/docs/matrix.html https://github.com/jlmakes/rematrix https://jsfiddle.net/2znLxda2/ */ /** Matrix3D; a set of functions to handle matrix3D */ ol.matrix3D = {}; /** Get transform matrix3D of an element * @param {Element} ele * @return {Array>} */ ol.matrix3D.getTransform = function(ele) { var style = window.getComputedStyle(ele, null); var tr = style.getPropertyValue("-webkit-transform") || style.getPropertyValue("-moz-transform") || style.getPropertyValue("-ms-transform") || style.getPropertyValue("-o-transform") || style.getPropertyValue("transform"); var values = tr.split('(')[1].split(')')[0].split(','); var mx = [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]; var i, j; if (values.length === 16) { for (i = 0; i < 4; ++i) { for (j = 0; j < 4; ++j) { mx[j][i] = +values[i * 4 + j]; } } } else { for (i = 0; i < 3; ++i) { for (j = 0; j < 2; ++j) { mx[j][i] = +values[i * 2 + j]; } } } return mx; }; /** Get transform matrix3D of an element * @param {Element} ele * @return {Array} */ ol.matrix3D.getTransformOrigin = function (ele) { var style = window.getComputedStyle(ele, null); var tr = style.getPropertyValue("-webkit-transform-origin") || style.getPropertyValue("-moz-transform-origin") || style.getPropertyValue("-ms-transform-origin") || style.getPropertyValue("-o-transform-origin") || style.getPropertyValue("transform-origin"); var values = tr.split(' '); var mx = [ 0, 0, 0, 1 ]; for (var i = 0; i < values.length; ++i) { mx[i] = parseInt(values[i]); } return mx; }; /** Compute translate matrix * @param {number} x * @param {number} y * @param {number} z * @return {Array>} */ ol.matrix3D.translateMatrix = function(x, y, z) { return [ [1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1] ]; }; /** Identity matrix * @return {Array>} */ ol.matrix3D.identity = function() { return [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]; }; /** Round matrix * @param {Array>} mx * @param {number} round Rounding value, default 1E-10 */ ol.matrix3D.roundTo = function(mx, round) { if (!round) round = 1E-10; var m = [[],[],[],[]]; for (var i=0; i<4; i++) { for (var j=0; j<4; j++) { m[i][j] = Math.round(mx[i][j] / round) * round; } } return m; }; /** Multiply matrix3D * @param {Array>} mx1 * @param {Array>} mx2 * @return {Array>} */ ol.matrix3D.multiply = function (mx1, mx2) { var mx = [ [], [], [], [] ]; for (var i = 0; i < 4; ++i) { for (var j = 0; j < 4; ++j) { var sum = 0; for (var k = 0; k < 4; ++k) { sum += (mx1[k][i] * mx2[j][k]); } mx[j][i] = sum; } } return mx; }; /** Compute the full transform that is applied to the transformed parent: -origin o tx o origin * @param {Array>} tx transform matrix * @param {Array>} origin transform origin * @return {Array>} */ ol.matrix3D.computeTransformMatrix = function(tx, origin) { var preTx = ol.matrix3D.translateMatrix(-origin[0], -origin[1], -origin[2]); var postTx = ol.matrix3D.translateMatrix(origin[0], origin[1], origin[2]); var temp1 = ol.matrix3D.multiply(preTx, tx); return ol.matrix3D.multiply(temp1, postTx); }; /** Apply transform to a coordinate * @param {Array>} tx * @param {ol.pixel} px */ ol.matrix3D.transformVertex = function(tx, px) { var vert = [px[0], px[1], 0, 1] var mx = [ ]; for (var i = 0; i < 4; ++i) { mx[i] = 0; for (var j = 0; j < 4; ++j) { mx[i] += +tx[i][j] * vert[j]; } } return mx; } /** Perform the homogeneous divide to apply perspective to the points (divide x,y,z by the w component). * @param {Array} vert * @return {Array} */ ol.matrix3D.projectVertex = function(vert) { var out = [ ]; for (var i = 0; i < 4; ++i) { out[i] = vert[i] / vert[3]; } return out; }; /** Inverse a matrix3D * @return {Array>} m matrix to transform * @return {Array>} */ ol.matrix3D.inverse = function(m) { var s0 = m[0][0] * m[1][1] - m[1][0] * m[0][1] var s1 = m[0][0] * m[1][2] - m[1][0] * m[0][2] var s2 = m[0][0] * m[1][3] - m[1][0] * m[0][3] var s3 = m[0][1] * m[1][2] - m[1][1] * m[0][2] var s4 = m[0][1] * m[1][3] - m[1][1] * m[0][3] var s5 = m[0][2] * m[1][3] - m[1][2] * m[0][3] var c5 = m[2][2] * m[3][3] - m[3][2] * m[2][3] var c4 = m[2][1] * m[3][3] - m[3][1] * m[2][3] var c3 = m[2][1] * m[3][2] - m[3][1] * m[2][2] var c2 = m[2][0] * m[3][3] - m[3][0] * m[2][3] var c1 = m[2][0] * m[3][2] - m[3][0] * m[2][2] var c0 = m[2][0] * m[3][1] - m[3][0] * m[2][1] var determinant = 1 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0) if (isNaN(determinant) || determinant === Infinity) { throw new Error('Inverse determinant attempted to divide by zero.') } return [ [ (m[1][1] * c5 - m[1][2] * c4 + m[1][3] * c3) * determinant, (-m[0][1] * c5 + m[0][2] * c4 - m[0][3] * c3) * determinant, (m[3][1] * s5 - m[3][2] * s4 + m[3][3] * s3) * determinant, (-m[2][1] * s5 + m[2][2] * s4 - m[2][3] * s3) * determinant ],[ (-m[1][0] * c5 + m[1][2] * c2 - m[1][3] * c1) * determinant, (m[0][0] * c5 - m[0][2] * c2 + m[0][3] * c1) * determinant, (-m[3][0] * s5 + m[3][2] * s2 - m[3][3] * s1) * determinant, (m[2][0] * s5 - m[2][2] * s2 + m[2][3] * s1) * determinant ],[ (m[1][0] * c4 - m[1][1] * c2 + m[1][3] * c0) * determinant, (-m[0][0] * c4 + m[0][1] * c2 - m[0][3] * c0) * determinant, (m[3][0] * s4 - m[3][1] * s2 + m[3][3] * s0) * determinant, (-m[2][0] * s4 + m[2][1] * s2 - m[2][3] * s0) * determinant ],[ (-m[1][0] * c3 + m[1][1] * c1 - m[1][2] * c0) * determinant, (m[0][0] * c3 - m[0][1] * c1 + m[0][2] * c0) * determinant, (-m[3][0] * s3 + m[3][1] * s1 - m[3][2] * s0) * determinant, (m[2][0] * s3 - m[2][1] * s1 + m[2][2] * s0) * determinant ] ] }; /* global ol */ /* Create ol.sphere for backward compatibility with ol < 5.0 * To use with Openlayers package */ if (window.ol && !ol.sphere) { ol.sphere = {}; ol.sphere.getDistance = function (c1, c2, radius) { var sphere = new ol.Sphere(radius || 6371008.8); return sphere.haversineDistance(c1, c2); } ol.sphere.getArea = ol.Sphere.getArea; ol.sphere.getLength = ol.Sphere.getLength; } /* 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). */ /** A simple filter to detect edges on images * @constructor * @requires ol.filter * @extends {ol.ext.SVGFilter} * @param {*} options * @param {number} options.neighbours nb of neighbour (4 or 8), default 8 * @param {boolean} options.grayscale get grayscale image, default false, * @param {boolean} options.alpha get alpha channel, default false */ ol.ext.SVGFilter.Laplacian = class olextSVGFilterLaplacian extends ol.ext.SVGFilter { constructor(options) { options = options || {}; super({ id: options.id }); var operation = { feoperation: 'feConvolveMatrix', in: 'SourceGraphic', preserveAlpha: true, result: 'C1' }; if (options.neighbours===4) { operation.kernelMatrix = [ 0, -1, 0, -1, 4, -1, 0, -1, 0 ]; } else { operation.kernelMatrix = [ -1, -1, -1, -1, 8, -1, -1, -1, -1 ]; } this.addOperation(operation); if (options.grayscale) this.grayscale(); else if (options.alpha) this.luminanceToAlpha({ gamma: options.gamma }); } }; /* 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). */ /** Apply a sobel filter on an image * @constructor * @requires ol.filter * @extends {ol.ext.SVGFilter} * @param {object} options * @param {string} [options.id] * @param {number} [options.scale=1] * @param {number} [options.ligth=50] light option. 0: darker, 100: lighter */ ol.ext.SVGFilter.Paper = class olextSVGFilterPaper extends ol.ext.SVGFilter { constructor(options) { options = options || {}; super({ id: options.id }); this.addOperation({ feoperation: 'feTurbulence', numOctaves: 4, seed: 0, type: 'fractalNoise', baseFrequency: 0.2 / (options.scale || 1) }); this.addOperation({ feoperation: 'feDiffuseLighting', 'lighting-color': 'rgb(255,255,255)', surfaceScale: 1.5, kernelUnitLength: 0.01, diffuseConstant: 1.1000000000000001, result: 'paper', operations: [{ feoperation: 'feDistantLight', elevation: options.light || 50, azimuth: 75 }] }); this.addOperation({ feoperation: 'feBlend', in: 'SourceGraphic', in2: 'paper', mode: 'multiply' }); } /** Set filter light * @param {number} light light option. 0: darker, 100: lighter */ setLight(light) { this.element.querySelector('feDistantLight').setAttribute('elevation', light); } } /* 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). */ /** Apply a Prewitt filter on an image * @constructor * @requires ol.filter * @extends {ol.ext.SVGFilter} * @param {*} options * @param {boolean} options.grayscale get grayscale image, default false, * @param {boolean} options.alpha get alpha channel, default false */ ol.ext.SVGFilter.Prewitt = class olextSVGFilterPrewitt extends ol.ext.SVGFilter { constructor(options) { options = options || {}; super({ id: options.id, color: 'sRGB' }); var operation = { feoperation: 'feConvolveMatrix', in: 'SourceGraphic', preserveAlpha: true, order: 3 }; // Vertical operation.kernelMatrix = [ -1, -1, -1, 0, 0, 0, 1, 1, 1 ]; operation.result = 'V1'; this.addOperation(operation); operation.kernelMatrix = [ 1, 1, 1, 0, 0, 0, -1, -1, -1 ]; operation.result = 'V2'; this.addOperation(operation); // Horizontal operation.kernelMatrix = [ -1, 0, 1, -1, 0, 1, -1, 0, 1 ]; operation.result = 'H1'; this.addOperation(operation); operation.kernelMatrix = [ 1, -0, -1, 1, 0, -1, 1, 0, -1 ]; operation.result = 'H2'; this.addOperation(operation); // Compose V this.addOperation({ feoperation: 'feComposite', operator: 'arithmetic', in: 'V1', in2: 'V2', k2: 1, k3: 1, result: 'V' }); // Compose H this.addOperation({ feoperation: 'feComposite', operator: 'arithmetic', in: 'H1', in2: 'H2', k2: 1, k3: 1, result: 'H' }); // Merge this.addOperation({ feoperation: 'feBlend', mode: 'lighten', in: 'H', in2: 'V' }); if (options.grayscale) this.grayscale(); else if (options.alpha) this.luminanceToAlpha(); } }; /* 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). */ /** Apply a Roberts filter on an image * @constructor * @requires ol.filter * @extends {ol.ext.SVGFilter} * @param {*} options * @param {boolean} options.grayscale get grayscale image, default false, * @param {boolean} options.alpha get alpha channel, default false */ ol.ext.SVGFilter.Roberts = class olextSVGFilterRoberts extends ol.ext.SVGFilter { constructor(options) { options = options || {}; super({ id: options.id, color: 'sRGB' }); var operation = { feoperation: 'feConvolveMatrix', in: 'SourceGraphic', preserveAlpha: true, order: 3 }; // Vertical operation.kernelMatrix = [ -1, 0, 0, 0, 0, 0, 0, 0, 1 ]; operation.result = 'V1'; this.addOperation(operation); operation.kernelMatrix = [ 1, 0, 0, 0, 0, 0, 0, 0, -1 ]; operation.result = 'V2'; this.addOperation(operation); // Horizontal operation.kernelMatrix = [ 0, 0, 1, 0, 0, 0, -1, 0, 0 ]; operation.result = 'H1'; this.addOperation(operation); operation.kernelMatrix = [ 0, -0, -1, 0, 0, 0, 1, 0, 0 ]; operation.result = 'H2'; this.addOperation(operation); // Compose V this.addOperation({ feoperation: 'feComposite', operator: 'arithmetic', in: 'V1', in2: 'V2', k2: 1, k3: 1, result: 'V' }); // Compose H this.addOperation({ feoperation: 'feComposite', operator: 'arithmetic', in: 'H1', in2: 'H2', k2: 1, k3: 1, result: 'H' }); // Merge this.addOperation({ feoperation: 'feBlend', mode: 'lighten', in: 'H', in2: 'V' }); if (options.grayscale) this.grayscale(); else if (options.alpha) this.luminanceToAlpha(); } }; /* 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). */ /** Apply a sobel filter on an image * @constructor * @requires ol.filter * @extends {ol.ext.SVGFilter} * @param {*} options * @param {boolean} options.grayscale get grayscale image, default false, * @param {boolean} options.alpha get alpha channel, default false */ ol.ext.SVGFilter.Sobel = class olextSVGFilterSobel extends ol.ext.SVGFilter { constructor(options) { options = options || {}; super({ id: options.id, color: 'sRGB' }); var operation = { feoperation: 'feConvolveMatrix', in: 'SourceGraphic', preserveAlpha: true, order: 3 }; // Vertical operation.kernelMatrix = [ -1, -2, -1, 0, 0, 0, 1, 2, 1 ]; operation.result = 'V1'; this.addOperation(operation); operation.kernelMatrix = [ 1, 2, 1, 0, 0, 0, -1, -2, -1 ]; operation.result = 'V2'; this.addOperation(operation); // Horizontal operation.kernelMatrix = [ -1, 0, 1, -2, 0, 2, -1, 0, 1 ]; operation.result = 'H1'; this.addOperation(operation); operation.kernelMatrix = [ 1, -0, -1, 2, 0, -2, 1, 0, -1 ]; operation.result = 'H2'; this.addOperation(operation); // Compose V this.addOperation({ feoperation: 'feComposite', operator: 'arithmetic', in: 'V1', in2: 'V2', k2: 1, k3: 1, result: 'V' }); // Compose H this.addOperation({ feoperation: 'feComposite', operator: 'arithmetic', in: 'H1', in2: 'H2', k2: 1, k3: 1, result: 'H' }); // Merge this.addOperation({ feoperation: 'feBlend', mode: 'lighten', in: 'H', in2: 'V' }); if (options.grayscale) this.grayscale(); else if (options.alpha) this.luminanceToAlpha({ gamma: options.gamma }); } }; /** Vanilla JS geographic inputs * color, size, width, font, symboles, dash, arrow, pattern */ /** Abstract base class; normally only used for creating subclasses and not instantiated in apps. * @constructor * @extends {ol.Object} * @param {*} options * @param {Element} [options.input] input element, if none create one * @param {string} [options.type] input type, if no input * @param {number} [options.min] input min, if no input * @param {number} [options.max] input max, if no input * @param {number} [options.step] input step, if no input * @param {string|number} [options.val] input value * @param {boolean} [options.checked] check input * @param {boolean} [options.hidden] the input is display:none * @param {boolean} [options.disabled] disable input * @param {Element} [options.parent] parent element, if no input */ ol.ext.input.Base = class olextinputBase extends ol.Object { constructor(options) { options = options || {}; super(); var input = this.input = options.input; if (!input) { input = this.input = document.createElement('INPUT'); if (options.type) input.setAttribute('type', options.type); if (options.min !== undefined) input.setAttribute('min', options.min); if (options.max !== undefined) input.setAttribute('max', options.max); if (options.step !== undefined) input.setAttribute('step', options.step); if (options.parent) options.parent.appendChild(input); } if (options.disabled) input.disabled = true; if (options.checked !== undefined) input.checked = !!options.checked; if (options.val !== undefined) input.value = options.val; if (options.hidden) input.style.display = 'none'; input.addEventListener('focus', function () { if (this.element) this.element.classList.add('ol-focus'); }.bind(this)); var tout; input.addEventListener('focusout', function () { if (this.element) { if (tout) clearTimeout(tout); tout = setTimeout(function () { this.element.classList.remove('ol-focus'); }.bind(this), 0); } }.bind(this)); } /** Listen to drag event * @param {Element} elt * @param {function} cback when draggin on the element * @private */ _listenDrag(elt, cback) { var handle = function (e) { this.moving = true; this.element.classList.add('ol-moving'); var listen = function (e) { if (e.type === 'pointerup') { document.removeEventListener('pointermove', listen); document.removeEventListener('pointerup', listen); document.removeEventListener('pointercancel', listen); setTimeout(function () { this.moving = false; this.element.classList.remove('ol-moving'); }.bind(this)); } if (e.target === elt) cback(e); e.stopPropagation(); e.preventDefault(); }.bind(this); document.addEventListener('pointermove', listen, false); document.addEventListener('pointerup', listen, false); document.addEventListener('pointercancel', listen, false); e.stopPropagation(); e.preventDefault(); }.bind(this); elt.addEventListener('mousedown', handle, false); elt.addEventListener('touchstart', handle, false); } /** Set the current value */ setValue(v) { if (v !== undefined) this.input.value = v; this.input.dispatchEvent(new Event('change')); } /** Get the current getValue * @returns {string} */ getValue() { return this.input.value; } /** Get the input element * @returns {Element} */ getInputElement() { return this.input; } } /** Checkbox input * @constructor * @extends {ol.ext.input.Base} * @param {*} options * @param {string} [options.className] * @param {Element} [options.input] input element, if non create one (use parent to tell where) * @param {Element} [options.parent] element to use as parent if no input option * @param {booelan} [options.hover=true] show popup on hover * @param {string} [options.align=left] align popup left/right * @param {string} [options.type] a slide type as 'size' * @param {number} [options.min] min value, default use input min * @param {number} [options.max] max value, default use input max * @param {number} [options.step] step value, default use input step * @param {boolean} [options.overflow=false] enable values over min/max * @param {string|Element} [options.before] an element to add before the slider * @param {string|Element} [options.after] an element to add after the slider * @param {boolean} [options.fixed=false] no pupop */ ol.ext.input.Slider = class olextinputSlider extends ol.ext.input.Base { constructor(options) { options = options || {}; super(options); this.set('overflow', !!options.overflow); this.element = ol.ext.element.create('DIV', { className: 'ol-input-slider' + (options.hover !== false ? ' ol-hover' : '') + (options.type ? ' ol-' + options.type : '') + (options.className ? ' ' + options.className : '') }); if (options.fixed) this.element.classList.add('ol-fixed'); var input = this.input; if (input.parentNode) input.parentNode.insertBefore(this.element, input); this.element.appendChild(input); if (options.align === 'right') this.element.classList.add('ol-right'); var popup = ol.ext.element.create('DIV', { className: 'ol-popup', parent: this.element }); // Before element if (options.before) { ol.ext.element.create('DIV', { className: 'ol-before', html: options.before, parent: popup }); } // Slider var slider = this.slider = ol.ext.element.create('DIV', { className: 'ol-slider', parent: popup }); ol.ext.element.create('DIV', { className: 'ol-back', parent: this.slider }); // Cursor var cursor = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: slider }); // After element if (options.after) { ol.ext.element.create('DIV', { className: 'ol-after', html: options.after, parent: popup }); } var min = (options.min !== undefined) ? options.min : parseFloat(input.min) || 0; var max = (options.max !== undefined) ? options.max : parseFloat(input.max) || 1; var step = (options.step !== undefined) ? options.step : parseFloat(input.step) || 1; var dstep = 1 / step; // Handle popup drag this._listenDrag(slider, function (e) { var tx = Math.max(0, Math.min(e.offsetX / slider.clientWidth, 1)); cursor.style.left = Math.max(0, Math.min(100, Math.round(tx * 100))) + '%'; var v = input.value = Math.round((tx * (max - min) + min) * dstep) / dstep; this.dispatchEvent({ type: 'change:value', value: v }); }.bind(this)); // Set value var setValue = function () { var v = parseFloat(input.value) || 0; if (!this.get('overflow')) v = Math.max(min, Math.min(max, v)); if (v != input.value) input.value = v; var tx = (v - min) / (max - min); cursor.style.left = Math.max(0, Math.min(100, Math.round(tx * 100))) + '%'; this.dispatchEvent({ type: 'change:value', value: v }); }.bind(this); input.addEventListener('change', setValue); setValue(); } } /** Base class for input popup * @constructor * @extends {ol.ext.input.Base} * @fires change:color * @fires color * @param {*} options * @param {string} [options.className] * @param {ol.colorLike} [options.color] default color * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input * @param {string} [options.position='popup'] fixed | static | popup | inline (no popup) * @param {boolean} [options.autoClose=true] close when click on color * @param {boolean} [options.hidden=false] display the input */ ol.ext.input.PopupBase = class olextinputPopupBase extends ol.ext.input.Base { constructor(options) { options = options || {}; options.hidden = options.hidden !== false; super(options); this.set('autoClose', options.autoClose !== false); this.element = ol.ext.element.create('DIV', { className: ('ol-ext-popup-input ' + (options.className || '')).trim(), }); switch (options.position) { case 'inline': break; case 'static': case 'fixed': { this.element.classList.add('ol-popup'); this.element.classList.add('ol-popup-fixed'); this._fixed = (options.position === 'fixed'); break; } default: { this.element.classList.add('ol-popup'); break; } } var input = this.input; if (input.parentNode) input.parentNode.insertBefore(this.element, input); // Show on element click this.element.addEventListener('click', function () { if (this.isCollapsed()) setTimeout(function () { this.collapse(false); }.bind(this)); }.bind(this)); this._elt = {}; // Popup container this._elt.popup = ol.ext.element.create('DIV', { className: 'ol-popup', parent: this.element }); this._elt.popup.addEventListener('click', function (e) { e.stopPropagation(); }); // Hide on click outside var down = false; this._elt.popup.addEventListener('pointerdown', function () { down = true; }); this._elt.popup.addEventListener('click', function () { down = false; }); document.addEventListener('click', function () { if (!this.moving && !down) this.collapse(true); down = false; }.bind(this)); // Hide on window resize window.addEventListener('resize', function () { this.collapse(true); }.bind(this)); } /** show/hide color picker * @param {boolean} [b=false] */ collapse(b) { if (b != this.isCollapsed()) { this.dispatchEvent({ type: 'change:visible', visible: !this.isCollapsed() }); } this.dispatchEvent({ type: 'collapse', visible: !b }); if (b) { this._elt.popup.classList.remove('ol-visible'); } else { this._elt.popup.classList.add('ol-visible'); if (this._fixed) { // Get fixed position var pos = this.element.getBoundingClientRect(); var offset = ol.ext.element.getFixedOffset(this.element); pos = { bottom: pos.bottom - offset.top, left: pos.left - offset.left }; // Test window overflow + recenter var dh = pos.bottom + this._elt.popup.offsetHeight + offset.top; if (dh > document.documentElement.clientHeight) { this._elt.popup.style.top = Math.max(document.documentElement.clientHeight - this._elt.popup.offsetHeight - offset.top, 0) + 'px'; } else { this._elt.popup.style.top = pos.bottom + 'px'; } var dw = pos.left + this._elt.popup.offsetWidth + offset.left; if (dw > document.documentElement.clientWidth) { this._elt.popup.style.left = Math.max(document.documentElement.clientWidth - this._elt.popup.offsetWidth - offset.left, 0) + 'px'; } else { this._elt.popup.style.left = pos.left + 'px'; } } } } /** Is the popup collapsed ? * @returns {boolean} */ isCollapsed() { return !this._elt.popup.classList.contains('ol-visible'); } /** Toggle the popup */ toggle() { this.collapse(!this.isCollapsed()); } } /** Checkbox input * @constructor * @extends {ol.ext.input.Base} * @fires check * @param {*} options * @param {string} [options.className] * @param {Element|string} [options.html] label content * @param {string} [options.after] label garnish (placed after) * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input * @param {boolean} [options.autoClose=true] * @param {boolean} [options.visible=false] display the input */ ol.ext.input.Checkbox = class olextinputCheckbox extends ol.ext.input.Base { constructor(options) { options = options || {}; super(options); var label = this.element = document.createElement('LABEL'); if (options.html instanceof Element) label.appendChild(options.html); else if (options.html !== undefined) label.innerHTML = options.html; label.className = ('ol-ext-check ol-ext-checkbox ' + (options.className || '')).trim(); if (this.input.parentNode) this.input.parentNode.insertBefore(label, this.input); label.appendChild(this.input); label.appendChild(document.createElement('SPAN')); if (options.after) { label.appendChild(document.createTextNode(options.after)); } // Handle change this.input.addEventListener('change', function () { this.dispatchEvent({ type: 'check', checked: this.input.checked, value: this.input.value }); }.bind(this)); } isChecked() { return this.input.checked; } } /** A list element synchronize with a Collection. * Element in the list can be reordered interactively and the associated Collection is kept up to date. * @constructor * @fires item:select * @fires item:dblclick * @fires item:order * @extends {ol.Object} * @param {*} options * @param {Element} [options.target] * @param {Collection} [options.collection] the collection to display in the list * @param {function} [options.getTitle] a function that takes a collection item and returns an Element or a string */ ol.ext.input.Collection = class olextinputCollection extends ol.Object { constructor(options) { super(); this.element = ol.ext.element.create('UL', { className: ('ol-collection-list ' + (options.className || '')).trim(), parent: options.target }); this._title = (typeof (options.getTitle) === 'function' ? options.getTitle : function (elt) { return elt.title; }); this.setCollection(options.collection); } /** Remove current collection (listeners) * /!\ remove collection when input list is removed from the DOM */ removeCollection() { if (this.collection) { this.collection.un('change:length', this._update); this.collection = null; } } /** Set the collection * @param {ol.Collection} collection */ setCollection(collection) { this.removeCollection(); this.collection = collection; this.refresh(); if (this.collection) { this._update = function () { if (!this._reorder) { this.refresh(); var pos = this.getSelectPosition(); if (pos < 0) { this.dispatchEvent({ type: 'item:select', position: -1, item: null }); } else { this.dispatchEvent({ type: 'item:order', position: pos, item: this._currentItem }); } } }.bind(this); this.collection.on('change:length', this._update); } } /** Select an item * @param {*} item */ select(item) { if (item === this._currentItem) return; var pos = -1; this._listElt.forEach(function (l, i) { if (l.item !== item) { l.li.classList.remove('ol-select'); } else { l.li.classList.add('ol-select'); pos = i; } }); this._currentItem = (pos >= 0 ? item : null); this.dispatchEvent({ type: 'item:select', position: pos, item: this._currentItem }); } /** Select an item at * @param {number} n */ selectAt(n) { this.select(this.collection.item(n)); } /** Get current selection * @returns {*} */ getSelect() { return this._currentItem; } /** Get current selection * @returns {number} */ getSelectPosition() { if (!this.collection) return -1; return this.collection.getArray().indexOf(this._currentItem); } /** Redraw the list */ refresh() { this.element.innerHTML = ''; this._listElt = []; if (!this.collection) return; this.collection.forEach((item, pos) => { var li = ol.ext.element.create('LI', { html: this._title(item), className: this._currentItem === item ? 'ol-select' : '', 'data-position': pos, on: { click: function () { this.select(item); }.bind(this), dblclick: function () { this.dispatchEvent({ type: 'item:dblclick', position: pos, item: item }); }.bind(this), }, parent: this.element }); this._listElt.push({ li: li, item: item }); var order = ol.ext.element.create('DIV', { className: 'ol-noscroll ol-order', parent: li }); var current = pos; var move = function (e) { // Get target var target = e.pointerType === 'touch' ? document.elementFromPoint(e.clientX, e.clientY) : e.target; while (target && target.parentNode !== this.element) { target = target.parentNode; } if (target && target !== li) { var over = parseInt(target.getAttribute('data-position')); if (target.getAttribute('data-position') < current) { target.insertAdjacentElement('beforebegin', li); current = over; } else { target.insertAdjacentElement('afterend', li); current = over + 1; } } }.bind(this); var stop = function () { document.removeEventListener('pointermove', move); document.removeEventListener('pointerup', stop); document.removeEventListener('pointercancel', stop); if (current !== pos) { this._reorder = true; this.collection.removeAt(pos); this.collection.insertAt(current > pos ? current - 1 : current, item); this._reorder = false; this.dispatchEvent({ type: 'item:order', position: current > pos ? current - 1 : current, oldPosition: pos, item: item }); this.refresh(); } }.bind(this); order.addEventListener('pointerdown', function () { this.select(item); document.addEventListener('pointermove', move); document.addEventListener('pointerup', stop); document.addEventListener('pointercancel', stop); }.bind(this)); }); } } /** Color picker * @constructor * @extends {ol.ext.input.PopupBase} * @fires color * @param {*} options * @param {string} [options.className] * @param {ol.colorLike} [options.color] default color * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input * @param {boolean} [options.hastab=false] use tabs for palette / picker * @param {string} [options.paletteLabel="palette"] label for the palette tab * @param {string} [options.pickerLabel="picker"] label for the picker tab * @param {string} [options.position='popup'] fixed | static | popup | inline (no popup) * @param {boolean} [options.opacity=true] enable opacity * @param {boolean} [options.autoClose=true] close when click on color * @param {boolean} [options.hidden=true] display the input */ ol.ext.input.Color = class olextinputColor extends ol.ext.input.PopupBase { constructor(options) { options = options || {}; options.hidden = options.hidden !== false; options.className = ('ol-ext-colorpicker ' + (options.hastab ? 'ol-tab ' : '') + (options.className || '')).trim(); super(options); if (options.opacity === false) { this.element.classList.add('ol-nopacity'); } this._cursor = {}; var hsv = this._hsv = {}; // Vignet this._elt.vignet = ol.ext.element.create('DIV', { className: 'ol-vignet', parent: this.element }); // Bar var bar = ol.ext.element.create('DIV', { className: 'ol-tabbar', parent: this._elt.popup }); ol.ext.element.create('DIV', { className: 'ol-tab', html: options.paletteLabel || 'palette', click: function () { this.element.classList.remove('ol-picker-tab'); }.bind(this), parent: bar }); ol.ext.element.create('DIV', { className: 'ol-tab', html: options.pickerLabel || 'picker', click: function () { this.element.classList.add('ol-picker-tab'); }.bind(this), parent: bar }); // Popup container var container = ol.ext.element.create('DIV', { className: 'ol-container', parent: this._elt.popup }); // Color picker var picker = this._elt.picker = ol.ext.element.create('DIV', { className: 'ol-picker', parent: container }); var pickerCursor = this._cursor.picker = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: picker }); this._listenDrag(picker, function (e) { var tx = Math.max(0, Math.min(e.offsetX / picker.clientWidth, 1)); var ty = Math.max(0, Math.min(e.offsetY / picker.clientHeight, 1)); pickerCursor.style.left = Math.round(tx * 100) + '%'; pickerCursor.style.top = Math.round(ty * 100) + '%'; hsv.s = tx * 100; hsv.v = 100 - ty * 100; this.setColor(); }.bind(this)); // Opacity cursor var slider = ol.ext.element.create('DIV', { className: 'ol-slider', parent: container }); this._elt.slider = ol.ext.element.create('DIV', { parent: slider }); var sliderCursor = this._cursor.slide = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: slider }); this._listenDrag(slider, function (e) { var t = Math.max(0, Math.min(e.offsetX / slider.clientWidth, 1)); hsv.a = t * 100; sliderCursor.style.left = Math.round(t * 100) + '%'; this.setColor(); }.bind(this)); // Tint cursor var tint = ol.ext.element.create('DIV', { className: 'ol-tint', parent: container }); var tintCursor = this._cursor.tint = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: tint }); this._listenDrag(tint, function (e) { var t = Math.max(0, Math.min(e.offsetY / tint.clientHeight, 1)); hsv.h = t * 360; tintCursor.style.top = Math.round(t * 100) + '%'; this.setColor(); }.bind(this)); // Clear button ol.ext.element.create('DIV', { className: 'ol-clear', click: function () { this.setColor([0, 0, 0, 0]); }.bind(this), parent: container }); // RVB input var rgb = ol.ext.element.create('DIV', { className: 'ol-rgb', parent: container }); var changergb = function () { var r = Math.max(0, Math.min(255, parseInt(this._elt.r.value))); var g = Math.max(0, Math.min(255, parseInt(this._elt.g.value))); var b = Math.max(0, Math.min(255, parseInt(this._elt.b.value))); var a = Math.max(0, Math.min(1, parseFloat(this._elt.a.value))); this.setColor([r, g, b, a]); }.bind(this); this._elt.r = ol.ext.element.create('INPUT', { type: 'number', lang: 'en-GB', change: changergb, min: 0, max: 255, parent: rgb }); this._elt.g = ol.ext.element.create('INPUT', { type: 'number', lang: 'en-GB', change: changergb, min: 0, max: 255, parent: rgb }); this._elt.b = ol.ext.element.create('INPUT', { type: 'number', lang: 'en-GB', change: changergb, min: 0, max: 255, parent: rgb }); this._elt.a = ol.ext.element.create('INPUT', { type: 'number', lang: 'en-GB', change: changergb, min: 0, max: 1, step: .1, parent: rgb }); // Text color input this._elt.txtColor = ol.ext.element.create('INPUT', { type: 'text', className: 'ol-txt-color', change: function () { var color; this._elt.txtColor.classList.remove('ol-error'); try { color = ol.color.asArray(this._elt.txtColor.value); } catch (e) { this._elt.txtColor.classList.add('ol-error'); } if (color) this.setColor(color); }.bind(this), parent: container }); ol.ext.element.create('BUTTON', { html: 'OK', click: function () { this._addCustomColor(this.getColor()); this.collapse(true); }.bind(this), parent: container }); var i; // Color palette this._paletteColor = {}; this._elt.palette = ol.ext.element.create('DIV', { className: 'ol-palette', parent: this._elt.popup }); for (i = 0; i < 8; i++) { var c = Math.round(255 - 255 * i / 7); this.addPaletteColor([c, c, c], c); //ol.color.toHexa([c,c,c])); } var colors = ['#f00', '#f90', '#ff0', '#0f0', '#0ff', '#48e', '#00f', '#f0f']; colors.forEach(function (c) { this.addPaletteColor(c, ol.color.toHexa(ol.color.asArray(c))); }.bind(this)); for (i = 0; i < 5; i++) { colors.forEach(function (c) { c = ol.color.toHSV(ol.color.asArray(c)); c = [c[0], i / 4 * 80 + 20, 100 - i / 4 * 60]; c = ol.color.fromHSV(c, 1); this.addPaletteColor(c, ol.color.toHexa(c)); }.bind(this)); } // Custom colors ol.ext.element.create('HR', { parent: this._elt.palette }); // Create custom color list if (!ol.ext.input.Color.customColorList) { ol.ext.input.Color.customColorList = new ol.Collection(); var ccolor = JSON.parse(localStorage.getItem('ol-ext@colorpicker') || '[]'); ccolor.forEach(function (c) { ol.ext.input.Color.customColorList.push(c); }); ol.ext.input.Color.customColorList.on(['add', 'remove'], function () { localStorage.setItem('ol-ext@colorpicker', JSON.stringify(ol.ext.input.Color.customColorList.getArray())); }); } // Handle custom color ol.ext.input.Color.customColorList.on('add', function (e) { this.addPaletteColor(this.getColorFromID(e.element)); }.bind(this)); ol.ext.input.Color.customColorList.on('remove', function (e) { if (this._paletteColor[e.element]) this._paletteColor[e.element].element.remove(); delete this._paletteColor[e.element]; }.bind(this)); // Add new one ol.ext.input.Color.customColorList.forEach(function (c) { this._addCustomColor(this.getColorFromID(c)); }.bind(this)); // Current color this.setColor(options.color || [0, 0, 0, 0]); this._currentColor = this.getColorID(this.getColor()); // Add new palette color this.on('color', function () { this._addCustomColor(this.getColor()); this._currentColor = this.getColorID(this.getColor()); this.setColor(); }.bind(this)); // Update color on hide this.on('collapse', function (e) { if (!e.visible) { var c = this.getColor(); if (this._currentColor !== this.getColorID(c)) { this.dispatchEvent({ type: 'color', color: c }); } } else { this._currentColor = this.getColorID(this.getColor()); } }.bind(this)); } /** Add color to palette * @param {ol.colorLike} color * @param {string} title * @param {boolean} select */ addPaletteColor(color, title, select) { // Get color id try { color = ol.color.asArray(color); } catch (e) { return; } var id = this.getColorID(color); // Add new one if (!this._paletteColor[id] && color[3]) { this._paletteColor[id] = { color: color, element: ol.ext.element.create('DIV', { title: title || '', className: (color[3] < 1 ? 'ol-alpha' : ''), style: { color: 'rgb(' + (color.join(',')) + ')' }, click: function () { this.setColor(color); if (this.get('autoClose')) this.collapse(true); }.bind(this), parent: this._elt.palette }) }; } if (select) { this._selectPalette(color); } } /** Show palette or picker tab * @param {string} what palette or picker */ showTab(what) { if (what === 'palette') this.element.classList.remove('ol-picker-tab'); else this.element.classList.add('ol-picker-tab'); } /** Show palette or picker tab * @returns {string} palette or picker */ getTab() { return this.element.classList.contains('ol-picker-tab') ? 'picker' : 'palette'; } /** Select a color in the palette * @private */ _selectPalette(color) { var id = this.getColorID(color); Object.keys(this._paletteColor).forEach(function (c) { this._paletteColor[c].element.classList.remove('ol-select'); }.bind(this)); if (this._paletteColor[id]) { this._paletteColor[id].element.classList.add('ol-select'); } } /** Set Color * @param { Array } */ setColor(color) { var hsv = this._hsv; if (color) { color = ol.color.asArray(color); var hsv2 = ol.color.toHSV(color); hsv.h = hsv2[0]; hsv.s = hsv2[1]; hsv.v = hsv2[2]; if (hsv2.length > 3) hsv.a = hsv2[3] * 100; else hsv.a = 100; this._cursor.picker.style.left = hsv.s + '%'; this._cursor.picker.style.top = (100 - hsv.v) + '%'; this._cursor.tint.style.top = (hsv.h / 360 * 100) + '%'; this._cursor.slide.style.left = hsv.a + '%'; if (this.isCollapsed()) { this.dispatchEvent({ type: 'color', color: color }); } } else { /* hsv.h = Math.round(hsv.h) % 360; hsv.s = Math.round(hsv.s); hsv.v = Math.round(hsv.v); */ hsv.a = Math.round(hsv.a); color = this.getColor(); } var val = 'rgba(' + color.join(', ') + ')'; // Show color this._elt.picker.style.color = 'hsl(' + hsv.h + ', 100%, 50%)'; this._elt.slider.style.backgroundImage = 'linear-gradient(45deg, transparent, rgba(' + this.getColor(false).join(',') + '))'; this._elt.vignet.style.color = val; // RGB this._elt.r.value = color[0]; this._elt.g.value = color[1]; this._elt.b.value = color[2]; this._elt.a.value = color[3]; // Txt color this._elt.txtColor.classList.remove('ol-error'); if (color[3] === 1) { this._elt.txtColor.value = ol.color.toHexa(color); } else { this._elt.txtColor.value = val; } this._selectPalette(color); // Set input value if (this.input.value !== val) { this.input.value = val; this.input.dispatchEvent(new Event('change')); } } /** Get current color * @param {boolean} [opacity=true] * @return {Array} */ getColor(opacity) { return ol.color.fromHSV([this._hsv.h, this._hsv.s, this._hsv.v, (opacity !== false) ? this._hsv.a / 100 : 1], 1); } /** * @private */ _addCustomColor(color) { var id = this.getColorID(color); if (this._paletteColor[id]) return; if (!color[3]) return; if (ol.ext.input.Color.customColorList.getArray().indexOf(id) < 0) { ol.ext.input.Color.customColorList.push(id); if (ol.ext.input.Color.customColorList.getLength() > 24) { ol.ext.input.Color.customColorList.removeAt(0); } } this.addPaletteColor(color); } clearCustomColor() { ol.ext.input.Color.customColorList.clear(); } /** Convert color to id * @param {ol.colorLike} Color * @returns {number} */ getColorID(color) { color = ol.color.asArray(color); if (color[3] === undefined) color[3] = 1; return color.join('-'); } /** Convert color to id * @param {number} id * @returns {Array} Color */ getColorFromID(id) { var c = id.split('-'); return ([parseFloat(c[0]), parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3])]); } } /** Custom color list * @private */ ol.ext.input.Color.customColorList = null; /** Checkbox input * @constructor * @extends {ol.ext.input.Base} * @param {*} options * @param {string} [options.className] * @param {Array} options.options an array of options to place in the popup { html:, title:, value: } * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input * @param {boolean} [options.hover=false] show popup on hover, default false or true if disabled or hidden * @param {boolean} [options.hidden] the input is display:none * @param {boolean} [options.disabled] disable input * @param {boolean} [options.fixed=false] don't use a popup, default use a popup * @param {string} [options.align=left] align popup left/right/middle */ ol.ext.input.List = class olextinputList extends ol.ext.input.Base { constructor(options) { options = options || {}; super(options); this._content = ol.ext.element.create('DIV'); if (options.hidden || options.disabled) options.hover = true; this.element = ol.ext.element.create('DIV', { html: this._content, className: 'ol-input-popup' + (options.hover ? ' ol-hover' : '') }); this.set('hideOnClick', options.hideOnClick !== false); if (options.className) this.element.classList.add(options.className); if (options.fixed) { this.element.classList.add('ol-fixed'); this.set('hideOnClick', false); } switch (options.align) { case 'middle': this.set('hideOnClick', false); // fall through case 'rigth': this.element.classList.add('ol-' + options.align); break; default: break; } var input = this.input; if (input.parentNode) input.parentNode.insertBefore(this.element, input); this.element.appendChild(input); var popup = this.popup = ol.ext.element.create('UL', { className: 'ol-popup', parent: this.element }); var opts = []; options.options.forEach(option => { opts.push({ value: option.value, element: ol.ext.element.create('LI', { html: option.html, title: option.title || option.value, className: 'ol-option', on: { pointerdown: function () { this.setValue(option.value); if (this.get('hideOnClick')) { popup.style.display = 'none'; setTimeout(function () { popup.style.display = ''; }, 200); } }.bind(this) }, parent: this.popup }) }); }); this.input.addEventListener('change', function () { var v = this.input.value; var val; opts.forEach(function (o) { if (o.value == v) { o.element.classList.add('ol-selected'); val = o.element; } else { o.element.classList.remove('ol-selected'); } }); this.dispatchEvent({ type: 'change:value', value: this.getValue() }); this._content.innerHTML = val ? val.innerHTML : ''; }.bind(this)); // Initial value var event = new Event('change'); setTimeout(function () { this.input.dispatchEvent(event); }.bind(this)); } } /** Switch input * @constructor * @extends {ol.ext.input.Checkbox} * @fires check * @param {*} options * @param {string} [options.className] * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input */ ol.ext.input.Radio = class olextinputRadio extends ol.ext.input.Checkbox { constructor(options) { options = options || {}; super(options); this.element.className = ('ol-ext-check ol-ext-radio ' + (options.className || '')).trim(); } } /** Checkbox input * @constructor * @extends {ol.ext.input.Base} * @fires change:value * @param {*} options * @param {string} [options.className] * @param {Element} [options.input] input element, if non create one (use parent to tell where) * @param {Element} [options.input2] input element * @param {Element} [options.parent] element to use as parent if no input option * @param {number} [options.min] min value, default use input min * @param {number} [options.max] max value, default use input max * @param {number} [options.step] step value, default use input step * @param {boolean} [options.overflow=false] enable values over min/max */ ol.ext.input.Range = class olextinputRange extends ol.ext.input.Base { constructor(options) { options = options || {}; super(options); this.set('overflow', !!options.overflow); this.element = ol.ext.element.create('DIV', { className: 'ol-input-slider ol-input-range' + (options.className ? ' ' + options.className : '') }); var input = this.input; if (input.parentNode) input.parentNode.insertBefore(this.element, input); this.element.appendChild(input); // Slider var slider = this.slider = ol.ext.element.create('DIV', { className: 'ol-slider', parent: this.element }); var back = ol.ext.element.create('DIV', { className: 'ol-back', parent: this.slider }); var input2 = this.input2 = options.input2; if (input2) this.element.appendChild(input2); // Cursors var cursor = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: slider }); var cursor2 = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: input2 ? slider : undefined }); var currentCursor = cursor; function setCursor(e) { currentCursor = e.target; } cursor.addEventListener('mousedown', setCursor, false); cursor.addEventListener('touchstart', setCursor, false); cursor2.addEventListener('mousedown', setCursor, false); cursor2.addEventListener('touchstart', setCursor, false); var min = (options.min !== undefined) ? options.min : parseFloat(input.min) || 0; var max = (options.max !== undefined) ? options.max : parseFloat(input.max) || 1; var step = (options.step !== undefined) ? options.step : parseFloat(input.step) || 1; var dstep = 1 / step; function setRange() { // range if (input2) { var l1 = parseFloat(cursor.style.left) || 0; var l2 = parseFloat(cursor2.style.left) || 0; back.style.left = Math.min(l1, l2) + '%'; back.style.right = (100 - Math.max(l1, l2)) + '%'; } else { back.style.left = 0; back.style.right = (100 - parseFloat(cursor.style.left) || 0) + '%'; } } function checkMinMax() { if (input2 && parseFloat(input.value) > parseFloat(input2.value)) { var v = input.value; input.value = input2.value; input2.value = v; setValue({ target: input }); if (input2) setValue({ target: input2 }); } } // Handle popup drag this._listenDrag(slider, function (e) { var current = (currentCursor === cursor ? input : input2); var tx = Math.max(0, Math.min(e.offsetX / slider.clientWidth, 1)); currentCursor.style.left = Math.max(0, Math.min(100, Math.round(tx * 100))) + '%'; var v = current.value = Math.round((tx * (max - min) + min) * dstep) / dstep; setRange(); this.dispatchEvent({ type: 'change:value', value: v }); if (e.type === 'pointerup') { checkMinMax(); } }.bind(this)); // Set value var setValue = function (e) { var current = e.target; var curs = (current === input ? cursor : cursor2); var v = parseFloat(current.value) || 0; if (!this.get('overflow')) v = Math.max(min, Math.min(max, v)); if (v != current.value) current.value = v; var tx = (v - min) / (max - min); curs.style.left = Math.max(0, Math.min(100, Math.round(tx * 100))) + '%'; setRange(); this.dispatchEvent({ type: 'change:value', value: v }); checkMinMax(); }.bind(this); input.addEventListener('change', setValue); if (input2) input2.addEventListener('change', setValue); setValue({ target: input }); if (input2) setValue({ target: input2 }); } /** Set the current value (second input) */ setValue2(v) { if (!this.input2) return; if (v !== undefined) this.input2.value = v; this.input2.dispatchEvent(new Event('change')); } /** Get the current value (second input) */ getValue2() { return this.input2 ? this.input2.value : null; } /** Get the current min value * @return {number} */ getMin() { return Math.min(parseFloat(this.getValue()), parseFloat(this.getValue2())); } /** Get the current max value * @return {number} */ getMax() { return Math.max(parseFloat(this.getValue()), parseFloat(this.getValue2())); } } /** Checkbox input * @constructor * @extends {ol.ext.input.List} * @param {*} options * @param {string} [options.className] * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input * @param {Array} [options.size] a list of size (default 0,2,3,5,8,13,21,34,55) */ ol.ext.input.Size = class olextinputSize extends ol.ext.input.List { constructor(options) { options = options || {}; options.options = []; (options.size || [0, 2, 3, 5, 8, 13, 21, 34, 55]).forEach(function (i) { options.options.push({ value: i, html: ol.ext.element.create('DIV', { className: 'ol-option-' + i, style: { fontSize: i ? i + 'px' : undefined } }) }); }); super(options); this._content.remove(); this.element.classList.add('ol-size'); } /** Get the current value * @returns {number} */ getValue() { return parseFloat(super.getValue()); } } /** Switch input * @constructor * @extends {ol.ext.input.Checkbox} * @fires check * @param {*} options * @param {string} [options.className] * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input */ ol.ext.input.Switch = class olextinputSwitch extends ol.ext.input.Checkbox { constructor(options) { options = options || {}; super(options); this.element.className = ('ol-ext-toggle-switch ' + (options.className || '')).trim(); } }; /** Checkbox input * @constructor * @extends {ol.ext.input.List} * @param {*} options * @param {string} [options.className] * @param {Element} [options.input] input element, if non create one * @param {Element} [options.parent] parent element, if create an input * @param {Array} [options.size] a list of size (default 0,1,2,3,5,10,15,20) */ ol.ext.input.Width = class olextinputWidth extends ol.ext.input.List { constructor(options) { options = options || {}; options.options = []; (options.size || [0, 1, 2, 3, 5, 10, 15, 20]).forEach(function (i) { options.options.push({ value: i, html: ol.ext.element.create('DIV', { className: 'ol-option-' + i, style: { height: i || undefined } }) }); }); super(options); this._content.remove(); this.element.classList.add('ol-width'); } /** Get the current value * @returns {number} */ getValue() { return parseFloat(super.getValue()); } } /** Legend class to draw features in a legend element * @constructor * @fires select * @fires refresh * @param {*} options * @param {String} options.title Legend title * @param {number} [options.maxWidth] maximum legend width * @param {ol.size} [options.size] Size of the symboles in the legend, default [40, 25] * @param {number} [options.margin=10] Size of the symbole's margin, default 10 * @param { ol.layer.Base } [layer] layer associated with the legend * @param { ol.style.Text} [options.textStyle='16px sans-serif'] a text style for the legend, default 16px sans-serif * @param { ol.style.Text} [options.titleStyle='bold 16px sans-serif'] a text style for the legend title, default textStyle + bold * @param { ol.style.Style | Array | ol.StyleFunction | undefined } options.style a style or a style function to use with features */ ol.legend.Legend = class ollegendLegend extends ol.Object { constructor(options) { super() options = options || {} // Handle item collection this._items = new ol.Collection() var listeners = [] var tout this._items.on('add', function (e) { listeners.push({ item: e.element, on: e.element.on('change', function () { this.refresh() }.bind(this)) }) if (tout) { clearTimeout(tout) tout = null } tout = setTimeout(function () { this.refresh() }.bind(this), 0) }.bind(this)) this._items.on('remove', function (e) { for (var i = 0; i < listeners; i++) { if (e.element === listeners[i].item) { ol.Observable.unByKey(listeners[i].on) listeners.splice(i, 1) break } } if (tout) { clearTimeout(tout) tout = null } tout = setTimeout(function () { this.refresh() }.bind(this), 0) }.bind(this)) // List item element this._listElement = ol.ext.element.create('UL', { className: 'ol-legend' }) // Legend canvas this._canvas = document.createElement('canvas') // Set layer this.setLayer(options.layer) // Properties this.set('maxWidth', options.maxWidth, true) this.set('size', options.size || [40, 25], true) this.set('margin', options.margin === 0 ? 0 : options.margin || 10, true) this._textStyle = options.textStyle || new ol.style.Text({ font: '16px sans-serif', fill: new ol.style.Fill({ color: '#333' }), backgroundFill: new ol.style.Fill({ color: 'rgba(255,255,255,.8)' }) }) this._title = new ol.legend.Item({ title: options.title || '', className: 'ol-title' }) if (options.titleStyle) { this._titleStyle = options.titleStyle } else { this._titleStyle = this._textStyle.clone() this._titleStyle.setFont('bold ' + this._titleStyle.getFont()) } this.setStyle(options.style) if (options.items instanceof Array) { options.items.forEach(function (item) { this.addItem(item) }.bind(this)) } this.refresh() } /** Get a symbol image for a given legend item * @param {olLegendItemOptions} item * @param {Canvas|undefined} canvas a canvas to draw in, if none creat one * @param {int|undefined} offsetY Y offset to draw in canvas, default 0 */ static getLegendImage(item, canvas, offsetY) { item = item || {} if (typeof (item.margin) === 'undefined'){ item.margin = 10 } var size = item.size || [40, 25] if (item.width) size[0] = item.width if (item.heigth) size[1] = item.heigth item.onload = item.onload || function () { setTimeout(function () { ol.legend.Legend.getLegendImage(item, canvas, offsetY) }, 100) } var width = size[0] + 2 * item.margin var height = item.lineHeight || (size[1] + 2 * item.margin) var ratio = item.pixelratio || ol.has.DEVICE_PIXEL_RATIO if (!canvas) { offsetY = 0 canvas = document.createElement('canvas') canvas.width = width * ratio canvas.height = height * ratio } var ctx = canvas.getContext('2d') ctx.save() var vectorContext = ol.render.toContext(ctx, { pixelRatio: ratio }) var typeGeom = item.typeGeom var style var feature = item.feature if (!feature && typeGeom) { if (/Point/.test(typeGeom)){ feature = new ol.Feature(new ol.geom.Point([0, 0])) } else if (/LineString/.test(typeGeom)) { feature = new ol.Feature(new ol.geom.LineString([0, 0])) } else { feature = new ol.Feature(new ol.geom.Polygon([[0, 0]])) } if (item.properties) { feature.setProperties(item.properties) } } if (feature) { style = feature.getStyle() if (typeof (style) === 'function') style = style(feature) if (!style) { style = typeof (item.style) === 'function' ? item.style(feature) : item.style || [] } typeGeom = feature.getGeometry().getType() } else { style = [] } if (!(style instanceof Array)) style = [style] var cx = width / 2 var cy = height / 2 var sx = size[0] / 2 var sy = size[1] / 2 var i, s // Get point offset if (typeGeom === 'Point') { var extent = null for (i = 0; s = style[i]; i++) { var img = s.getImage() // Refresh legend on image load if (img) { var imgElt = img.getPhoto ? img.getPhoto() : img.getImage() // Check image is loaded if (imgElt && imgElt instanceof HTMLImageElement && !imgElt.naturalWidth) { if (typeof (item.onload) === 'function') { imgElt.addEventListener('load', function () { setTimeout(function () { item.onload() }, 100) }) } img.load() } // Check anchor to center the image if (img.getAnchor) { var anchor = img.getAnchor() if (anchor) { var si = img.getSize() var dx = anchor[0] - si[0] var dy = anchor[1] - si[1] if (!extent) { extent = [dx, dy, dx + si[0], dy + si[1]] } else { ol.extent.extend(extent, [dx, dy, dx + si[0], dy + si[1]]) } } } } } if (extent) { cx = cx + (extent[2] + extent[0]) / 2 cy = cy + (extent[3] + extent[1]) / 2 } } // Draw image cy += offsetY || 0 for (i = 0; s = style[i]; i++) { vectorContext.setStyle(s) ctx.save() var geom switch (typeGeom) { case ol.geom.Point: case 'Point': case 'MultiPoint': { geom = new ol.geom.Point([cx, cy]) break } case ol.geom.LineString: case 'LineString': case 'MultiLineString': { // Clip lines ctx.rect(item.margin * ratio, 0, size[0] * ratio, canvas.height) ctx.clip() geom = new ol.geom.LineString([[cx - sx, cy], [cx + sx, cy]]) break } case ol.geom.Polygon: case 'Polygon': case 'MultiPolygon': { geom = new ol.geom.Polygon([[[cx - sx, cy - sy], [cx + sx, cy - sy], [cx + sx, cy + sy], [cx - sx, cy + sy], [cx - sx, cy - sy]]]) break } } // Geometry function? if (s.getGeometryFunction()) { geom = s.getGeometryFunction()(new ol.Feature(geom)) } vectorContext.drawGeometry(geom) ctx.restore() } ctx.restore() return canvas } /** Set legend title * @param {string} title */ setTitle(title) { this._title.setTitle(title) this.refresh() } /** Get legend title * @returns {string} */ getTitle() { return this._title.get('title') } /** Set the layer associated with the legend * @param {ol.layer.Layer} [layer] */ setLayer(layer) { if (this._layerListener) ol.Observable.unByKey(this._layerListener) this._layer = layer; if (layer) { this._layerListener = layer.on('change:visible', function() { this.refresh(); }.bind(this)) } else { this._layerListener = null; } } /** Get text Style * @returns {ol.style.Text} */ getTextStyle() { return this._textStyle } /** Set legend size * @param {ol.size} size */ set(key, value, opt_silent) { super.set(key, value, opt_silent) if (!opt_silent) this.refresh() } /** Get legend list element * @returns {Element} */ getListElement() { return this._listElement } /** Get legend canvas * @returns {HTMLCanvasElement} */ getCanvas() { return this._canvas } /** Set the style * @param { ol.style.Style | Array | ol.StyleFunction | undefined } style a style or a style function to use with features */ setStyle(style) { this._style = style this.refresh() } /** Add a new item to the legend * @param {olLegendItemOptions|ol.legend.Item} item */ addItem(item) { if (item instanceof ol.legend.Legend) { this._items.push(item) item.on('refresh', function() { this.refresh(true) }.bind(this)) } else if (item instanceof ol.legend.Item || item instanceof ol.legend.Image) { this._items.push(item) } else { this._items.push(new ol.legend.Item(item)) } } /** Remove an item at index * @param {ol.legend.Item} item */ removeItem(item) { this._items.remove(item) } /** Remove an item at index * @param {number} index */ removeItemAt(index) { this._items.removeAt(index) } /** Get item collection * @param {ol.Collection} */ getItems() { return this._items } /** Draw legend text * @private */ _drawText(ctx, text, x, y) { ctx.save() ctx.scale(ol.has.DEVICE_PIXEL_RATIO, ol.has.DEVICE_PIXEL_RATIO) text = text || '' var txt = text.split('\n') if (txt.length === 1) { ctx.fillText(text, x, y) } else { ctx.textBaseline = 'bottom' ctx.fillText(txt[0], x, y) ctx.textBaseline = 'top' ctx.fillText(txt[1], x, y) } ctx.restore() } /** Draw legend text * @private */ _measureText(ctx, text) { var txt = (text || '').split('\n') if (txt.length === 1) { return ctx.measureText(text) } else { var m1 = ctx.measureText(txt[0]) var m2 = ctx.measureText(txt[1]) return { width: Math.max(m1.width, m2.width), height: m1.height + m2.height } } } /** Refresh the legend */ refresh(opt_silent) { var table = this._listElement if (!table) return; table.innerHTML = '' var margin = this.get('margin') var width = this.get('size')[0] + 2 * margin var height = this.get('lineHeight') || this.get('size')[1] + 2 * margin var canvas = this.getCanvas() var ctx = canvas.getContext('2d') ctx.textAlign = 'left' ctx.textBaseline = 'middle' var ratio = ol.has.DEVICE_PIXEL_RATIO // Canvas size var w = Math.min(this.getWidth(), this.get('maxWidth') || Infinity); var h = this.getHeight() canvas.width = w * ratio canvas.height = h * ratio canvas.style.height = h + 'px' ctx.textBaseline = 'middle' ctx.fillStyle = ol.color.asString(this._textStyle.getFill().getColor()) // Add Title if (this.getTitle()) { table.appendChild(this._title.getElement([width, height], function (b) { this.dispatchEvent({ type: 'select', index: -1, symbol: b, item: this._title }) }.bind(this))) ctx.font = this._titleStyle.getFont() ctx.textAlign = 'center' this._drawText(ctx, this.getTitle(), canvas.width / ratio / 2, height / 2) } // Add items var offsetY = 0; if (this.getTitle()) offsetY = height; var nb = 0; this._items.forEach(function (r, i) { if (r instanceof ol.legend.Legend) { if ((!r._layer || r._layer.getVisible()) && r.getCanvas().height) { ctx.drawImage(r.getCanvas(), 0, offsetY * ratio) var list = r._listElement.querySelectorAll('li') for (var l=0; l} options.features a collection of feature to search in, the collection will be kept in date while selection * @param {ol.source.Vector | Array} options.source the source to search in if no features set * @param {string} options.btInfo ok button label */ ol.control.SelectBase = class olcontrolSelectBase extends ol.control.Control { constructor(options) { options = options || {}; var element = document.createElement('div'); super({ element: element, target: options.target }); this._features = this.setFeatures(options.features); if (!options.target) { element.className = 'ol-select ol-unselectable ol-control ol-collapsed'; ol.ext.element.create('BUTTON', { type: 'button', on: { 'click touchstart': function (e) { element.classList.toggle('ol-collapsed'); e.preventDefault(); } }, parent: element }); } if (options.className) element.classList.add(options.className); var content = options.content || ol.ext.element.create('DIV'); element.appendChild(content); // OK button ol.ext.element.create('BUTTON', { html: options.btInfo || 'OK', className: 'ol-ok', on: { 'click': this.doSelect.bind(this) }, parent: content }); this.setSources(options.source); } /** Set the current sources * @param {ol.source.Vector|Array|undefined} source */ setSources(source) { if (source) { this.set('source', (source instanceof Array) ? source : [source]); } else { this.unset('source'); } } /** Set feature collection to search in * @param {ol.Collection} features */ setFeatures(features) { if (features instanceof ol.Collection) this._features = features; else this._features = null; } /** Get feature collection to search in * @return {ol.Collection} */ getFeatures() { return this._features; } /** Escape string for regexp * @param {*} s value to escape * @return {string} */ _escape(s) { return String(s || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } /** * Test if a feature check aconditino * @param {ol.Feature} f the feature to check condition * @param {Object} condition an object to use for test * @param {string} condition.attr attribute name * @param {string} condition.op operator * @param {any} condition.val value to test * @param {boolean} usecase use case or not when testing strings * @return {boolean} * @private */ _checkCondition(f, condition, usecase) { if (!condition.attr) return true; var val = f.get(condition.attr); // Try to test numeric values var isNumber = (Number(val) == val && Number(condition.val) == condition.val); if (isNumber) val = Number(val); // Check var rex; switch (condition.op) { case '=': if (isNumber) { return val == condition.val; } else { rex = new RegExp('^' + this._escape(condition.val) + '$', usecase ? '' : 'i'); return rex.test(val); } case '!=': if (isNumber) { return val != condition.val; } else { rex = new RegExp('^' + this._escape(condition.val) + '$', usecase ? '' : 'i'); return !rex.test(val); } case '<': return val < condition.val; case '<=': return val <= condition.val; case '>': return val > condition.val; case '>=': return val >= condition.val; case 'contain': rex = new RegExp(this._escape(condition.val), usecase ? '' : 'i'); return rex.test(val); case '!contain': rex = new RegExp(this._escape(condition.val), usecase ? '' : 'i'); return !rex.test(val); case 'regexp': rex = new RegExp(condition.val, usecase ? '' : 'i'); return rex.test(val); case '!regexp': rex = new RegExp(condition.val, usecase ? '' : 'i'); return !rex.test(val); default: return false; } } /** Selection features in a list of features * @param {Array} result the current list of features * @param {Array} features to test in * @param {Object} condition * @param {string} condition.attr attribute name * @param {string} condition.op operator * @param {any} condition.val value to test * @param {boolean} all all conditions must be valid * @param {boolean} usecase use case or not when testing strings */ _selectFeatures(result, features, conditions, all, usecase) { conditions = conditions || []; var f; for (var i = features.length - 1; f = features[i]; i--) { var isok = all; for (var k = 0, c; c = conditions[k]; k++) { if (c.attr) { if (all) { isok = isok && this._checkCondition(f, c, usecase); } else { isok = isok || this._checkCondition(f, c, usecase); } } } if (isok) { result.push(f); } else if (this._features) { this._features.removeAt(i); } } return result; } /** Get vector source * @return {Array} */ getSources() { if (this.get('source')) return this.get('source'); var sources = []; function getSources(layers) { layers.forEach(function (l) { if (l.getLayers) { getSources(l.getLayers()); } else if (l.getSource && l.getSource() instanceof ol.source.Vector) { sources.push(l.getSource()); } }); } if (this.getMap()) { getSources(this.getMap().getLayers()); } return sources; } /** Select features by attributes * @param {*} options * @param {Array|undefined} options.sources source to apply rules, default the select sources * @param {bool} options.useCase case sensitive, default false * @param {bool} options.matchAll match all conditions, default false * @param {Array} options.conditions array of conditions * @return {Array} * @fires select */ doSelect(options) { options = options || {}; var features = []; if (options.features) { this._selectFeatures(features, options.features, options.conditions, options.matchAll, options.useCase); } else if (this._features) { this._selectFeatures(features, this._features.getArray(), options.conditions, options.matchAll, options.useCase); } else { var sources = options.sources || this.getSources(); sources.forEach(function (s) { this._selectFeatures(features, s.getFeatures(), options.conditions, options.matchAll, options.useCase); }.bind(this)); } this.dispatchEvent({ type: "select", features: features }); return features; } } /** List of operators / translation * @api */ ol.control.SelectBase.prototype.operationsList = { '=': '=', '!=': '≠', '<': '<', '<=': '≤', '>=': '≥', '>': '>', 'contain': '⊂', // ∈ '!contain': '⊄', // ∉ 'regexp': '≃', '!regexp': '≄' }; /* 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). */ /** A simple push button control * @constructor * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {String} options.title title of the control * @param {String} options.name an optional name, default none * @param {String} options.html html to insert in the control * @param {function} options.handleClick callback when control is clicked (or use change:active event) */ ol.control.Button = class olcontrolButton extends ol.control.Control { constructor(options) { options = options || {}; var element = document.createElement('div'); element.className = (options.className || '') + " ol-button ol-unselectable ol-control"; super({ element: element, target: options.target }); var self = this; var bt = this.button_ = document.createElement(/ol-text-button/.test(options.className) ? "div" : "button"); bt.type = "button"; if (options.title) bt.title = options.title; if (options.name) bt.name = options.name; if (options.html instanceof Element) bt.appendChild(options.html); else bt.innerHTML = options.html || ""; var evtFunction = function (e) { if (e && e.preventDefault) { e.preventDefault(); e.stopPropagation(); } if (options.handleClick) { options.handleClick.call(self, e); } }; bt.addEventListener("click", evtFunction); // bt.addEventListener("touchstart", evtFunction); element.appendChild(bt); // Try to get a title in the button content if (!options.title && bt.firstElementChild) { bt.title = bt.firstElementChild.title; } if (options.title) { this.set("title", options.title); } if (options.title) this.set("title", options.title); if (options.name) this.set("name", options.name); } /** Set the control visibility * @param {boolean} b */ setVisible(val) { if (val) ol.ext.element.show(this.element); else ol.ext.element.hide(this.element); } /** * Set the button title * @param {string} title */ setTitle(title) { this.button_.setAttribute('title', title); } /** * Set the button html * @param {string} html */ setHtml(html) { ol.ext.element.setHTML(this.button_, html); } /** * Get the button element * @returns {Element} */ getButtonElement() { return this.button_; } } /* 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). */ /** A simple toggle control * The control can be created with an interaction to control its activation. * * @constructor * @extends {ol.control.Button} * @fires change:active, change:disable * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {String} options.title title of the control * @param {String} options.html html to insert in the control * @param {ol.interaction} options.interaction interaction associated with the control * @param {bool} options.active the control is created active, default false * @param {bool} options.disable the control is created disabled, default false * @param {ol.control.Bar} options.bar a subbar associated with the control (drawn when active if control is nested in a ol.control.Bar) * @param {bool} options.autoActive the control will activate when shown in an ol.control.Bar, default false * @param {function} options.onToggle callback when control is clicked (or use change:active event) */ ol.control.Toggle = class olcontrolToggle extends ol.control.Button { constructor(options) { options = options || {}; if (options.toggleFn) { options.onToggle = options.toggleFn; // compat old version } options.handleClick = function () { self.toggle(); if (options.onToggle) { options.onToggle.call(self, self.getActive()); } }; options.className = (options.className || '') + ' ol-toggle'; super(options); var self = this; this.interaction_ = options.interaction; if (this.interaction_) { this.interaction_.setActive(options.active); this.interaction_.on("change:active", function () { self.setActive(self.interaction_.getActive()); }); } this.set("title", options.title); this.set("autoActivate", options.autoActivate); if (options.bar) this.setSubBar(options.bar); this.setActive(options.active); this.setDisable(options.disable); } /** * Set the map instance the control is associated with * and add interaction attached to it to this map. * @param {_ol_Map_} map The map instance. */ setMap(map) { if (!map && this.getMap()) { if (this.interaction_) { this.getMap().removeInteraction(this.interaction_); } if (this.subbar_) this.getMap().removeControl(this.subbar_); } super.setMap(map); if (map) { if (this.interaction_) map.addInteraction(this.interaction_); if (this.subbar_) map.addControl(this.subbar_); } } /** Get the subbar associated with a control * @return {ol.control.Bar} */ getSubBar() { return this.subbar_; } /** Set the subbar associated with a control * @param {ol.control.Bar} [bar] a subbar if none remove the current subbar */ setSubBar(bar) { var map = this.getMap(); if (map && this.subbar_) map.removeControl(this.subbar_); this.subbar_ = bar; if (bar) { this.subbar_.setTarget(this.element); this.subbar_.element.classList.add("ol-option-bar"); if (map) map.addControl(this.subbar_); } } /** * Test if the control is disabled. * @return {bool}. * @api stable */ getDisable() { var button = this.element.querySelector("button"); return button && button.disabled; } /** Disable the control. If disable, the control will be deactivated too. * @param {bool} b disable (or enable) the control, default false (enable) */ setDisable(b) { if (this.getDisable() == b) return; this.element.querySelector("button").disabled = b; if (b && this.getActive()) this.setActive(false); this.dispatchEvent({ type: 'change:disable', key: 'disable', oldValue: !b, disable: b }); } /** * Test if the control is active. * @return {bool}. * @api stable */ getActive() { return this.element.classList.contains("ol-active"); } /** Toggle control state active/deactive */ toggle() { if (this.getActive()) this.setActive(false); else this.setActive(true); } /** Change control state * @param {bool} b activate or deactivate the control, default false */ setActive(b) { if (this.interaction_) this.interaction_.setActive(b); if (this.subbar_) this.subbar_.setActive(b); if (this.getActive() === b) return; if (b) this.element.classList.add("ol-active"); else this.element.classList.remove("ol-active"); this.dispatchEvent({ type: 'change:active', key: 'active', oldValue: !b, active: b }); } /** Set the control interaction * @param {_ol_interaction_} i interaction to associate with the control */ setInteraction(i) { this.interaction_ = i; } /** Get the control interaction * @return {_ol_interaction_} interaction associated with the control */ getInteraction() { return this.interaction_; } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search Control. * This is the base class for search controls. You can use it for simple custom search or as base to new class. * @see ol.control.SearchFeature * @see ol.control.SearchPhoton * * @constructor * @extends {ol.control.Control} * @fires select * @fires change:input * @param {Object=} options * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.title Title to use for the search button tooltip, default "Search" * @param {string | undefined} options.reverseTitle Title to use for the reverse geocoding button tooltip, default "Click on the map..." * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {boolean | undefined} options.reverse enable reverse geocoding, default false * @param {string | undefined} options.inputLabel label for the input, default none * @param {string | undefined} options.collapsed search is collapsed on start, default true * @param {string | undefined} options.noCollapse prevent collapsing on input blur, default false * @param {number | undefined} options.typing a delay on each typing to start searching (ms) use -1 to prevent autocompletion, default 300. * @param {integer | undefined} options.minLength minimum length to start searching, default 1 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * @param {integer | undefined} options.maxHistory maximum number of items to display in history. Set -1 if you don't want history, default maxItems * @param {function} options.getTitle a function that takes a feature and return the name to display in the index. * @param {function} options.autocomplete a function that take a search string and callback function to send an array * @param {function} options.onselect a function called when a search is selected * @param {boolean} options.centerOnSelect center map on search, default false * @param {number|boolean} options.zoomOnSelect center map on search and zoom to value if zoom < value, default false */ ol.control.Search = class olcontrolSearch extends ol.control.Control { constructor(options) { options = options || {}; var classNames = (options.className || '') + ' ol-search' + (options.target ? '' : ' ol-unselectable ol-control'); var element = ol.ext.element.create('DIV', { className: classNames }); super({ element: element, target: options.target }); var self = this; if (options.typing == undefined) { options.typing = 300; } // Class name for history this._classname = options.className || 'search'; if (options.collapsed !== false) element.classList.add('ol-collapsed'); if (!options.target) { this.button = document.createElement('BUTTON'); this.button.setAttribute('type', 'button'); this.button.setAttribute('title', options.title || options.label || 'Search'); this.button.addEventListener('click', function () { element.classList.toggle('ol-collapsed'); if (!element.classList.contains('ol-collapsed')) { element.querySelector('input.search').focus(); var listElements = element.querySelectorAll('li'); for (var i = 0; i < listElements.length; i++) { listElements[i].classList.remove('select'); } // Display history if (!input.value) { self.drawList_(); } } }); element.appendChild(this.button); } // Input label if (options.inputLabel) { var label = document.createElement("LABEL"); label.innerText = options.inputLabel; element.appendChild(label); } // Search input var tout, cur = ""; var input = this._input = document.createElement("INPUT"); input.setAttribute("type", "search"); input.setAttribute("class", "search"); input.setAttribute("autocomplete", "off"); input.setAttribute("placeholder", options.placeholder || "Search..."); input.addEventListener("change", function (e) { self.dispatchEvent({ type: "change:input", input: e, value: input.value }); }); var doSearch = function (e) { // console.log(e.type+" "+e.key)' var li = element.querySelector("ul.autocomplete li.select"); var val = input.value; // move up/down if (e.key == 'ArrowDown' || e.key == 'ArrowUp' || e.key == 'Down' || e.key == 'Up') { if (li) { li.classList.remove("select"); li = (/Down/.test(e.key)) ? li.nextElementSibling : li.previousElementSibling; if (li) li.classList.add("select"); } else { element.querySelector("ul.autocomplete li").classList.add("select"); } } // Clear input else if (e.type == 'input' && !val) { setTimeout(function () { self.drawList_(); }, 200); } // Select in the list else if (li && (e.type == "search" || e.key == "Enter")) { if (element.classList.contains("ol-control")) input.blur(); li.classList.remove("select"); cur = val; self._handleSelect(self._list[li.getAttribute("data-search")]); } // Search / autocomplete else if ((e.type == "search" || e.key == 'Enter') || (cur != val && options.typing >= 0)) { // current search cur = val; if (cur) { // prevent searching on each typing if (tout) clearTimeout(tout); var minLength = self.get("minLength"); tout = setTimeout(function () { if (cur.length >= minLength) { var s = self.autocomplete(cur, function (auto) { self.drawList_(auto); }); if (s) self.drawList_(s); } else self.drawList_(); }, options.typing); } else { self.drawList_(); } } // Clear list selection else { li = element.querySelector("ul.autocomplete li"); if (li) li.classList.remove('select'); } }; input.addEventListener("keyup", doSearch); input.addEventListener("search", doSearch); input.addEventListener("cut", doSearch); input.addEventListener("paste", doSearch); input.addEventListener("input", doSearch); if (!options.noCollapse) { input.addEventListener('blur', function () { setTimeout(function () { if (input !== document.activeElement) { element.classList.add('ol-collapsed'); this.set('reverse', false); element.classList.remove('ol-revers'); } }.bind(this), 200); }.bind(this)); input.addEventListener('focus', function () { if (!this.get('reverse')) { element.classList.remove('ol-collapsed'); element.classList.remove('ol-revers'); } }.bind(this)); } element.appendChild(input); // Reverse geocode if (options.reverse) { var reverse = ol.ext.element.create('BUTTON', { type: 'button', class: 'ol-revers', title: options.reverseTitle || 'click on the map', click: function () { if (!this.get('reverse')) { this.set('reverse', !this.get('reverse')); input.focus(); element.classList.add('ol-revers'); } else { this.set('reverse', false); } }.bind(this) }); element.appendChild(reverse); } // Autocomplete list var ul = document.createElement('UL'); ul.classList.add('autocomplete'); element.appendChild(ul); if (typeof (options.getTitle) == 'function') this.getTitle = options.getTitle; if (typeof (options.autocomplete) == 'function') this.autocomplete = options.autocomplete; // Options this.set('copy', options.copy); this.set('minLength', options.minLength || 1); this.set('maxItems', options.maxItems || 10); this.set('maxHistory', options.maxHistory || options.maxItems || 10); // Select if (options.onselect) this.on('select', options.onselect); // Center on select if (options.centerOnSelect) { this.on('select', function (e) { var map = this.getMap(); if (map) { map.getView().setCenter(e.coordinate); } }.bind(this)); } // Zoom on select if (options.zoomOnSelect) { this.on('select', function (e) { var map = this.getMap(); if (map) { map.getView().setCenter(e.coordinate); if (map.getView().getZoom() < options.zoomOnSelect) map.getView().setZoom(options.zoomOnSelect); } }.bind(this)); } // History this.restoreHistory(); this.drawList_(); } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._listener) ol.Observable.unByKey(this._listener); this._listener = null; super.setMap(map); if (map) { this._listener = map.on('click', this._handleClick.bind(this)); } } /** Collapse the search * @param {boolean} [b=true] * @api */ collapse(b) { if (b === false) this.element.classList.remove('ol-collapsed'); else this.element.classList.add('ol-collapsed'); } /** Get the input field * @return {Element} * @api */ getInputField() { return this._input; } /** Returns the text to be displayed in the menu * @param {any} f feature to be displayed * @return {string} the text to be displayed in the index, default f.name * @api */ getTitle(f) { return f.name || "No title"; } /** Returns title as text * @param {any} f feature to be displayed * @return {string} * @api */ _getTitleTxt(f) { return ol.ext.element.create('DIV', { html: this.getTitle(f) }).innerText; } /** Force search to refresh */ search() { var search = this.element.querySelector("input.search"); this._triggerCustomEvent('search', search); } /** Reverse geocode * @param {Object} event * @param {ol.coordinate} event.coordinate * @private */ _handleClick(e) { if (this.get('reverse')) { document.activeElement.blur(); this.reverseGeocode(e.coordinate); } } /** Reverse geocode * @param {ol.coordinate} coord * @param {function | undefined} cback a callback function, default trigger a select event * @api */ reverseGeocode( /*coord, cback*/) { // this._handleSelect(f); } /** Trigger custom event on elemebt * @param {*} eventName * @param {*} element * @private */ _triggerCustomEvent(eventName, element) { ol.ext.element.dispatchEvent(eventName, element); } /** Set the input value in the form (for initialisation purpose) * @param {string} value * @param {boolean} search to start a search * @api */ setInput(value, search) { var input = this.element.querySelector("input.search"); input.value = value; if (search) this._triggerCustomEvent("keyup", input); } /** A line has been clicked in the menu > dispatch event * @param {any} f the feature, as passed in the autocomplete * @param {boolean} reverse true if reverse geocode * @param {ol.coordinate} coord * @param {*} options options passed to the event * @api */ select(f, reverse, coord, options) { var event = { type: "select", search: f, reverse: !!reverse, coordinate: coord }; if (options) { for (var i in options) { event[i] = options[i]; } } this.dispatchEvent(event); } /** * Save history and select * @param {*} f * @param {boolean} reverse true if reverse geocode * @param {*} options options send in the event * @private */ _handleSelect(f, reverse, options) { if (!f) return; // Save input in history var hist = this.get('history'); // Prevent error on stringify var i; try { var fstr = JSON.stringify(f); for (i = hist.length - 1; i >= 0; i--) { if (!hist[i] || JSON.stringify(hist[i]) === fstr) { hist.splice(i, 1); } } } catch (e) { for (i = hist.length - 1; i >= 0; i--) { if (hist[i] === f) { hist.splice(i, 1); } } } hist.unshift(f); var size = Math.max(0, this.get('maxHistory') || 10) || 0; while (hist.length > size) { hist.pop(); } this.saveHistory(); // Select feature this.select(f, reverse, null, options); if (reverse) { this.setInput(this._getTitleTxt(f)); this.drawList_(); setTimeout(function () { this.collapse(false); }.bind(this), 300); } } /** Save history (in the localstorage) */ saveHistory() { try { if (this.get('maxHistory') >= 0) { localStorage["ol@search-" + this._classname] = JSON.stringify(this.get('history')); } else { localStorage.removeItem("ol@search-" + this._classname); } } catch (e) { console.warn('Failed to access localStorage...'); } } /** Restore history (from the localstorage) */ restoreHistory() { if (this._history[this._classname]) { this.set('history', this._history[this._classname]); } else { try { this._history[this._classname] = JSON.parse(localStorage["ol@search-" + this._classname]); this.set('history', this._history[this._classname]); } catch (e) { this.set('history', []); } } } /** * Remove previous history */ clearHistory() { this.set('history', []); this.saveHistory(); this.drawList_(); } /** * Get history table */ getHistory() { return this.get('history'); } /** Autocomplete function * @param {string} s search string * @param {function} cback a callback function that takes an array to display in the autocomplete field (for asynchronous search) * @return {Array|false} an array of search solutions or false if the array is send with the cback argument (asnchronous) * @api */ autocomplete(s, cback) { cback([]); return false; // or just return []; } /** Draw the list * @param {Array} auto an array of search result * @private */ drawList_(auto) { var self = this; var ul = this.element.querySelector("ul.autocomplete"); ul.innerHTML = ''; this._list = []; if (!auto) { var input = this.element.querySelector("input.search"); var value = input.value; if (!value) { auto = this.get('history'); } else { return; } ul.setAttribute('class', 'autocomplete history'); } else { ul.setAttribute('class', 'autocomplete'); } var li, max = Math.min(self.get("maxItems"), auto.length); for (var i = 0; i < max; i++) { if (auto[i]) { if (!i || !self.equalFeatures(auto[i], auto[i - 1])) { li = document.createElement("LI"); li.setAttribute("data-search", this._list.length); this._list.push(auto[i]); li.addEventListener("click", function (e) { self._handleSelect(self._list[e.currentTarget.getAttribute("data-search")]); }); var title = self.getTitle(auto[i]); if (title instanceof Element) li.appendChild(title); else li.innerHTML = title; ul.appendChild(li); } } } if (max && this.get("copy")) { li = document.createElement("LI"); li.classList.add("copy"); li.innerHTML = this.get("copy"); ul.appendChild(li); } } /** Test if 2 features are equal * @param {any} f1 * @param {any} f2 * @return {boolean} */ equalFeatures( /* f1, f2 */) { return false; } } /** Current history */ ol.control.Search.prototype._history = {}; /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * This is the base class for search controls that use a json service to search features. * You can use it for simple custom search or as base to new class. * * @constructor * @extends {ol.control.Search} * @fires select * @param {any} options extend ol.control.Search options * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.title Title to use for the search button tooltip, default "Search" * @param {string | undefined} options.reverseTitle Title to use for the reverse geocoding button tooltip, default "Click on the map..." * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 1000. * @param {integer | undefined} options.minLength minimum length to start searching, default 3 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * @param {function | undefined} options.handleResponse Handle server response to pass the features array to the list * * @param {string|undefined} options.url Url of the search api * @param {string | undefined} options.authentication: basic authentication for the search API as btoa("login:pwd") */ ol.control.SearchJSON = class olcontrolSearchJSON extends ol.control.Search { constructor(options) { options = options || {}; options.className = options.className || 'JSON'; delete options.autocomplete; options.minLength = options.minLength || 3; options.typing = options.typing || 800; super(options); // Handle Mix Content Warning // If the current connection is an https connection all other connections must be https either var url = options.url || ""; if (window.location.protocol === "https:") { var parser = document.createElement('a'); parser.href = url; parser.protocol = window.location.protocol; url = parser.href; } this.set('url', url); this._ajax = new ol.ext.Ajax({ dataType: 'JSON', auth: options.authentication }); this._ajax.on('success', function (resp) { if (resp.status >= 200 && resp.status < 400) { if (typeof (this._callback) === 'function') this._callback(resp.response); } else { if (typeof (this._callback) === 'function') this._callback(false, 'error'); console.log('AJAX ERROR', arguments); } }.bind(this)); this._ajax.on('error', function () { if (typeof (this._callback) === 'function') this._callback(false, 'error'); console.log('AJAX ERROR', arguments); }.bind(this)); // Handle searchin this._ajax.on('loadstart', function () { this.element.classList.add('searching'); }.bind(this)); this._ajax.on('loadend', function () { this.element.classList.remove('searching'); }.bind(this)); // Overwrite handleResponse if (typeof (options.handleResponse) === 'function') this.handleResponse = options.handleResponse; } /** Send ajax request * @param {string} url * @param {*} data * @param {function} cback a callback function that takes an array of {name, feature} to display in the autocomplete field */ ajax(url, data, cback, options) { options = options || {}; this._callback = cback; this._ajax.set('dataType', options.dataType || 'JSON'); this._ajax.send(url, data, options); } /** Autocomplete function (ajax request to the server) * @param {string} s search string * @param {function} cback a callback function that takes an array of {name, feature} to display in the autocomplete field */ autocomplete(s, cback) { var data = this.requestData(s); var url = encodeURI(this.get('url')); this.ajax(url, data, function (resp) { if (typeof (cback) === 'function') cback(this.handleResponse(resp)); }); } /** * @param {string} s the search string * @return {Object} request data (as key:value) * @api */ requestData(s) { return { q: s }; } /** * Handle server response to pass the features array to the display list * @param {any} response server response * @return {Array} an array of feature * @api */ handleResponse(response) { return response; } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search places using the photon API. * * @constructor * @extends {ol.control.SearchJSON} * @fires select * @param {Object=} Control options. * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.title Title to use for the search button tooltip, default "Search" * @param {string | undefined} options.reverseTitle Title to use for the reverse geocoding button tooltip, default "Click on the map..." * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 1000. * @param {integer | undefined} options.minLength minimum length to start searching, default 3 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * @param {function | undefined} options.handleResponse Handle server response to pass the features array to the list * * @param {string|undefined} options.url Url to photon api, default "http://photon.komoot.de/api/" * @param {string|undefined} options.lang Force preferred language, default none * @param {boolean} options.position Search, with priority to geo position, default false * @param {function} options.getTitle a function that takes a feature and return the name to display in the index, default return street + name + contry */ ol.control.SearchPhoton = class olcontrolSearchPhoton extends ol.control.SearchJSON { constructor(options) { options = options || {}; options.className = options.className || 'photon'; options.url = options.url || 'https://photon.komoot.io/api/'; options.copy = options.copy || '© OpenStreetMap contributors'; super(options); this.set('lang', options.lang); this.set('position', options.position); } /** Returns the text to be displayed in the menu * @param {ol.Feature} f the feature * @return {string} the text to be displayed in the index * @api */ getTitle(f) { var p = f.properties; return (p.housenumber || "") + " " + (p.street || p.name || "") + "" + " " + (p.postcode || "") + " " + (p.city || "") + " (" + p.country + ")"; } /** * @param {string} s the search string * @return {Object} request data (as key:value) * @api */ requestData(s) { var data = { q: s, lang: this.get('lang'), limit: this.get('maxItems') }; // Handle position proirity if (this.get('position')) { var view = this.getMap().getView(); var pt = new ol.geom.Point(view.getCenter()); pt = (pt.transform(view.getProjection(), "EPSG:4326")).getCoordinates(); data.lon = pt[0]; data.lat = pt[1]; } return data; } /** * Handle server response to pass the features array to the list * @param {any} response server response * @return {Array} an array of feature */ handleResponse(response) { return response.features; } /** Prevent same feature to be drawn twice: test equality * @param {} f1 First feature to compare * @param {} f2 Second feature to compare * @return {boolean} * @api */ equalFeatures(f1, f2) { return (this.getTitle(f1) === this.getTitle(f2) && f1.geometry.coordinates[0] === f2.geometry.coordinates[0] && f1.geometry.coordinates[1] === f2.geometry.coordinates[1]); } /** A ligne has been clicked in the menu > dispatch event * @param {any} f the feature, as passed in the autocomplete * @api */ select(f) { var c = f.geometry.coordinates; // Add coordinate to the event try { c = ol.proj.transform(f.geometry.coordinates, 'EPSG:4326', this.getMap().getView().getProjection()); } catch (e) { /* ok */ } this.dispatchEvent({ type: "select", search: f, coordinate: c }); } /** Get data for reverse geocode * @param {ol.coordinate} coord */ reverseData(coord) { var lonlat = ol.proj.transform(coord, this.getMap().getView().getProjection(), 'EPSG:4326'); return { lon: lonlat[0], lat: lonlat[1] }; } /** Reverse geocode * @param {ol.coordinate} coord * @api */ reverseGeocode(coord, cback) { this.ajax( this.get('url').replace('/api/', '/reverse/').replace('/search/', '/reverse/'), this.reverseData(coord), function (resp) { if (resp.features) resp = resp.features; if (!(resp instanceof Array)) resp = [resp]; if (cback) { cback.call(this, resp); } else { this._handleSelect(resp[0], true); // this.setInput('', true); } }.bind(this) ); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search places using the French National Base Address (BAN) API. * * @constructor * @extends {ol.control.SearchJSON} * @fires select * @param {any} options extend ol.control.SearchJSON options * @param {string} options.className control class name * @param {string | undefined} [options.apiKey] the service api key. * @param {string | undefined} [options.version] API version '2' to use geocodage-beta-2, default v1 * @param {string | undefined} options.authentication: basic authentication for the service API as btoa("login:pwd") * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {boolean | undefined} options.reverse enable reverse geocoding, default false * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 500. * @param {integer | undefined} options.minLength minimum length to start searching, default 3 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * * @param {StreetAddress|PositionOfInterest|CadastralParcel|Commune} options.type type of search. Using Commune will return the INSEE code, default StreetAddress,PositionOfInterest * @see {@link https://geoservices.ign.fr/documentation/geoservices/geocodage.html} * @see {@link https://geoservices.ign.fr/documentation/services/api-et-services-ogc/geocodage-beta-20/documentation-technique-de-lapi} */ ol.control.SearchGeoportail = class olcontrolSearchGeoportail extends ol.control.SearchJSON { constructor(options) { options = options || {}; options.className = options.className || 'IGNF'; options.typing = options.typing || 500; if (options.version == 1) { options.url = 'https://wxs.ign.fr/' + (options.apiKey || 'essentiels') + '/ols/apis/completion'; } else { options.url = 'https://wxs.ign.fr/' + (options.apiKey || 'essentiels') + '/geoportail/geocodage/rest/0.1/completion'; } options.copy = '© IGN-Géoportail'; super(options); this.set('type', options.type || 'StreetAddress,PositionOfInterest'); this.set('timeout', options.timeout || 2000); // Authentication // this._auth = options.authentication; } /** Reverse geocode * @param {ol.coordinate} coord * @param {function|*} options callback function called when revers located or options passed to the select event * @api */ reverseGeocode(coord, options) { var lonlat = ol.proj.transform(coord, this.getMap().getView().getProjection(), 'EPSG:4326'); this._handleSelect({ x: lonlat[0], y: lonlat[1], fulltext: lonlat[0].toFixed(6) + ',' + lonlat[1].toFixed(6) }, true, options); // Search type var type = this.get('type') === 'Commune' ? 'PositionOfInterest' : this.get('type') || 'StreetAddress'; if (/,/.test(type)) type = 'StreetAddress'; // Search url var url = this.get('url').replace('ols/apis/completion', 'geoportail/ols').replace('completion', 'reverse'); if (/ols/.test(url)) { // request var request = '' + '' + ' ' + ' ' + ' ' + type + '' + ' ' + ' ' + lonlat[1] + ' ' + lonlat[0] + '' + ' ' + ' ' + ' ' + ''; this.ajax(url, { xls: request }, function (xml) { var f = {}; if (!xml) { f = { x: lonlat[0], y: lonlat[1], fulltext: lonlat[0].toFixed(6) + ',' + lonlat[1].toFixed(6) }; } else { xml = xml.replace(/\n|\r/g, ''); var p = (xml.replace(/.*(.*)<\/gml:pos>.*/, "$1")).split(' '); if (!Number(p[1]) && !Number(p[0])) { f = { x: lonlat[0], y: lonlat[1], fulltext: lonlat[0].toFixed(6) + ',' + lonlat[1].toFixed(6) }; } else { f.x = lonlat[0]; f.y = lonlat[1]; f.city = (xml.replace(/.*([^<]*)<\/Place>.*/, "$1")); f.insee = (xml.replace(/.*([^<]*)<\/Place>.*/, "$1")); f.zipcode = (xml.replace(/.*([^<]*)<\/PostalCode>.*/, "$1")); if (//.test(xml)) { f.kind = ''; f.country = 'StreetAddress'; f.street = (xml.replace(/.*([^<]*)<\/Street>.*/, "$1")); var number = (xml.replace(/.*([^<]*)<\/Place>.*/, "$1")); f.country = 'PositionOfInterest'; f.street = ''; f.fulltext = f.zipcode + ' ' + f.city; } } } if (typeof (options) === 'function') { options.call(this, [f]); } else { this.getHistory().shift(); this._handleSelect(f, true, options); // this.setInput('', true); // this.drawList_(); } }.bind(this), { timeout: this.get('timeout'), dataType: 'XML' }); } else { this.ajax(url + '?lon='+lonlat[0] + '&lat=' + lonlat[1], {}, function(resp) { var f; try { resp = JSON.parse(resp).features[0]; f = resp.properties; // lonlat f.x = resp.geometry.coordinates[0]; f.y = resp.geometry.coordinates[1]; f.click = lonlat; // Fulltext if (f.name) { f.fulltext = f.name + ', ' + f.postcode + ' ' + f.city; } else { f.fulltext = f.postcode + ' ' + f.city; } } catch(e) { f = { x: lonlat[0], y: lonlat[1], lonlat: lonlat, fulltext: lonlat[0].toFixed(6) + ',' + lonlat[1].toFixed(6) }; } if (typeof (options) === 'function') { options.call(this, [f]); } else { this.getHistory().shift(); this._handleSelect(f, true, options); // this.setInput('', true); // this.drawList_(); } }.bind(this), { timeout: this.get('timeout'), dataType: 'XML' } ); } } /** Returns the text to be displayed in the menu * @param {ol.Feature} f the feature * @return {string} the text to be displayed in the index * @api */ getTitle(f) { return (f.fulltext); } /** * @param {string} s the search string * @return {Object} request data (as key:value) * @api */ requestData(s) { return { text: s, type: this.get('type') === 'Commune' ? 'PositionOfInterest' : this.get('type') || 'StreetAddress,PositionOfInterest', maximumResponses: this.get('maxItems') }; } /** * Handle server response to pass the features array to the display list * @param {any} response server response * @return {Array} an array of feature * @api */ handleResponse(response) { var features = response.results; if (this.get('type') === 'Commune') { for (var i = features.length - 1; i >= 0; i--) { if (features[i].kind && (features[i].classification > 5 || features[i].kind == "Département")) { features.splice(i, 1); } } } return features; } /** A ligne has been clicked in the menu > dispatch event * @param {any} f the feature, as passed in the autocomplete * @param {boolean} reverse true if reverse geocode * @param {ol.coordinate} coord * @param {*} options options passed to the event * @api */ select(f, reverse, coord, options) { if (f.x || f.y) { var c = [Number(f.x), Number(f.y)]; // Add coordinate to the event try { c = ol.proj.transform(c, 'EPSG:4326', this.getMap().getView().getProjection()); } catch (e) { /* ok */ } // Get insee commune ? if (this.get('type') === 'Commune') { this.searchCommune(f, function () { ol.control.Search.prototype.select.call(this, f, reverse, c, options); //this.dispatchEvent({ type:"select", search:f, coordinate: c, revers: reverse, options: options }); }.bind(this)); } else { super.select(f, reverse, c, options); //this.dispatchEvent({ type:"select", search:f, coordinate: c, revers: reverse, options: options }); } } else { this.searchCommune(f); } } /** Search if no position and get the INSEE code * @param {string} s le nom de la commune */ searchCommune(f, cback) { // Search url var url = this.get('url').replace('ols/apis/completion', 'geoportail/ols').replace('completion', 'reverse'); if (/ols/.test(url)) { var request = '' + '' + '' + '' + '' + '
' + '' + f.zipcode + ' ' + f.city + '+' + '
' + '
' + '
' + '
'; // Search this.ajax(this.get('url').replace('ols/apis/completion', 'geoportail/ols'), { 'xls': request }, function (xml) { if (xml) { // XML to JSON var parser = new DOMParser(); var xmlDoc = parser.parseFromString(xml, "text/xml"); var com = xmlDoc.getElementsByTagName('GeocodedAddress')[0]; var coord = com.getElementsByTagName('gml:Point')[0].textContent.trim().split(' '); f.x = Number(coord[1]); f.y = Number(coord[0]); var place = com.getElementsByTagName('Place'); for (var i = 0; i < place.length; i++) { switch (place[i].attributes.type.value) { case 'Nature': f.kind = place[i].textContent; break; case 'INSEE': f.insee = place[i].textContent; break; } } if (f.x || f.y) { if (cback) cback.call(this, [f]); else this._handleSelect(f); } } }.bind(this), { dataType: 'XML' } ); } else { this.ajax(url + '?lon=' + f.x + '&lat=' + f.y + '&limit=1', {}, function (resp) { try { var r = JSON.parse(resp).features[0]; f.insee = r.properties.citycode if (cback) { cback.call(this, [f]); } else { this._handleSelect(f); } } catch(e) { /* ok */ } }.bind(this), { timeout: this.get('timeout'), dataType: 'XML' } ) } } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ // ol < 6 compatibility VectorImage is not defined // /** Layer Switcher Control. * @fires select * @fires drawlist * @fires toggle * @fires reorder-start * @fires reorder-end * @fires layer:visible * @fires layer:opacity * * @constructor * @extends {ol.control.Control} * @param {Object=} options * @param {boolean} options.selection enable layer selection when click on the title * @param {function} options.displayInLayerSwitcher function that takes a layer and return a boolean if the layer is displayed in the switcher, default test the displayInLayerSwitcher layer attribute * @param {boolean} options.show_progress show a progress bar on tile layers, default false * @param {boolean} options.mouseover show the panel on mouseover, default false * @param {boolean} options.reordering allow layer reordering, default true * @param {boolean} options.trash add a trash button to delete the layer, default false * @param {function} options.oninfo callback on click on info button, if none no info button is shown DEPRECATED: use on(info) instead * @param {boolean} options.extent add an extent button to zoom to the extent of the layer * @param {function} options.onextent callback when click on extent, default fits view to extent * @param {number} options.drawDelay delay in ms to redraw the layer (usefull to prevent flickering when manipulating the layers) * @param {boolean} options.collapsed collapse the layerswitcher at beginning, default true * @param {ol.layer.Group} options.layerGroup a layer group to display in the switcher, default display all layers of the map * @param {boolean} options.noScroll prevent handle scrolling, default false * @param {function} options.onchangeCheck optional callback on click on checkbox, you can call this method for doing operations after check/uncheck a layer * * Layers attributes that control the switcher * - allwaysOnTop {boolean} true to force layer stay on top of the others while reordering, default false * - displayInLayerSwitcher {boolean} display the layer in switcher, default true * - noSwitcherDelete {boolean} to prevent layer deletion (w. trash option = true), default false */ ol.control.LayerSwitcher = class olcontrolLayerSwitcher extends ol.control.Control { constructor(options) { options = options || {} var element = ol.ext.element.create('DIV', { className: options.switcherClass || 'ol-layerswitcher' }) super({ element: element, target: options.target }) var self = this this.dcount = 0 this.show_progress = options.show_progress this.oninfo = (typeof (options.oninfo) == 'function' ? options.oninfo : null) this.onextent = (typeof (options.onextent) == 'function' ? options.onextent : null) this.hasextent = options.extent || options.onextent this.hastrash = options.trash this.reordering = (options.reordering !== false) this._layers = [] this._layerGroup = (options.layerGroup && options.layerGroup.getLayers) ? options.layerGroup : null this.onchangeCheck = (typeof (options.onchangeCheck) == "function" ? options.onchangeCheck : null) // displayInLayerSwitcher if (typeof (options.displayInLayerSwitcher) === 'function') { this.displayInLayerSwitcher = options.displayInLayerSwitcher } // Insert in the map if (!options.target) { element.classList.add('ol-unselectable') element.classList.add('ol-control') element.classList.add(options.collapsed !== false ? 'ol-collapsed' : 'ol-forceopen') this.button = ol.ext.element.create('BUTTON', { type: 'button', parent: element }) this.button.addEventListener('touchstart', function (e) { element.classList.toggle('ol-forceopen') element.classList.add('ol-collapsed') self.dispatchEvent({ type: 'toggle', collapsed: element.classList.contains('ol-collapsed') }) e.preventDefault() self.overflow() }) this.button.addEventListener('click', function () { element.classList.toggle('ol-forceopen') element.classList.add('ol-collapsed') self.dispatchEvent({ type: 'toggle', collapsed: !element.classList.contains('ol-forceopen') }) self.overflow() }) if (options.mouseover) { element.addEventListener('mouseleave', function () { element.classList.add("ol-collapsed") self.dispatchEvent({ type: 'toggle', collapsed: true }) }) element.addEventListener('mouseover', function () { element.classList.remove("ol-collapsed") self.dispatchEvent({ type: 'toggle', collapsed: false }) }) } if (options.minibar) options.noScroll = true if (!options.noScroll) { this.topv = ol.ext.element.create('DIV', { className: 'ol-switchertopdiv', parent: element, click: function () { self.overflow("+50%") } }) this.botv = ol.ext.element.create('DIV', { className: 'ol-switcherbottomdiv', parent: element, click: function () { self.overflow("-50%") } }) } this._noScroll = options.noScroll } this.panel_ = ol.ext.element.create('UL', { className: 'panel', }) this.panelContainer_ = ol.ext.element.create('DIV', { className: 'panel-container', html: this.panel_, parent: element }) // Handle mousewheel if (!options.target && !options.noScroll) { ol.ext.element.addListener(this.panel_, 'mousewheel DOMMouseScroll onmousewheel', function (e) { if (self.overflow(Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail))))) { e.stopPropagation() e.preventDefault() } }) } this.header_ = ol.ext.element.create('LI', { className: 'ol-header', parent: this.panel_ }) this.set('drawDelay', options.drawDelay || 0) this.set('selection', options.selection) if (options.minibar) { // Wait init complete (for child classes) setTimeout(function () { var mbar = ol.ext.element.scrollDiv(this.panelContainer_, { mousewheel: true, vertical: true, minibar: true }) this.on(['drawlist', 'toggle'], function () { mbar.refresh() }) }.bind(this)) } } /** Test if a layer should be displayed in the switcher * @param {ol.layer} layer * @return {boolean} true if the layer is displayed */ displayInLayerSwitcher(layer) { return (layer.get('displayInLayerSwitcher') !== false) } /** * Set the map instance the control is associated with. * @param {_ol_Map_} map The map instance. */ setMap(map) { super.setMap(map) this.drawPanel() if (this._listener) { for (var i in this._listener){ ol.Observable.unByKey(this._listener[i]) } } this._listener = null // Get change (new layer added or removed) if (map) { this._listener = { moveend: map.on('moveend', this.viewChange.bind(this)), size: map.on('change:size', this.overflow.bind(this)) } // Listen to a layer group if (this._layerGroup) { this._listener.change = this._layerGroup.getLayers().on('change:length', this.drawPanel.bind(this)) } else { //Listen to all layers this._listener.change = map.getLayerGroup().getLayers().on('change:length', this.drawPanel.bind(this)) } } } /** Show control */ show() { this.element.classList.add('ol-forceopen') this.overflow() this.dispatchEvent({ type: 'toggle', collapsed: false }) } /** Hide control */ hide() { this.element.classList.remove('ol-forceopen') this.overflow() this.dispatchEvent({ type: 'toggle', collapsed: true }) } /** Toggle control */ toggle() { this.element.classList.toggle("ol-forceopen") this.overflow() this.dispatchEvent({ type: 'toggle', collapsed: !this.isOpen() }) } /** Is control open * @return {boolean} */ isOpen() { return this.element.classList.contains("ol-forceopen") } /** Add a custom header * @param {Element|string} html content html */ setHeader(html) { ol.ext.element.setHTML(this.header_, html) } /** Calculate overflow and add scrolls * @param {Number} dir scroll direction -1|0|1|'+50%'|'-50%' * @private */ overflow(dir) { if (this.button && !this._noScroll) { // Nothing to show if (ol.ext.element.hidden(this.panel_)) { ol.ext.element.setStyle(this.element, { height: 'auto' }) return } // Calculate offset var h = ol.ext.element.outerHeight(this.element) var hp = ol.ext.element.outerHeight(this.panel_) var dh = this.button.offsetTop + ol.ext.element.outerHeight(this.button) //var dh = this.button.position().top + this.button.outerHeight(true); var top = this.panel_.offsetTop - dh if (hp > h - dh) { // Bug IE: need to have an height defined ol.ext.element.setStyle(this.element, { height: '100%' }) var li = this.panel_.querySelectorAll('li.ol-visible .li-content')[0] var lh = li ? 2 * ol.ext.element.getStyle(li, 'height') : 0 switch (dir) { case 1: top += lh; break case -1: top -= lh; break case "+50%": top += Math.round(h / 2); break case "-50%": top -= Math.round(h / 2); break default: break } // Scroll div if (top + hp <= h - 3 * dh / 2) { top = h - 3 * dh / 2 - hp ol.ext.element.hide(this.botv) } else { ol.ext.element.show(this.botv) } if (top >= 0) { top = 0 ol.ext.element.hide(this.topv) } else { ol.ext.element.show(this.topv) } // Scroll ? ol.ext.element.setStyle(this.panel_, { top: top + "px" }) return true } else { ol.ext.element.setStyle(this.element, { height: "auto" }) ol.ext.element.setStyle(this.panel_, { top: 0 }) ol.ext.element.hide(this.botv) ol.ext.element.hide(this.topv) return false } } else return false } /** Set the layer associated with a li * @param {Element} li * @param {ol.layer} layer * @private */ _setLayerForLI(li, layer) { var listeners = [] if (layer.getLayers) { listeners.push(layer.getLayers().on('change:length', this.drawPanel.bind(this))) } if (li) { // Handle opacity change listeners.push(layer.on('change:opacity', (function () { this.setLayerOpacity(layer, li) }).bind(this))) // Handle visibility chage listeners.push(layer.on('change:visible', (function () { this.setLayerVisibility(layer, li) }).bind(this))) } // Other properties listeners.push(layer.on('propertychange', (function (e) { if (e.key === 'displayInLayerSwitcher' || e.key === 'openInLayerSwitcher' || e.key === 'title' || e.key === 'name') { this.drawPanel(e) } }).bind(this))) this._layers.push({ li: li, layer: layer, listeners: listeners }) } /** Set opacity for a layer * @param {ol.layer.Layer} layer * @param {Element} li the list element * @private */ setLayerOpacity(layer, li) { var i = li.querySelector('.layerswitcher-opacity-cursor') if (i){ i.style.left = (layer.getOpacity() * 100) + "%" } this.dispatchEvent({ type: 'layer:opacity', layer: layer }) } /** Set visibility for a layer * @param {ol.layer.Layer} layer * @param {Element} li the list element * @api */ setLayerVisibility(layer, li) { var i = li.querySelector('.ol-visibility') if (i) { i.checked = layer.getVisible() } if (layer.getVisible()){ li.classList.add('ol-visible') } else{ li.classList.remove('ol-visible') } this.dispatchEvent({ type: 'layer:visible', layer: layer }) } /** Clear layers associated with li * @private */ _clearLayerForLI() { this._layers.forEach(function (li) { li.listeners.forEach(function (l) { ol.Observable.unByKey(l) }) }) this._layers = [] } /** Get the layer associated with a li * @param {Element} li * @return {ol.layer} * @private */ _getLayerForLI(li) { for (var i = 0, l; l = this._layers[i]; i++) { if (l.li === li) { return l.layer } } return null } /** * On view change hide layer depending on resolution / extent * @private */ viewChange() { this.panel_.querySelectorAll('li').forEach(function (li) { var l = this._getLayerForLI(li) if (l) { if (this.testLayerVisibility(l)) { li.classList.remove('ol-layer-hidden') } else { li.classList.add('ol-layer-hidden') } } }.bind(this)) } /** Get control panel * @api */ getPanel() { return this.panelContainer_ } /** Draw the panel control (prevent multiple draw due to layers manipulation on the map with a delay function) * @api */ drawPanel() { if (!this.getMap()) return var self = this // Multiple event simultaneously / draw once => put drawing in the event queue this.dcount++ setTimeout(function () { self.drawPanel_() }, this.get('drawDelay') || 0) } /** Delayed draw panel control * @private */ drawPanel_() { if (--this.dcount || this.dragging_) { return } var scrollTop = this.panelContainer_.scrollTop // Remove existing layers this._clearLayerForLI() this.panel_.querySelectorAll('li').forEach(function (li) { if (!li.classList.contains('ol-header')) li.remove() }.bind(this)) // Draw list if (this._layerGroup) { this.drawList(this.panel_, this._layerGroup.getLayers()) } else if (this.getMap()) { this.drawList(this.panel_, this.getMap().getLayers()) } // Reset scrolltop this.panelContainer_.scrollTop = scrollTop } /** Change layer visibility according to the baselayer option * @param {ol.layer} * @param {Array} related layers * @private */ switchLayerVisibility(l, layers) { if (!l.get('baseLayer')) { l.setVisible(!l.getVisible()) } else { if (!l.getVisible()) { l.setVisible(true) } layers.forEach(function (li) { if (l !== li && li.get('baseLayer') && li.getVisible()) { li.setVisible(false) } }) } } /** Check if layer is on the map (depending on resolution / zoom and extent) * @param {ol.layer} * @return {boolean} * @private */ testLayerVisibility(layer) { if (!this.getMap()) return true var res = this.getMap().getView().getResolution() var zoom = this.getMap().getView().getZoom() // Calculate visibility on resolution or zoom if (layer.getMaxResolution() <= res || layer.getMinResolution() >= res) { return false } else if (layer.getMinZoom && (layer.getMinZoom() >= zoom || layer.getMaxZoom() < zoom)) { return false } else { // Check extent var ex0 = layer.getExtent() if (ex0) { var ex = this.getMap().getView().calculateExtent(this.getMap().getSize()) return ol.extent.intersects(ex, ex0) } return true } } /** Start ordering the list * @param {event} e drag event * @private */ dragOrdering_(e) { e.stopPropagation() e.preventDefault() // Get params var self = this var elt = e.currentTarget.parentNode.parentNode var start = true var panel = this.panel_ var pageY var pageY0 = e.pageY || (e.touches && e.touches.length && e.touches[0].pageY) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY) var target, dragElt var layer, group elt.parentNode.classList.add('drag') // Stop ordering function stop() { if (target) { // Get drag on parent var drop = layer var isSelected = self.getSelection() === drop if (drop && target) { var collection if (group) collection = group.getLayers() else collection = self._layerGroup ? self._layerGroup.getLayers() : self.getMap().getLayers() var layers = collection.getArray() // Switch layers for (var i = 0; i < layers.length; i++) { if (layers[i] == drop) { collection.removeAt(i) break } } for (var j = 0; j < layers.length; j++) { if (layers[j] === target) { if (i > j) collection.insertAt(j, drop) else collection.insertAt(j + 1, drop) break } } } if (isSelected) self.selectLayer(drop) self.dispatchEvent({ type: "reorder-end", layer: drop, group: group }) } elt.parentNode.querySelectorAll('li').forEach(function (li) { li.classList.remove('dropover') li.classList.remove('dropover-after') li.classList.remove('dropover-before') }) elt.classList.remove("drag") elt.parentNode.classList.remove("drag") self.element.classList.remove('drag') if (dragElt) dragElt.remove() ol.ext.element.removeListener(document, 'mousemove touchmove', move) ol.ext.element.removeListener(document, 'mouseup touchend touchcancel', stop) } // Ordering function move(e) { // First drag (more than 2 px) => show drag element (ghost) pageY = e.pageY || (e.touches && e.touches.length && e.touches[0].pageY) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY) if (start && Math.abs(pageY0 - pageY) > 2) { start = false elt.classList.add("drag") layer = self._getLayerForLI(elt) target = false group = self._getLayerForLI(elt.parentNode.parentNode) // Ghost div dragElt = ol.ext.element.create('LI', { className: 'ol-dragover', html: elt.innerHTML, style: { position: "absolute", "z-index": 10000, left: elt.offsetLeft, opacity: 0.5, width: ol.ext.element.outerWidth(elt), height: ol.ext.element.getStyle(elt, 'height'), }, parent: panel }) self.element.classList.add('drag') self.dispatchEvent({ type: "reorder-start", layer: layer, group: group }) } // Start a new drag sequence if (!start) { e.preventDefault() e.stopPropagation() // Ghost div ol.ext.element.setStyle(dragElt, { top: pageY - ol.ext.element.offsetRect(panel).top + panel.scrollTop + 5 }) var li if (!e.touches) { li = e.target } else { li = document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY) } if (li.classList.contains("ol-switcherbottomdiv")) { self.overflow(-1) } else if (li.classList.contains("ol-switchertopdiv")) { self.overflow(1) } // Get associated li while (li && li.tagName !== 'LI') { li = li.parentNode } if (!li || !li.classList.contains('dropover')) { elt.parentNode.querySelectorAll('li').forEach(function (li) { li.classList.remove('dropover') li.classList.remove('dropover-after') li.classList.remove('dropover-before') }) } if (li && li.parentNode.classList.contains('drag') && li !== elt) { target = self._getLayerForLI(li) // Don't mix layer level if (target && !target.get('allwaysOnTop') == !layer.get('allwaysOnTop')) { li.classList.add("dropover") li.classList.add((elt.offsetTop < li.offsetTop) ? 'dropover-after' : 'dropover-before') } else { target = false } ol.ext.element.show(dragElt) } else { target = false if (li === elt) ol.ext.element.hide(dragElt) else ol.ext.element.show(dragElt) } if (!target) dragElt.classList.add("forbidden") else dragElt.classList.remove("forbidden") } } // Start ordering ol.ext.element.addListener(document, 'mousemove touchmove', move) ol.ext.element.addListener(document, 'mouseup touchend touchcancel', stop) } /** Change opacity on drag * @param {event} e drag event * @private */ dragOpacity_(e) { e.stopPropagation() e.preventDefault() var self = this // Register start params var elt = e.target var layer = this._getLayerForLI(elt.parentNode.parentNode.parentNode) if (!layer) return var x = e.pageX || (e.touches && e.touches.length && e.touches[0].pageX) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageX) var start = ol.ext.element.getStyle(elt, 'left') - x self.dragging_ = true // stop dragging function stop() { ol.ext.element.removeListener(document, "mouseup touchend touchcancel", stop) ol.ext.element.removeListener(document, "mousemove touchmove", move) self.dragging_ = false } // On draggin function move(e) { var x = e.pageX || (e.touches && e.touches.length && e.touches[0].pageX) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageX) var delta = (start + x) / ol.ext.element.getStyle(elt.parentNode, 'width') var opacity = Math.max(0, Math.min(1, delta)) ol.ext.element.setStyle(elt, { left: (opacity * 100) + "%" }) elt.parentNode.nextElementSibling.innerHTML = Math.round(opacity * 100) layer.setOpacity(opacity) } // Register move ol.ext.element.addListener(document, "mouseup touchend touchcancel", stop) ol.ext.element.addListener(document, "mousemove touchmove", move) } /** Render a list of layer * @param {Elemen} element to render * @layers {Array{ol.layer}} list of layer to show * @api stable * @private */ drawList(ul, collection) { var self = this var layers = collection.getArray() // Change layer visibility var setVisibility = function (e) { e.stopPropagation() e.preventDefault() var l = self._getLayerForLI(this.parentNode.parentNode) self.switchLayerVisibility(l, collection) if (self.get('selection') && l.getVisible()) { self.selectLayer(l) } if (self.onchangeCheck) { self.onchangeCheck(l) } } // Info button click function onInfo(e) { e.stopPropagation() e.preventDefault() var l = self._getLayerForLI(this.parentNode.parentNode) self.oninfo(l) self.dispatchEvent({ type: "info", layer: l }) } // Zoom to extent button function zoomExtent(e) { e.stopPropagation() e.preventDefault() var l = self._getLayerForLI(this.parentNode.parentNode) if (self.onextent) { self.onextent(l) } else { self.getMap().getView().fit(l.getExtent(), self.getMap().getSize()) } self.dispatchEvent({ type: "extent", layer: l }) } // Remove a layer on trash click function removeLayer(e) { e.stopPropagation() e.preventDefault() var li = this.parentNode.parentNode.parentNode.parentNode var layer, group = self._getLayerForLI(li) // Remove the layer from a group or from a map if (group) { layer = self._getLayerForLI(this.parentNode.parentNode) group.getLayers().remove(layer) if (group.getLayers().getLength() == 0 && !group.get('noSwitcherDelete')) { removeLayer.call(li.querySelectorAll('.layerTrash')[0], e) } } else { li = this.parentNode.parentNode self.getMap().removeLayer(self._getLayerForLI(li)) } } // Create a list for a layer function createLi(layer) { if (!this.displayInLayerSwitcher(layer)) { this._setLayerForLI(null, layer) return } var li = ol.ext.element.create('LI', { className: (layer.getVisible() ? "ol-visible " : " ") + (layer.get('baseLayer') ? "baselayer" : ""), parent: ul }) this._setLayerForLI(li, layer) if (this._selectedLayer === layer) { li.classList.add('ol-layer-select') } var layer_buttons = ol.ext.element.create('DIV', { className: 'ol-layerswitcher-buttons', parent: li }) // Content div var d = ol.ext.element.create('DIV', { className: 'li-content', parent: li }) // Visibility ol.ext.element.create('INPUT', { type: layer.get('baseLayer') ? 'radio' : 'checkbox', className: 'ol-visibility', checked: layer.getVisible(), click: setVisibility, parent: d }) // Label var label = ol.ext.element.create('LABEL', { title: layer.get('title') || layer.get('name'), click: setVisibility, style: { userSelect: 'none' }, parent: d }) label.addEventListener('selectstart', function () { return false }) ol.ext.element.create('SPAN', { html: layer.get('title') || layer.get('name'), click: function (e) { if (this.get('selection')) { e.stopPropagation() this.selectLayer(layer) } }.bind(this), parent: label }) // up/down if (this.reordering) { if ((i < layers.length - 1 && (layer.get("allwaysOnTop") || !layers[i + 1].get("allwaysOnTop"))) || (i > 0 && (!layer.get("allwaysOnTop") || layers[i - 1].get("allwaysOnTop")))) { ol.ext.element.create('DIV', { className: 'layerup ol-noscroll', title: this.tip.up, on: { 'mousedown touchstart': function (e) { self.dragOrdering_(e) } }, parent: layer_buttons }) } } // Show/hide sub layers if (layer.getLayers) { var nb = 0 layer.getLayers().forEach(function (l) { if (self.displayInLayerSwitcher(l)) nb++ }) if (nb) { ol.ext.element.create('DIV', { className: layer.get("openInLayerSwitcher") ? "collapse-layers" : "expend-layers", title: this.tip.plus, click: function () { var l = self._getLayerForLI(this.parentNode.parentNode) l.set("openInLayerSwitcher", !l.get("openInLayerSwitcher")) }, parent: layer_buttons }) } } // Info button if (this.oninfo) { ol.ext.element.create('DIV', { className: 'layerInfo', title: this.tip.info, click: onInfo, parent: layer_buttons }) } // Layer remove if (this.hastrash && !layer.get("noSwitcherDelete")) { ol.ext.element.create('DIV', { className: 'layerTrash', title: this.tip.trash, click: removeLayer, parent: layer_buttons }) } // Layer extent if (this.hasextent && layers[i].getExtent()) { var ex = layers[i].getExtent() if (ex.length == 4 && ex[0] < ex[2] && ex[1] < ex[3]) { ol.ext.element.create('DIV', { className: 'layerExtent', title: this.tip.extent, click: zoomExtent, parent: layer_buttons }) } } // Progress if (this.show_progress && layer instanceof ol.layer.Tile) { var p = ol.ext.element.create('DIV', { className: 'layerswitcher-progress', parent: d }) this.setprogress_(layer) layer.layerswitcher_progress = ol.ext.element.create('DIV', { parent: p }) } // Opacity var opacity = ol.ext.element.create('DIV', { className: 'layerswitcher-opacity', // Click on the opacity line click: function (e) { if (e.target !== this) return e.stopPropagation() e.preventDefault() var op = Math.max(0, Math.min(1, e.offsetX / ol.ext.element.getStyle(this, 'width'))) self._getLayerForLI(this.parentNode.parentNode).setOpacity(op) this.parentNode.querySelectorAll('.layerswitcher-opacity-label')[0].innerHTML = Math.round(op * 100) }, parent: d }) // Start dragging ol.ext.element.create('DIV', { className: 'layerswitcher-opacity-cursor ol-noscroll', style: { left: (layer.getOpacity() * 100) + "%" }, on: { 'mousedown touchstart': function (e) { self.dragOpacity_(e) } }, parent: opacity }) // Percent ol.ext.element.create('DIV', { className: 'layerswitcher-opacity-label', html: Math.round(layer.getOpacity() * 100), parent: d }) // Layer group if (layer.getLayers) { li.classList.add('ol-layer-group') if (layer.get("openInLayerSwitcher") === true) { var ul2 = ol.ext.element.create('UL', { parent: li }) this.drawList(ul2, layer.getLayers()) } } li.classList.add(this.getLayerClass(layer)) // Dispatch a dralist event to allow customisation this.dispatchEvent({ type: 'drawlist', layer: layer, li: li }) } // Add the layer list for (var i = layers.length - 1; i >= 0; i--) { createLi.call(this, layers[i]) } this.viewChange() if (ul === this.panel_) this.overflow() } /** Select a layer * @param {ol.layer.Layer} layer * @returns {string} the layer classname * @api */ getLayerClass(layer) { if (!layer) return 'none' if (layer.getLayers) return 'ol-layer-group' if (layer instanceof ol.layer.Vector) return 'ol-layer-vector' if (layer instanceof ol.layer.VectorTile) return 'ol-layer-vectortile' if (layer instanceof ol.layer.Tile) return 'ol-layer-tile' if (layer instanceof ol.layer.Image) return 'ol-layer-image' if (layer instanceof ol.layer.Heatmap) return 'ol-layer-heatmap' /* ol < 6 compatibility VectorImage is not defined */ // if (layer instanceof ol.layer.VectorImage) return 'ol-layer-vectorimage'; if (layer.getFeatures) return 'ol-layer-vectorimage' /* */ return 'unknown' } /** Select a layer * @param {ol.layer.Layer} layer * @api */ selectLayer(layer, silent) { if (!layer) { if (!this.getMap()) return layer = this.getMap().getLayers().item(this.getMap().getLayers().getLength() - 1) } this._selectedLayer = layer this.drawPanel() if (!silent) this.dispatchEvent({ type: 'select', layer: layer }) } /** Get selected layer * @returns {ol.layer.Layer} */ getSelection() { return this._selectedLayer } /** Handle progress bar for a layer * @private */ setprogress_(layer) { if (!layer.layerswitcher_progress) { var loaded = 0 var loading = 0 var draw = function () { if (loading === loaded) { loading = loaded = 0 ol.ext.element.setStyle(layer.layerswitcher_progress, { width: 0 }) // layer.layerswitcher_progress.width(0); } else { ol.ext.element.setStyle(layer.layerswitcher_progress, { width: (loaded / loading * 100).toFixed(1) + '%' }) // layer.layerswitcher_progress.css('width', (loaded / loading * 100).toFixed(1) + '%'); } } layer.getSource().on('tileloadstart', function () { loading++ draw() }) layer.getSource().on('tileloadend', function () { loaded++ draw() }) layer.getSource().on('tileloaderror', function () { loaded++ draw() }) } } } /** List of tips for internationalization purposes */ ol.control.LayerSwitcher.prototype.tip = { up: "up/down", down: "down", info: "informations...", extent: "zoom to extent", trash: "remove layer", plus: "expand/shrink" }; /* 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). */ /** Control bar for OL3 * The control bar is a container for other controls. It can be used to create toolbars. * Control bars can be nested and combined with ol.control.Toggle to handle activate/deactivate. * @class * @constructor * @fires control:active * @fires control:add * @extends ol.control.Control * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {boolean} options.group is a group, default false * @param {boolean} options.toggleOne only one toggle control is active at a time, default false * @param {boolean} options.autoDeactivate used with subbar to deactivate all control when top level control deactivate, default false * @param {Array } options.controls a list of control to add to the bar */ ol.control.Bar = class olcontrolBar extends ol.control.Control { constructor(options) { options = options || {}; var element = document.createElement('DIV'); element.classList.add('ol-unselectable', 'ol-control', 'ol-bar'); if (options.className) { var classes = options.className.split(' ').filter(function (className) { return className.length > 0; }); element.classList.add.apply(element.classList, classes); } if (options.group) element.classList.add('ol-group'); super({ element: element, target: options.target }); this.set('toggleOne', options.toggleOne); this.set('autoDeactivate', options.autoDeactivate); this.controls_ = []; if (options.controls instanceof Array) { for (var i = 0; i < options.controls.length; i++) { this.addControl(options.controls[i]); } } } /** Set the control visibility * @param {boolean} val */ setVisible(val) { if (val) this.element.style.display = ''; else this.element.style.display = 'none'; } /** Get the control visibility * @return {boolean} b */ getVisible() { return this.element.style.display != 'none'; } /** * 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); for (var i = 0; i < this.controls_.length; i++) { var c = this.controls_[i]; // map.addControl(c); c.setMap(map); } } /** Get controls in the panel * @param {Array} */ getControls() { return this.controls_; } /** Set tool bar position * @param {string} pos a combinaison of top|left|bottom|right separated with - */ setPosition(pos) { this.element.classList.remove('ol-left', 'ol-top', 'ol-bottom', 'ol-right'); pos = pos.split('-'); for (var i = 0; i < pos.length; i++) { switch (pos[i]) { case 'top': case 'left': case 'bottom': case 'right': this.element.classList.add("ol-" + pos[i]); break; default: break; } } } /** Add a control to the bar * @param {ol.control.Control} c control to add */ addControl(c) { this.controls_.push(c); c.setTarget(this.element); if (this.getMap()) { this.getMap().addControl(c); } // Activate and toogleOne if (c._activateBar) c.un('change:active', c._activateBar); c._activateBar = function (e) { this.onActivateControl_(e, c); }.bind(this); c.on('change:active', c._activateBar); if (c.getActive) { // c.dispatchEvent({ type:'change:active', key:'active', oldValue:false, active:true }); this.onActivateControl_({ target: c, active: c.getActive() }, c); } } /** Remove a control from the bar * @param {ol.control.Control} c control to remove */ removeControl(c) { var index = this.controls_.indexOf(c); if (index > -1) { this.controls_.splice(index, 1); if (this.getMap()) { this.getMap().removeControl(c); } // remove and toogleOne if (c._activateBar) c.un('change:active', c._activateBar); delete c._activateBar; } } /** Deativate all controls in a bar * @param {ol.control.Control} [except] a control */ deactivateControls(except) { for (var i = 0; i < this.controls_.length; i++) { if (this.controls_[i] !== except && this.controls_[i].setActive) { this.controls_[i].setActive(false); } } } /** Get active control in the bar * @returns {Array} */ getActiveControls() { var active = []; for (var i = 0, c; c = this.controls_[i]; i++) { if (c.getActive && c.getActive()) active.push(c); } return active; } /** Auto activate/deactivate controls in the bar * @param {boolean} b activate/deactivate */ setActive(b) { if (!b && this.get("autoDeactivate")) { this.deactivateControls(); } if (b) { var ctrls = this.getControls(); for (var i = 0, sb; (sb = ctrls[i]); i++) { if (sb.get("autoActivate")) sb.setActive(true); } } } /** Post-process an activated/deactivated control * @param {ol.event} e :an object with a target {_ol_control_} and active flag {bool} */ onActivateControl_(e, ctrl) { if (this.get('toggleOne')) { if (e.active) { var n; //var ctrl = e.target; for (n = 0; n < this.controls_.length; n++) { if (this.controls_[n] === ctrl) break; } // Not here! if (n == this.controls_.length) return; this.deactivateControls(this.controls_[n]); } else { // No one active > test auto activate if (!this.getActiveControls().length) { for (var i = 0, c; c = this.controls_[i]; i++) { if (c.get("autoActivate")) { c.setActive(true); break; } } } } } if (e.type) { this.dispatchEvent({ type: 'control:active', control: ctrl, active: e.active }) } else { this.dispatchEvent({ type: 'control:add', control: ctrl, active: e.active }) } } /** * @param {string} name of the control to search * @return {ol.control.Control} */ getControlsByName(name) { var controls = this.getControls(); return controls.filter( function (control) { return (control.get('name') === name); } ); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * @classdesc * OpenLayers 3 Attribution Control integrated in the canvas (for jpeg/png * @see http://www.kreidefossilien.de/webgis/dokumentation/beispiele/export-map-to-png-with-scale * * @constructor * @extends ol.control.Attribution * @param {Object=} options extend the ol.control.Attribution options. * @param {ol.style.Style} options.style option is usesd to draw the text. * @paream {boolean} [options.canvas=false] draw on canvas */ ol.control.CanvasAttribution = class olcontrolCanvasAttribution extends ol.control.Attribution { constructor(options) { options = options || {} super(options) this.element.classList.add('ol-canvas-control') // Draw in canvas this.setCanvas(!!options.canvas) // Get style options if (!options) options = {} if (!options.style) options.style = new ol.style.Style() this.setStyle(options.style) } /** * Draw attribution on canvas * @param {boolean} b draw the attribution on canvas. */ setCanvas(b) { this.isCanvas_ = b if (b) this.setCollapsed(false) this.element.style.visibility = b ? "hidden" : "visible" if (this.getMap()) { try { this.getMap().renderSync() } catch (e) { /* ok */ } } } /** * Change the control style * @param {ol.style.Style} style */ setStyle(style) { var text = style.getText() this.font_ = text ? text.getFont() : "10px sans-serif" var stroke = text ? text.getStroke() : null var fill = text ? text.getFill() : null this.fontStrokeStyle_ = stroke ? ol.color.asString(stroke.getColor()) : "#fff" this.fontFillStyle_ = fill ? ol.color.asString(fill.getColor()) : "#000" this.fontStrokeWidth_ = stroke ? stroke.getWidth() : 3 if (this.getMap()) this.getMap().render() } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { ol.control.CanvasBase.prototype.getCanvas.call(this, map) var oldmap = this.getMap() if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null super.setMap(map) if (oldmap) { try { oldmap.renderSync()} catch (e) { /* ok */ } } // Get change (new layer added or removed) if (map) { this._listener = map.on('postcompose', this.drawAttribution_.bind(this)) } this.setCanvas(this.isCanvas_) } /** * Draw attribution in the final canvas * @private */ drawAttribution_(e) { if (!this.isCanvas_) return var ctx = this.getContext(e) if (!ctx) return var text = "" Array.prototype.slice.call(this.element.querySelectorAll('li')) .filter(function (el) { return el.style.display !== "none" }) .map(function (el) { text += (text ? " - " : "") + el.textContent }) // Retina device var ratio = e.frameState.pixelRatio ctx.save() ctx.scale(ratio, ratio) // Position var eltRect = this.element.getBoundingClientRect() var mapRect = this.getMap().getViewport().getBoundingClientRect() var sc = this.getMap().getSize()[0] / mapRect.width ctx.translate((eltRect.left - mapRect.left) * sc, (eltRect.top - mapRect.top) * sc) var h = this.element.clientHeight var w = this.element.clientWidth var textAlign = ol.ext.element.getStyle(this.element, 'textAlign') || 'center' var left switch (textAlign) { case 'left': { left = 0 break } case 'right': { left = w break } default: { left = w / 2 break } } // Draw scale text ctx.beginPath() ctx.strokeStyle = this.fontStrokeStyle_ ctx.fillStyle = this.fontFillStyle_ ctx.lineWidth = this.fontStrokeWidth_ ctx.textAlign = textAlign ctx.textBaseline = 'middle' ctx.font = this.font_ ctx.lineJoin = 'round'; ctx.strokeText(text, left, h / 2) ctx.fillText(text, left, h / 2) ctx.closePath() ctx.restore() } /** Get map Canvas * @private */ getContext(e) { return ol.control.CanvasBase.prototype.getContext.call(this, e); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * @classdesc * OpenLayers Scale Line Control integrated in the canvas (for jpeg/png * @see http://www.kreidefossilien.de/webgis/dokumentation/beispiele/export-map-to-png-with-scale * * @constructor * @extends {ol.control.ScaleLine} * @param {Object=} options extend the ol.control.ScaleLine options. * @param {ol.style.Style} options.style used to draw the scale line (default is black/white, 10px Arial). */ ol.control.CanvasScaleLine = class olcontrolCanvasScaleLine extends ol.control.ScaleLine { constructor(options) { super(options) this.element.classList.add('ol-canvas-control') this.scaleHeight_ = 6 // Get style options if (!options) options = {} if (!options.style) options.style = new ol.style.Style() this.setStyle(options.style) } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { ol.control.CanvasBase.prototype.getCanvas.call(this, map) var oldmap = this.getMap() if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null super.setMap(map) if (oldmap) { try { oldmap.renderSync()} catch (e) { /* ok */ } } // Add postcompose on the map if (map) { this._listener = map.on('postcompose', this.drawScale_.bind(this)) } // Hide the default DOM element this.element.style.visibility = 'hidden' this.olscale = this.element.querySelector(".ol-scale-line-inner") } /** * Change the control style * @param {ol.style.Style} style */ setStyle(style) { var stroke = style.getStroke() this.strokeStyle_ = stroke ? ol.color.asString(stroke.getColor()) : "#000" this.strokeWidth_ = stroke ? stroke.getWidth() : 2 var fill = style.getFill() this.fillStyle_ = fill ? ol.color.asString(fill.getColor()) : "#fff" var text = style.getText() this.font_ = text ? text.getFont() : "10px Arial" stroke = text ? text.getStroke() : null fill = text ? text.getFill() : null this.fontStrokeStyle_ = stroke ? ol.color.asString(stroke.getColor()) : this.fillStyle_ this.fontStrokeWidth_ = stroke ? stroke.getWidth() : 3 this.fontFillStyle_ = fill ? ol.color.asString(fill.getColor()) : this.strokeStyle_ // refresh if (this.getMap()) this.getMap().render() } /** * Draw attribution in the final canvas * @param {ol.render.Event} e * @private */ drawScale_(e) { if (this.element.style.visibility !== 'hidden' || ol.ext.element.getStyle(this.element, 'display') === 'none') return var ctx = this.getContext(e) if (!ctx) return // Get size of the scale div var scalewidth = parseInt(this.olscale.style.width) if (!scalewidth) return var text = this.olscale.textContent var position = { left: this.element.offsetLeft, top: this.element.offsetTop } // Retina device var ratio = e.frameState.pixelRatio ctx.save() ctx.scale(ratio, ratio) // On top position.top += this.element.clientHeight - this.scaleHeight_ // Draw scale text ctx.beginPath() ctx.strokeStyle = this.fontStrokeStyle_ ctx.fillStyle = this.fontFillStyle_ ctx.lineWidth = this.fontStrokeWidth_ ctx.textAlign = 'center' ctx.textBaseline = 'bottom' ctx.font = this.font_ ctx.lineJoin = 'round'; ctx.strokeText(text, position.left + scalewidth / 2, position.top) ctx.fillText(text, position.left + scalewidth / 2, position.top) ctx.closePath() // Draw scale bar position.top += 2 ctx.lineWidth = this.strokeWidth_ ctx.strokeStyle = this.strokeStyle_ var max = 4 var n = parseInt(text) while (n % 10 === 0) n /= 10 if (n % 5 === 0) max = 5 for (var i = 0; i < max; i++) { ctx.beginPath() ctx.fillStyle = i % 2 ? this.fillStyle_ : this.strokeStyle_ ctx.rect(position.left + i * scalewidth / max, position.top, scalewidth / max, this.scaleHeight_) ctx.stroke() ctx.fill() ctx.closePath() } ctx.restore() } /** Get map Canvas * @private */ getContext(e) { return ol.control.CanvasBase.prototype.getContext.call(this, e); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * A title Control integrated in the canvas (for jpeg/png * * @constructor * @extends {ol.control.CanvasBase} * @param {Object=} options extend the ol.control options. * @param {string} [options.title] the title, default 'Title' * @param {boolean} [options.visible=true] * @param {ol.style.Style} [options.style] style used to draw the title (with a text). */ ol.control.CanvasTitle = class olcontrolCanvasTitle extends ol.control.CanvasBase { constructor(options) { options = options || {}; var elt = ol.ext.element.create('DIV', { className: (options.className || '') + ' ol-control-title ol-unselectable', style: { display: 'block', visibility: 'hidden' } }); super({ element: elt, style: options.style }); this.setTitle(options.title || ''); this.setVisible(options.visible !== false); this.element.style.font = this.getTextFont(); } /** * Change the control style * @param {ol.style.Style} style */ setStyle(style) { super.setStyle(style); // Element style if (this.element) { this.element.style.font = this.getTextFont(); } // refresh if (this.getMap()) this.getMap().render(); } /** * Set the map title * @param {string} map title. * @api stable */ setTitle(title) { this.element.textContent = title; this.set('title', title); if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** * Get the map title * @param {string} map title. * @api stable */ getTitle() { return this.get('title'); } /** * Set control visibility * @param {bool} b * @api stable */ setVisible(b) { this.element.style.display = (b ? 'block' : 'none'); if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** * Get control visibility * @return {bool} * @api stable */ getVisible() { return this.element.style.display !== 'none'; } /** Draw title in the final canvas * @private */ _draw(e) { if (!this.getVisible()) return; var ctx = this.getContext(e); if (!ctx) return; // Retina device var ratio = e.frameState.pixelRatio; ctx.save(); ctx.scale(ratio, ratio); // Position var eltRect = this.element.getBoundingClientRect(); var mapRect = this.getMap().getViewport().getBoundingClientRect(); var sc = this.getMap().getSize()[0] / mapRect.width; ctx.translate( Math.round((eltRect.left - mapRect.left) * sc), Math.round((eltRect.top - mapRect.top) * sc) ); var h = this.element.clientHeight; var w = this.element.clientWidth; var left = w / 2; ctx.beginPath(); ctx.fillStyle = ol.color.asString(this.getFill().getColor()); ctx.rect(0, 0, w, h); ctx.fill(); ctx.closePath(); ctx.beginPath(); ctx.fillStyle = ol.color.asString(this.getTextFill().getColor()); ctx.strokeStyle = ol.color.asString(this.getTextStroke().getColor()); ctx.lineWidth = this.getTextStroke().getWidth(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = this.getTextFont(); ctx.lineJoin = 'round'; if (ctx.lineWidth) ctx.strokeText(this.getTitle(), left, h / 2); ctx.fillText(this.getTitle(), left, h / 2); ctx.closePath(); ctx.restore(); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * A Control to display map center coordinates on the canvas. * * @constructor * @extends {ol.control.CanvasBase} * @param {Object=} options extend the ol.control options. * @param {string} options.className CSS class name * @param {ol.style.Style} options.style style used to draw in the canvas * @param {ol.proj.ProjectionLike} options.projection Projection. Default is the view projection. * @param {ol.coordinate.CoordinateFormat} options.coordinateFormat A function that takes a ol.Coordinate and transforms it into a string. * @param {boolean} options.canvas true to draw in the canvas */ ol.control.CenterPosition = class olcontrolCenterPosition extends ol.control.CanvasBase { constructor(options) { options = options || {} var elt = ol.ext.element.create('DIV', { className: (options.className || '') + ' ol-center-position ol-unselectable', style: { display: 'block', visibility: 'hidden' } }) super({ element: elt, style: options.style }) this.element.style.font = this.getTextFont() this.set('projection', options.projection) this.setCanvas(options.canvas) this._format = (typeof options.coordinateFormat === 'function') ? options.coordinateFormat : ol.coordinate.toStringXY } /** * Change the control style * @param {ol.style.Style} style */ setStyle(style) { super.setStyle(style) // Element style if (this.element) { this.element.style.font = this.getTextFont() } // refresh if (this.getMap()) this.getMap().render() } /** * Draw on canvas * @param {boolean} b draw the attribution on canvas. */ setCanvas(b) { this.set('canvas', b) this.element.style.visibility = b ? "hidden" : "visible" if (this.getMap()) { try { this.getMap().renderSync()} catch (e) { /* ok */ } } } /** * Set control visibility * @param {bool} b * @api stable */ setVisible(b) { this.element.style.display = (b ? '' : 'none') if (this.getMap()) { try { this.getMap().renderSync()} catch (e) { /* ok */ } } } /** * Get control visibility * @return {bool} * @api stable */ getVisible() { return this.element.style.display !== 'none' } /** Draw position in the final canvas * @private */ _draw(e) { if (!this.getVisible() || !this.getMap()) return // Coordinate var coord = this.getMap().getView().getCenter() if (this.get('projection')) coord = ol.proj.transform(coord, this.getMap().getView().getProjection(), this.get('projection')) coord = this._format(coord) this.element.textContent = coord if (!this.get('canvas')) return var ctx = this.getContext(e) if (!ctx) return // Retina device var ratio = e.frameState.pixelRatio ctx.save() ctx.scale(ratio, ratio) // Position var eltRect = this.element.getBoundingClientRect() var mapRect = this.getMap().getViewport().getBoundingClientRect() var sc = this.getMap().getSize()[0] / mapRect.width ctx.translate((eltRect.left - mapRect.left) * sc, (eltRect.top - mapRect.top) * sc) var h = this.element.clientHeight var w = this.element.clientWidth ctx.beginPath() ctx.fillStyle = ol.color.asString(this.getTextFill().getColor()) ctx.strokeStyle = ol.color.asString(this.getTextStroke().getColor()) ctx.lineWidth = this.getTextStroke().getWidth() ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.font = this.getTextFont() ctx.lineJoin = 'round'; if (ctx.lineWidth) ctx.strokeText(coord, w / 2, h / 2) ctx.fillText(coord, w / 2, h / 2) ctx.closePath() ctx.restore() } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Draw a compass on the map. The position/size of the control is defined in the css. * * @constructor * @extends {ol.control.CanvasBase} * @param {Object=} options Control options. The style {_ol_style_Stroke_} option is usesd to draw the text. * @param {string} options.className class name for the control * @param {boolean} [options.visible=true] * @param {Image} options.image an image, default use the src option or a default image * @param {string} options.src image src or 'default' or 'compact', default use the image option or a default image * @param {boolean} options.rotateVithView rotate vith view (false to show watermark), default true * @param {ol.style.Stroke} options.style style to draw the lines, default draw no lines */ ol.control.Compass = class olcontrolCompass extends ol.control.CanvasBase { constructor(options) { options = options || {}; // Initialize parent var elt = document.createElement("div"); elt.className = "ol-control ol-compassctrl ol-unselectable ol-hidden" + (options.className ? " " + options.className : ""); elt.style.position = "absolute"; elt.style.visibility = "hidden"; var style = (options.style instanceof ol.style.Stroke) ? new ol.style.Style({ stroke: options.style }) : options.style; if (!options.style) { style = new ol.style.Style({ stroke: new ol.style.Stroke({ width: 0 }) }); } super({ element: elt, style: style }); this.set('rotateVithView', options.rotateWithView !== false); this.setVisible(options.visible !== false); this.setImage(options.image || options.src); } /** Set compass image * @param {Image|string} [img=default] the image or an url or 'compact' or 'default' */ setImage(img) { // The image if (img instanceof Image) { this.img_ = img; this.img_.onload = function () { if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } }.bind(this); } else if (typeof (img) === 'string') { // Load source switch (img) { case 'compact': { this.img_ = this.compactCompass_(this.element.clientWidth, this.getStroke().getColor()); break; } case 'default': { this.img_ = this.defaultCompass_(this.element.clientWidth, this.getStroke().getColor()); break; } default: { this.img_ = new Image(); this.img_.onload = function () { if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } }.bind(this); this.img_.src = img; break; } } } else { this.img_ = this.defaultCompass_(this.element.clientWidth, this.getStroke().getColor()); } } /** Create a default image. * @param {number} s the size of the compass * @private */ compactCompass_(s, color) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext("2d"); s = canvas.width = canvas.height = s || 150; var r = s / 2; ctx.translate(r, r); ctx.fillStyle = color || '#963'; ctx.lineWidth = 5; ctx.lineJoin = ctx.lineCap = 'round'; ctx.font = 'bold ' + (r * 0.4) + 'px sans-serif'; ctx.textBaseline = 'bottom'; ctx.textAlign = 'center'; ctx.strokeStyle = '#fff'; ctx.globalAlpha = .75; ctx.strokeText('N', 0, -r / 2); ctx.globalAlpha = 1; ctx.fillText('N', 0, -r / 2); ctx.beginPath(); ctx.moveTo(0, r / 4); ctx.lineTo(r / 3, r / 2); ctx.lineTo(0, -r / 2); ctx.lineTo(-r / 3, r / 2); ctx.lineTo(0, r / 4); ctx.lineWidth = 12; ctx.fillStyle = "#fff"; ctx.globalAlpha = .75; ctx.fill(); ctx.stroke(); ctx.globalAlpha = 1; ctx.fillStyle = ctx.strokeStyle = color || '#963'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(0, r / 4); ctx.lineTo(0, -r / 2); ctx.lineTo(r / 3, r / 2); ctx.lineTo(0, r / 4); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, r / 4); ctx.lineTo(0, -r / 2); ctx.lineTo(-r / 3, r / 2); ctx.lineTo(0, r / 4); ctx.stroke(); return canvas; } /** Create a default image. * @param {number} s the size of the compass * @private */ defaultCompass_(s, color) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext("2d"); s = canvas.width = canvas.height = s || 150; var r = s / 2; var r2 = 0.22 * r; function draw(r, r2) { ctx.fillStyle = color || "#963"; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(r, 0); ctx.lineTo(r2, r2); ctx.moveTo(0, 0); ctx.lineTo(-r, 0); ctx.lineTo(-r2, -r2); ctx.moveTo(0, 0); ctx.lineTo(0, r); ctx.lineTo(-r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, -r); ctx.lineTo(r2, -r2); ctx.moveTo(0, 0); ctx.fill(); ctx.stroke(); } function draw2(r, r2) { ctx.globalCompositeOperation = "destination-out"; ctx.fillStyle = "#fff"; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(r, 0); ctx.lineTo(r2, -r2); ctx.moveTo(0, 0); ctx.lineTo(-r, 0); ctx.lineTo(-r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, r); ctx.lineTo(r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, -r); ctx.lineTo(-r2, -r2); ctx.moveTo(0, 0); ctx.fill(); ctx.globalCompositeOperation = "source-over"; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(r, 0); ctx.lineTo(r2, -r2); ctx.moveTo(0, 0); ctx.lineTo(-r, 0); ctx.lineTo(-r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, r); ctx.lineTo(r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, -r); ctx.lineTo(-r2, -r2); ctx.moveTo(0, 0); ctx.stroke(); } ctx.translate(r, r); ctx.strokeStyle = color || "#963"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(0, 0, s * 0.41, 0, 2 * Math.PI); ctx.arc(0, 0, s * 0.44, 0, 2 * Math.PI); ctx.stroke(); ctx.rotate(Math.PI / 4); draw(r * 0.9, r2 * 0.8); draw2(r * 0.9, r2 * 0.8); ctx.rotate(-Math.PI / 4); draw(r, r2); draw2(r, r2); return canvas; } /** Get control visibility * @return {boolean} */ getVisible() { return ol.ext.element.getStyle(this.element, 'display') === 'block'; } /** Set visibility * @param {boolean} b */ setVisible(b) { if (b) this.element.classList.add('ol-visible'); else this.element.classList.remove('ol-visible'); if (this.getMap()) this.getMap().render(); } /** Draw compass * @param {ol.event} e postcompose event * @private */ _draw(e) { var ctx = this.getContext(e); if (!ctx || !this.getVisible()) return; var canvas = ctx.canvas; // 8 angles var i, da = []; for (i = 0; i < 8; i++) da[i] = [Math.cos(Math.PI * i / 8), Math.sin(Math.PI * i / 8)]; // Retina device var ratio = e.frameState.pixelRatio; ctx.save(); ctx.scale(ratio, ratio); var w = this.element.clientWidth; var h = this.element.clientHeight; var pos = { left: this.element.offsetLeft, top: this.element.offsetTop }; var compass = this.img_; var rot = e.frameState.viewState.rotation; ctx.beginPath(); ctx.translate(pos.left + w / 2, pos.top + h / 2); if (this.get('rotateVithView')) ctx.rotate(rot); if (this.getStroke().getWidth()) { ctx.beginPath(); ctx.strokeStyle = this.getStroke().getColor(); ctx.lineWidth = this.getStroke().getWidth(); var m = Math.max(canvas.width, canvas.height); for (i = 0; i < 8; i++) { ctx.moveTo(-da[i][0] * m, -da[i][1] * m); ctx.lineTo(da[i][0] * m, da[i][1] * m); } ctx.stroke(); } if (compass.width) { ctx.drawImage(compass, -w / 2, -h / 2, w, h); } ctx.closePath(); ctx.restore(); } } /** * @classdesc * Application dialog * @extends {ol.control.Control} * @constructor * @param {*} options * @param {string} options.className * @param {ol.Map} options.map the map to place the dialog inside * @param {Element} options.target target to place the dialog * @param {boolean} options.fullscreen view dialog fullscreen (same as options.target = document.body) * @param {boolean} options.zoom add a zoom effect * @param {boolean} options.closeBox add a close button * @param {number} options.max if not null add a progress bar to the dialog, default null * @param {boolean} options.hideOnClick close dialog when click * @param {boolean} options.hideOnBack close dialog when click the background * @param {boolean} options.closeOnSubmit Prevent closing the dialog on submit */ ol.control.Dialog = class olcontrolDialog extends ol.control.Control { constructor(options) { options = options || {}; if (options.fullscreen) options.target = document.body; var element = ol.ext.element.create('DIV', { className: ((options.className || '') + (options.zoom ? ' ol-zoom' : '') + ' ol-ext-dialog').trim() }) super({ element: element, target: options.target }); // Constructor element.addEventListener('click', function (e) { if (this.get('hideOnBack') && e.target === element) this.close(); if (this.get('hideOnClick')) this.close(); }.bind(this)); // form var form = ol.ext.element.create('FORM', { on: { submit: this._onButton('submit') }, parent: element }); // Title ol.ext.element.create('H2', { parent: form }); // Close box ol.ext.element.create('DIV', { className: 'ol-closebox', click: this._onButton('cancel'), parent: form }); // Content ol.ext.element.create('DIV', { className: 'ol-content', parent: form }); // Progress this._progress = ol.ext.element.create('DIV', { style: { display: 'none' }, parent: form }); var bar = ol.ext.element.create('DIV', { className: 'ol-progress-bar', parent: this._progress }); this._progressbar = ol.ext.element.create('DIV', { parent: bar }); this._progressMessage = ol.ext.element.create('DIV', { className: 'ol-progress-message', parent: this._progress }); // Buttons ol.ext.element.create('DIV', { className: 'ol-buttons', parent: form }); this.set('closeBox', options.closeBox !== false); this.set('zoom', !!options.zoom); this.set('hideOnClick', !!options.hideOnClick); this.set('hideOnBack', !!options.hideOnBack); this.set('className', element.className); this.set('closeOnSubmit', options.closeOnSubmit); this.set('buttons', options.buttons); this.setContent(options); } /** Show a new dialog * @param { * | Element | string } options options or a content to show * @param {Element | String} options.content dialog content * @param {string} options.title title of the dialog * @param {string} options.className dialog class name * @param {number} options.autoclose a delay in ms before auto close * @param {boolean} options.hideOnBack close dialog when click the background * @param {number} options.max if not null add a progress bar to the dialog * @param {number} options.progress set the progress bar value * @param {Object} options.buttons a key/value list of button to show * @param {function} [options.onButton] a function that takes the button id and a list of input by className */ show(options) { if (options) { if (options instanceof Element || typeof (options) === 'string') { options = { content: options }; } this.setContent(options); } this.element.classList.add('ol-visible'); var input = this.element.querySelector('input[type="text"],input[type="search"],input[type="number"]'); if (input) input.focus(); this.dispatchEvent({ type: 'show' }); if (options) { // Auto close if (options.autoclose) { var listener = setTimeout(function () { this.hide(); }.bind(this), options.autoclose); this.once('hide', function () { clearTimeout(listener); }); } // hideOnBack if (options.hideOnBack) { // save value var value = this.get('hideOnBack'); this.set('hideOnBack', true); this.once('hide', function () { this.set('hideOnBack', value); }.bind(this)); } } } /** Open the dialog */ open() { this.show(); } /** Set the dialog content * @param {Element | String} content dialog content */ setContentMessage(content) { if (content !== undefined) { var elt = this.getContentElement(); if (content instanceof Element) ol.ext.element.setHTML(elt, ''); ol.ext.element.setHTML(elt, content || ''); } } /** Set the dialog title * @param {Element | String} content dialog content */ setTitle(title) { var form = this.element.querySelector('form'); form.querySelector('h2').innerText = title || ''; if (title) { form.classList.add('ol-title'); } else { form.classList.remove('ol-title'); } } /** Set the dialog content * @param {*} options * @param {Element | String} options.content dialog content * @param {string} options.title title of the dialog * @param {string} options.className dialog class name * @param {number} options.max if not null add a progress bar to the dialog * @param {number} options.progress set the progress bar value * @param {Object} options.buttons a key/value list of button to show * @param {function} [options.onButton] a function that takes the button id and a list of input by className */ setContent(options) { if (!options) return; this.element.className = this.get('className'); if (typeof (options) === 'string') options = { content: options }; options = options || {}; this.setProgress(false); if (options.max) this.setProgress(0, options.max); if (options.progress !== undefined) this.setProgress(options.progress); //this.element.className = 'ol-ext-dialog' + (this.get('zoom') ? ' ol-zoom' : ''); if (this.get('zoom')) this.element.classList.add('ol-zoom'); else this.element.classList.remove('ol-zoom'); if (options.className) { options.className.split(' ').forEach(function (c) { this.element.classList.add(c); }.bind(this)); } var form = this.element.querySelector('form'); // Content if (options.content !== undefined) { if (options.content instanceof Element) ol.ext.element.setHTML(form.querySelector('.ol-content'), ''); ol.ext.element.setHTML(form.querySelector('.ol-content'), options.content || ''); } // Title this.setTitle(options.title); // Closebox if (options.closeBox || (this.get('closeBox') && options.closeBox !== false)) { form.classList.add('ol-closebox'); } else { form.classList.remove('ol-closebox'); } // Buttons var buttons = this.element.querySelector('.ol-buttons'); buttons.innerHTML = ''; var btn = options.buttons || this.get('buttons'); if (btn) { form.classList.add('ol-button'); for (var i in btn) { ol.ext.element.create('INPUT', { type: (i === 'submit' ? 'submit' : 'button'), value: btn[i], click: this._onButton(i, options.onButton), parent: buttons }); } } else { form.classList.remove('ol-button'); } } /** Get dialog content element * @returns {Element} */ getContentElement() { return this.element.querySelector('form .ol-content'); } /** Set progress * @param {number|boolean} val the progress value or false to hide the progressBar * @param {number} max * @param {string|element} message */ setProgress(val, max, message) { if (val === false) { ol.ext.element.setStyle(this._progress, { display: 'none' }); return; } if (max > 0) { this.set('max', Number(max)); } else { max = this.get('max'); } if (!max) { ol.ext.element.setStyle(this._progress, { display: 'none' }); } else { var p = Math.round(val / max * 100); ol.ext.element.setStyle(this._progress, { display: '' }); this._progressbar.className = p ? '' : 'notransition'; ol.ext.element.setStyle(this._progressbar, { width: p + '%' }); } this._progressMessage.innerHTML = ''; ol.ext.element.setHTML(this._progressMessage, message || ''); } /** Returns a function to do something on button click * @param {strnig} button button id * @param {function} callback * @returns {function} * @private */ _onButton(button, callback) { // Dispatch a button event var fn = function (e) { e.preventDefault(); if (button !== 'submit' || this.get('closeOnSubmit') !== false) this.hide(); var inputs = this.getInputs(); this.dispatchEvent({ type: 'button', button: button, inputs: inputs }); if (typeof (callback) === 'function') callback(button, inputs); }.bind(this); return fn; } /** Get inputs, textarea an select of the dialog by classname * @return {Object} a {key:value} list of Elements by classname */ getInputs() { var inputs = {}; ['input', 'textarea', 'select'].forEach(function (type) { this.element.querySelectorAll('form ' + type).forEach(function (input) { if (input.className) { input.className.split(' ').forEach(function (n) { inputs[n] = input; }); } }); }.bind(this)); return inputs; } /** Close the dialog */ hide() { this.element.classList.remove('ol-visible'); this.dispatchEvent({ type: 'hide' }); } /** Close the dialog */ close() { this.hide(); } /** The dialog is shown * @return {bool} true if a dialog is open */ isOpen() { return (this.element.classList.contains('ol-visible')); } } /** A simple control to disable all actions on the map. * The control will create an invisible div over the map. * @constructor * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {String} options.class class of the control * @param {String} options.html html code to insert in the control * @param {bool} options.on the control is on * @param {function} options.toggleFn callback when control is clicked */ ol.control.Disable = class olcontrolDisable extends ol.control.Control { constructor(options) { options = options || {}; var element = document.createElement('div'); element.className = (options.className || '' + ' ol-disable ol-unselectable ol-control').trim(); var stylesOptions = { top: "0px", left: "0px", right: "0px", bottom: "0px", "zIndex": 10000, background: "none", display: "none" }; Object.keys(stylesOptions).forEach(function (styleKey) { element.style[styleKey] = stylesOptions[styleKey]; }); super({ element: element }); } /** Test if the control is on * @return {bool} * @api stable */ isOn() { return this.element.classList.contains('ol-disable'); } /** Disable all action on the map * @param {bool} b, default false * @api stable */ disableMap(b) { if (b) { this.element.classList.add('ol-enable') this.element.style.display = 'block' } else { this.element.classList.remove('ol-enable') this.element.style.display = 'none' } } } /** Control bar for editing in a layer * @constructor * @extends {ol.control.Bar} * @fires info * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {String} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {boolean} options.edition false to remove the edition tools, default true * @param {Object} options.interactions List of interactions to add to the bar * ie. Select, Delete, Info, DrawPoint, DrawLine, DrawPolygon * Each interaction can be an interaction or true (to get the default one) or false to remove it from bar * @param {ol.source.Vector} options.source Source for the drawn features. */ ol.control.EditBar = class olcontrolEditBar extends ol.control.Bar { constructor(options) { options = options || {} options.interactions = options.interactions || {} // New bar super({ className: (options.className ? options.className + ' ' : '') + 'ol-editbar', toggleOne: true, target: options.target }) this._source = options.source // Add buttons / interaction this._interactions = {} this._setSelectInteraction(options) if (options.edition !== false) this._setEditInteraction(options) this._setModifyInteraction(options) } /** * 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) { if (this.getMap()) { if (this._interactions.Delete) this.getMap().removeInteraction(this._interactions.Delete) if (this._interactions.ModifySelect) this.getMap().removeInteraction(this._interactions.ModifySelect) } super.setMap(map) if (this.getMap()) { if (this._interactions.Delete) this.getMap().addInteraction(this._interactions.Delete) if (this._interactions.ModifySelect) this.getMap().addInteraction(this._interactions.ModifySelect) } } /** Get an interaction associated with the bar * @param {string} name */ getInteraction(name) { return this._interactions[name] } /** Get the option title */ _getTitle(option) { if (option) { if (option.get) return option.get('title') else if (typeof (option) === 'string') return option else return option.title } } /** Add selection tool: * 1. a toggle control with a select interaction * 2. an option bar to delete / get information on the selected feature * @private */ _setSelectInteraction(options) { var self = this // Sub bar var sbar = new ol.control.Bar() var selectCtrl // Delete button if (options.interactions.Delete !== false) { if (options.interactions.Delete instanceof ol.interaction.Delete) { this._interactions.Delete = options.interactions.Delete } else { this._interactions.Delete = new ol.interaction.Delete() } var del = this._interactions.Delete del.setActive(false) if (this.getMap()) this.getMap().addInteraction(del) sbar.addControl(new ol.control.Button({ className: 'ol-delete', title: this._getTitle(options.interactions.Delete) || "Delete", name: 'Delete', handleClick: function (e) { // Delete selection del.delete(selectCtrl.getInteraction().getFeatures()) var evt = { type: 'select', selected: [], deselected: selectCtrl.getInteraction().getFeatures().getArray().slice(), mapBrowserEvent: e.mapBrowserEvent } selectCtrl.getInteraction().getFeatures().clear() selectCtrl.getInteraction().dispatchEvent(evt) } })) } // Info button if (options.interactions.Info !== false) { sbar.addControl(new ol.control.Button({ className: 'ol-info', name: 'Info', title: this._getTitle(options.interactions.Info) || "Show informations", handleClick: function () { self.dispatchEvent({ type: 'info', features: selectCtrl.getInteraction().getFeatures() }) } })) } // Select button if (options.interactions.Select !== false) { if (options.interactions.Select instanceof ol.interaction.Select) { this._interactions.Select = options.interactions.Select } else { this._interactions.Select = new ol.interaction.Select({ condition: ol.events.condition.click }) } var sel = this._interactions.Select selectCtrl = new ol.control.Toggle({ className: 'ol-selection', name: 'Select', title: this._getTitle(options.interactions.Select) || "Select", interaction: sel, bar: sbar.getControls().length ? sbar : undefined, autoActivate: true, active: true }) this.addControl(selectCtrl) sel.on('change:active', function () { if (!sel.getActive()) sel.getFeatures().clear() }) } } /** Add editing tools * @private */ _setEditInteraction(options) { if (options.interactions.DrawPoint !== false) { if (options.interactions.DrawPoint instanceof ol.interaction.Draw) { this._interactions.DrawPoint = options.interactions.DrawPoint } else { this._interactions.DrawPoint = new ol.interaction.Draw({ type: 'Point', source: this._source }) } var pedit = new ol.control.Toggle({ className: 'ol-drawpoint', name: 'DrawPoint', title: this._getTitle(options.interactions.DrawPoint) || 'Point', interaction: this._interactions.DrawPoint }) this.addControl(pedit) } if (options.interactions.DrawLine !== false) { if (options.interactions.DrawLine instanceof ol.interaction.Draw) { this._interactions.DrawLine = options.interactions.DrawLine } else { this._interactions.DrawLine = new ol.interaction.Draw({ type: 'LineString', source: this._source, // Count inserted points geometryFunction: function (coordinates, geometry) { if (geometry) geometry.setCoordinates(coordinates) else geometry = new ol.geom.LineString(coordinates) this.nbpts = geometry.getCoordinates().length return geometry } }) } var ledit = new ol.control.Toggle({ className: 'ol-drawline', title: this._getTitle(options.interactions.DrawLine) || 'LineString', name: 'DrawLine', interaction: this._interactions.DrawLine, // Options bar associated with the control bar: new ol.control.Bar({ controls: [ new ol.control.TextButton({ html: this._getTitle(options.interactions.UndoDraw) || 'undo', title: this._getTitle(options.interactions.UndoDraw) || "delete last point", handleClick: function () { if (ledit.getInteraction().nbpts > 1) ledit.getInteraction().removeLastPoint() } }), new ol.control.TextButton({ html: this._getTitle(options.interactions.FinishDraw) || 'finish', title: this._getTitle(options.interactions.FinishDraw) || "finish", handleClick: function () { // Prevent null objects on finishDrawing if (ledit.getInteraction().nbpts > 2) ledit.getInteraction().finishDrawing() } }) ] }) }) this.addControl(ledit) } if (options.interactions.DrawPolygon !== false) { if (options.interactions.DrawPolygon instanceof ol.interaction.Draw) { this._interactions.DrawPolygon = options.interactions.DrawPolygon } else { this._interactions.DrawPolygon = new ol.interaction.Draw({ type: 'Polygon', source: this._source, // Count inserted points geometryFunction: function (coordinates, geometry) { this.nbpts = coordinates[0].length if (geometry) geometry.setCoordinates([coordinates[0].concat([coordinates[0][0]])]) else geometry = new ol.geom.Polygon(coordinates) return geometry } }) } this._setDrawPolygon( 'ol-drawpolygon', this._interactions.DrawPolygon, this._getTitle(options.interactions.DrawPolygon) || 'Polygon', 'DrawPolygon', options ) } // Draw hole if (options.interactions.DrawHole !== false) { if (options.interactions.DrawHole instanceof ol.interaction.DrawHole) { this._interactions.DrawHole = options.interactions.DrawHole } else { this._interactions.DrawHole = new ol.interaction.DrawHole() } this._setDrawPolygon( 'ol-drawhole', this._interactions.DrawHole, this._getTitle(options.interactions.DrawHole) || 'Hole', 'DrawHole', options ) } // Draw regular if (options.interactions.DrawRegular !== false) { var label = { pts: 'pts', circle: 'circle' } if (options.interactions.DrawRegular instanceof ol.interaction.DrawRegular) { this._interactions.DrawRegular = options.interactions.DrawRegular label.pts = this._interactions.DrawRegular.get('ptsLabel') || label.pts label.circle = this._interactions.DrawRegular.get('circleLabel') || label.circle } else { this._interactions.DrawRegular = new ol.interaction.DrawRegular({ source: this._source, sides: 4 }) if (options.interactions.DrawRegular) { label.pts = options.interactions.DrawRegular.ptsLabel || label.pts label.circle = options.interactions.DrawRegular.circleLabel || label.circle } } var regular = this._interactions.DrawRegular var div = document.createElement('DIV') var down = ol.ext.element.create('DIV', { parent: div }) ol.ext.element.addListener(down, ['click', 'touchstart'], function () { var sides = regular.getSides() - 1 if (sides < 2) sides = 2 regular.setSides(sides) text.textContent = sides > 2 ? sides + ' ' + label.pts : label.circle }.bind(this)) var text = ol.ext.element.create('TEXT', { html: '4 ' + label.pts, parent: div }) var up = ol.ext.element.create('DIV', { parent: div }) ol.ext.element.addListener(up, ['click', 'touchstart'], function () { var sides = regular.getSides() + 1 if (sides < 3) sides = 3 regular.setSides(sides) text.textContent = sides + ' ' + label.pts }.bind(this)) var ctrl = new ol.control.Toggle({ className: 'ol-drawregular', title: this._getTitle(options.interactions.DrawRegular) || 'Regular', name: 'DrawRegular', interaction: this._interactions.DrawRegular, // Options bar associated with the control bar: new ol.control.Bar({ controls: [ new ol.control.TextButton({ html: div }) ] }) }) this.addControl(ctrl) } } /** * @private */ _setDrawPolygon(className, interaction, title, name, options) { var fedit = new ol.control.Toggle({ className: className, name: name, title: title, interaction: interaction, // Options bar associated with the control bar: new ol.control.Bar({ controls: [ new ol.control.TextButton({ html: this._getTitle(options.interactions.UndoDraw) || 'undo', title: this._getTitle(options.interactions.UndoDraw) || 'undo last point', handleClick: function () { if (fedit.getInteraction().nbpts > 1) fedit.getInteraction().removeLastPoint() } }), new ol.control.TextButton({ html: this._getTitle(options.interactions.FinishDraw) || 'finish', title: this._getTitle(options.interactions.FinishDraw) || 'finish', handleClick: function () { // Prevent null objects on finishDrawing if (fedit.getInteraction().nbpts > 3) fedit.getInteraction().finishDrawing() } }) ] }) }) this.addControl(fedit) return fedit } /** Add modify tools * @private */ _setModifyInteraction(options) { // Modify on selected features if (options.interactions.ModifySelect !== false && options.interactions.Select !== false) { if (options.interactions.ModifySelect instanceof ol.interaction.ModifyFeature) { this._interactions.ModifySelect = options.interactions.ModifySelect } else { this._interactions.ModifySelect = new ol.interaction.ModifyFeature({ features: this.getInteraction('Select').getFeatures() }) } if (this.getMap()) this.getMap().addInteraction(this._interactions.ModifySelect) // Activate with select this._interactions.ModifySelect.setActive(this._interactions.Select.getActive()) this._interactions.Select.on('change:active', function () { this._interactions.ModifySelect.setActive(this._interactions.Select.getActive()) }.bind(this)) } if (options.interactions.Transform !== false) { if (options.interactions.Transform instanceof ol.interaction.Transform) { this._interactions.Transform = options.interactions.Transform } else { this._interactions.Transform = new ol.interaction.Transform({ addCondition: ol.events.condition.shiftKeyOnly }) } var transform = new ol.control.Toggle({ html: '', className: 'ol-transform', title: this._getTitle(options.interactions.Transform) || 'Transform', name: 'Transform', interaction: this._interactions.Transform }) this.addControl(transform) } if (options.interactions.Split !== false) { if (options.interactions.Split instanceof ol.interaction.Split) { this._interactions.Split = options.interactions.Split } else { this._interactions.Split = new ol.interaction.Split({ sources: this._source }) } var split = new ol.control.Toggle({ className: 'ol-split', title: this._getTitle(options.interactions.Split) || 'Split', name: 'Split', interaction: this._interactions.Split }) this.addControl(split) } if (options.interactions.Offset !== false) { if (options.interactions.Offset instanceof ol.interaction.Offset) { this._interactions.Offset = options.interactions.Offset } else { this._interactions.Offset = new ol.interaction.Offset({ source: this._source }) } var offset = new ol.control.Toggle({ html: '', className: 'ol-offset', title: this._getTitle(options.interactions.Offset) || 'Offset', name: 'Offset', interaction: this._interactions.Offset }) this.addControl(offset) } } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A simple gauge control to display level information on the map. * * @constructor * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {String} options.title title of the control * @param {number} options.max maximum value, default 100; * @param {number} options.val the value, default 0 */ ol.control.Gauge = class olcontrolGauge extends ol.control.Control { constructor(options) { options = options || {}; var element = ol.ext.element.create('DIV', { className: ((options.className || "") + ' ol-gauge ol-unselectable ol-control').trim() }); super({ element: element, target: options.target }); this.title_ = ol.ext.element.create('SPAN', { parent: element }); var div = ol.ext.element.create('DIV', { parent: element }); this.gauge_ = ol.ext.element.create('BUTTON', { type: 'button', style: { width: '0px' }, parent: div }); this.setTitle(options.title); this.set("max", options.max || 100); this.val(options.val); } /** Set the control title * @param {string} title */ setTitle(title) { this.title_.innerHTML = title || ""; if (!title) this.title_.display = 'none'; else this.title_.display = ''; } /** Set/get the gauge value * @param {number|undefined} v the value or undefined to get it * @return {number} the value */ val(v) { if (v !== undefined) { this.val_ = v; this.gauge_.style.width = (v / this.get('max') * 100) + "%"; } return this.val_; } } /** Bookmark positions on ol maps. * * @constructor * @extends {ol.control.Control} * @fires add * @fires remove * @fires select * @param {} options Geobookmark's options * @param {string} options.className default ol-bookmark * @param {string | undefined} options.title Title to use for the button tooltip, default "Geobookmarks" * @param {string} options.placeholder input placeholder, default Add a new geomark... * @param {string} [options.deleteTitle='Suppr.'] title for delete buttons * @param {bool} options.editable enable modification, default true * @param {string} options.namespace a namespace to save the boolmark (if more than one on a page), default ol * @param {Array} options.marks a list of default bookmarks: * @see [Geobookmark example](../../examples/control/map.control.geobookmark.html) * @example var bm = new GeoBookmark ({ marks: { "Paris": {pos:ol.proj.transform([2.351828, 48.856578], 'EPSG:4326', 'EPSG:3857'), zoom:11, permanent: true }, "London": {pos:ol.proj.transform([-0.1275,51.507222], 'EPSG:4326', 'EPSG:3857'), zoom:12} } }); */ ol.control.GeoBookmark = class olcontrolGeoBookmark extends ol.control.Control { constructor(options) { options = options || {}; var element = document.createElement('div'); // Init super({ element: element, target: options.target }); var self = this; if (options.target) { element.className = options.className || "ol-bookmark"; } else { element.className = (options.className || "ol-bookmark") + " ol-unselectable ol-control ol-collapsed"; element.addEventListener("mouseleave", function () { if (input !== document.activeElement) { menu.style.display = 'none'; } }); // Show bookmarks on click this.button = ol.ext.element.create('BUTTON', { type: 'button', title: options.title || 'Geobookmarks', click: function () { var show = (menu.style.display === '' || menu.style.display === 'none'); menu.style.display = (show ? 'block' : 'none'); if (show) this.setBookmarks(); }.bind(this) }); element.appendChild(this.button); } // The menu var menu = document.createElement('div'); element.appendChild(menu); var ul = document.createElement('ul'); menu.appendChild(ul); var input = document.createElement('input'); input.setAttribute("placeholder", options.placeholder || "Add a new geomark..."); input.addEventListener("keydown", function (e) { e.stopPropagation(); if (e.keyCode === 13) { e.preventDefault(); var title = this.value; if (title) { self.addBookmark(title); this.value = ''; self.dispatchEvent({ type: "add", name: title }); } menu.style.display = 'none'; } }); input.addEventListener("blur", function () { menu.style.display = 'none'; }); menu.appendChild(input); this.on("propertychange", function (e) { if (e.key === 'editable') { element.className = element.className.replace(" ol-editable", ""); if (this.get('editable')) { element.className += " ol-editable"; } } // console.log(e); }.bind(this)); this.set("namespace", options.namespace || 'ol'); this.set("editable", options.editable !== false); this.set('deleteTitle', options.deleteTitle || 'Suppr.'); // Set default bmark var bmark = {}; try { if (localStorage[this.get('namespace') + "@bookmark"]) { bmark = JSON.parse(localStorage[this.get('namespace') + "@bookmark"]); } } catch (e) { console.warn('Failed to access localStorage...'); } if (options.marks) { for (var i in options.marks) { bmark[i] = options.marks[i]; } } this.setBookmarks(bmark); } /** Set bookmarks * @param {} bmark a list of bookmarks, default retreave in the localstorage * @example bm.setBookmarks({ "Paris": {pos:_ol_proj_.transform([2.351828, 48.856578], 'EPSG:4326', 'EPSG:3857'), zoom:11, permanent: true }, "London": {pos:_ol_proj_.transform([-0.1275,51.507222], 'EPSG:4326', 'EPSG:3857'), zoom:12} }); */ setBookmarks(bmark) { if (!bmark) { bmark = {}; try { bmark = JSON.parse(localStorage[this.get('namespace') + "@bookmark"] || "{}"); } catch (e) { console.warn('Failed to access localStorage...'); } } var modify = this.get("editable"); var ul = this.element.querySelector("ul"); var menu = this.element.querySelector("div"); var self = this; ul.innerHTML = ''; for (var b in bmark) { var li = document.createElement('li'); li.textContent = b; li.setAttribute('data-bookmark', JSON.stringify(bmark[b])); li.setAttribute('data-name', b); li.addEventListener('click', function () { var bm = JSON.parse(this.getAttribute("data-bookmark")); self.getMap().getView().setCenter(bm.pos); self.getMap().getView().setZoom(bm.zoom); self.getMap().getView().setRotation(bm.rot || 0); menu.style.display = 'none'; self.dispatchEvent({ type: 'select', name: this.getAttribute("data-name"), bookmark: bm }); }); ul.appendChild(li); if (modify && !bmark[b].permanent) { var button = document.createElement('button'); button.setAttribute('data-name', b); button.setAttribute('type', 'button'); button.setAttribute('title', this.get('deleteTitle') || 'Suppr.'); button.addEventListener('click', function (e) { self.removeBookmark(this.getAttribute("data-name")); self.dispatchEvent({ type: "remove", name: this.getAttribute("data-name") }); e.stopPropagation(); }); li.appendChild(button); } } try { localStorage[this.get('namespace') + "@bookmark"] = JSON.stringify(bmark); } catch (e) { console.warn('Failed to access localStorage...'); } } /** Get Geo bookmarks * @return {any} a list of bookmarks : { BM1:{pos:ol.coordinates, zoom: integer}, BM2:{pos:ol.coordinates, zoom: integer} } */ getBookmarks() { var bm = {}; try { bm = JSON.parse(localStorage[this.get('namespace') + "@bookmark"] || "{}"); } catch (e) { console.warn('Failed to access localStorage...'); } return bm; } /** Remove a Geo bookmark * @param {string} name */ removeBookmark(name) { if (!name) { return; } var bmark = this.getBookmarks(); delete bmark[name]; this.setBookmarks(bmark); } /** Add a new Geo bookmark (replace existing one if any) * @param {string} name name of the bookmark (display in the menu) * @param {*} options * @param {ol.coordinate} position default current position * @param {number} zoom default current map zoom * @param {number} rotation default current map rotation * @param {bool} permanent prevent from deletion, default false */ addBookmark(name, position, zoom, permanent) { if (!name) return; var options = position; var rot; if (options && options.position) { zoom = options.zoom; permanent = options.permanent; rot = options.rotation; position = options.position; } else { rot = this.getMap().getView().getRotation(); } var bmark = this.getBookmarks(); // Don't override permanent bookmark if (bmark[name] && bmark[name].permanent) return; // Create or override bmark[name] = { pos: position || this.getMap().getView().getCenter(), zoom: zoom || this.getMap().getView().getZoom(), permanent: !!permanent }; if (rot) { bmark[name].rot = rot; } this.setBookmarks(bmark); } } /* 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). */ /** Geolocation bar * The control bar is a container for other controls. It can be used to create toolbars. * Control bars can be nested and combined with ol.control.Toggle to handle activate/deactivate. * * @constructor * @extends {ol.control.Bar} * @param {Object=} options Control bar options. * @param {String} options.className class of the control * @param {String} options.centerLabel label for center button, default center * @param {String} options.position position of the control, default bottom-right */ ol.control.GeolocationBar = class olcontrolGeolocationBar extends ol.control.Bar { constructor(options) { options = options || {} options.className = options.className || 'ol-geobar' super(options) this.setPosition(options.position || 'bottom-right') var element = this.element // Geolocation draw interaction var interaction = new ol.interaction.GeolocationDraw({ source: options.source, zoom: options.zoom, minZoom: options.minZoom, tolerance: options.tolerance, followTrack: options.followTrack, minAccuracy: options.minAccuracy || 10000 }) this._geolocBt = new ol.control.Toggle({ className: 'geolocBt', interaction: interaction, onToggle: function () { interaction.pause(true) interaction.setFollowTrack(options.followTrack) element.classList.remove('pauseTrack') } }) this.addControl(this._geolocBt) this._geolocBt.setActive(false) // Buttons var bar = new ol.control.Bar() this.addControl(bar) var centerBt = new ol.control.TextButton({ className: 'centerBt', html: options.centerLabel || 'center', handleClick: function () { interaction.setFollowTrack('auto') } }) bar.addControl(centerBt) var startBt = new ol.control.Button({ className: 'startBt', handleClick: function () { interaction.pause(false) interaction.setFollowTrack('auto') element.classList.add('pauseTrack') } }) bar.addControl(startBt) var pauseBt = new ol.control.Button({ className: 'pauseBt', handleClick: function () { interaction.pause(true) interaction.setFollowTrack('auto') element.classList.remove('pauseTrack') } }) bar.addControl(pauseBt) interaction.on('follow', function (e) { if (e.following) { element.classList.remove('centerTrack') } else { element.classList.add('centerTrack') } }) // Activate this._geolocBt.on('change:active', function (e) { if (e.active) { element.classList.add('ol-active') } else { element.classList.remove('ol-active') } }) } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null super.setMap(map) // Get change (new layer added or removed) if (map) { this._listener = map.on('moveend', function () { var geo = this.getInteraction() if (geo.getActive() && geo.get('followTrack') === 'auto' && geo.path_.length) { if (geo.path_[geo.path_.length - 1][0] !== map.getView().getCenter()[0]) { this.element.classList.add('centerTrack') } } }.bind(this)) } } /** Get the ol.interaction.GeolocationDraw associatedwith the bar * @return {ol.interaction.GeolocationDraw} */ getInteraction() { return this._geolocBt.getInteraction() } } /* 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). */ /** Geolocation bar * The control bar is a container for other controls. It can be used to create toolbars. * Control bars can be nested and combined with ol.control.Toggle to handle activate/deactivate. * * @constructor * @fires tracking * @extends {ol.control.Toggle} * @param {Object=} options ol.interaction.GeolocationDraw option. * @param {String} options.className class of the control * @param {String} options.title title of the control to display as tooltip, default Geolocation * @param {number} options.delay delay before removing the location in ms, delfaut 3000 (3s) */ ol.control.GeolocationButton = class olcontrolGeolocationButton extends ol.control.Toggle { constructor(options) { options = options || {}; // Geolocation draw interaction options.followTrack = options.followTrack || 'auto'; options.zoom = options.zoom || 16; //options.minZoom = options.minZoom || 16; var interaction = new ol.interaction.GeolocationDraw(options); super({ className: options.className = ((options.className || '') + ' ol-geobt').trim(), interaction: interaction, title: options.title || 'Geolocation', onToggle: function () { interaction.pause(true); interaction.setFollowTrack(options.followTrack || 'auto'); } }); this.setActive(false); interaction.on('tracking', function (e) { this.dispatchEvent({ type: 'position', coordinate: e.geolocation.getPosition() }); }.bind(this)); // Timeout delay var tout; interaction.on('change:active', function () { this.dispatchEvent({ type: 'position' }); if (tout) { clearTimeout(tout); tout = null; } if (interaction.getActive()) { tout = setTimeout(function () { interaction.setActive(false); tout = null; }.bind(this), options.delay || 3000); } }.bind(this)); } } /* 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). */ /** * OpenLayers 3 lobe Overview Control. * The globe can rotate with map (follow.) * * @constructor * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {boolean} follow follow the map when center change, default false * @param {top|bottom-left|right} align position as top-left, etc. * @param {Array} layers list of layers to display on the globe * @param {ol.style.Style | Array. | undefined} style style to draw the position on the map , default a marker */ ol.control.Globe = class olcontrolGlobe extends ol.control.Control { constructor(opt_options) { var options = opt_options || {} var element = document.createElement('div'); super({ element: element, target: options.target }) var self = this // API if (options.target) { this.panel_ = options.target } else { element.classList.add('ol-globe', 'ol-unselectable', 'ol-control') if (/top/.test(options.align)) element.classList.add('ol-control-top') if (/right/.test(options.align)) element.classList.add('ol-control-right') this.panel_ = document.createElement("div") this.panel_.classList.add("panel") element.appendChild(this.panel_) this.pointer_ = document.createElement("div") this.pointer_.classList.add("ol-pointer") element.appendChild(this.pointer_) } // http://openlayers.org/en/latest/examples/sphere-mollweide.html ??? // Create a globe map this.ovmap_ = new ol.Map({ controls: new ol.Collection(), interactions: new ol.Collection(), target: this.panel_, view: new ol.View({ zoom: 0, center: [0, 0] }), layers: options.layers }) setTimeout(function () { self.ovmap_.updateSize() }, 0) this.set('follow', options.follow || false) // Cache extent this.extentLayer = new ol.layer.Vector({ name: 'Cache extent', source: new ol.source.Vector(), style: options.style || [ new ol.style.Style({ image: new ol.style.Circle({ fill: new ol.style.Fill({ color: 'rgba(255,0,0, 1)' }), stroke: new ol.style.Stroke( { width: 7, color: 'rgba(255,0,0, 0.8)' }), radius: 5 }) })] }) this.ovmap_.addLayer(this.extentLayer) } /** * Set the map instance the control associated with. * @param {ol.Map} map The map instance. */ setMap(map) { if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null ol.control.Control.prototype.setMap.call(this, map) // Get change (new layer added or removed) if (map) { this._listener = map.getView().on('propertychange', this.setView.bind(this)) this.setView() } } /** Set the globe center with the map center */ setView() { if (this.getMap() && this.get('follow')) { this.setCenter(this.getMap().getView().getCenter()) } } /** Get globe map * @return {ol.Map} */ getGlobe() { return this.ovmap_ } /** Show/hide the globe */ show(b) { if (b !== false) this.element.classList.remove("ol-collapsed") else this.element.classList.add("ol-collapsed") this.ovmap_.updateSize() } /** Set position on the map * @param {top|bottom-left|right} align */ setPosition(align) { if (/top/.test(align)) this.element.classList.add("ol-control-top") else this.element.classList.remove("ol-control-top") if (/right/.test(align)) this.element.classList.add("ol-control-right") else this.element.classList.remove("ol-control-right") } /** Set the globe center * @param {_ol_coordinate_} center the point to center to * @param {boolean} show show a pointer on the map, defaylt true */ setCenter(center, show) { var self = this this.pointer_.classList.add("hidden") if (center) { var map = this.ovmap_ var p = map.getPixelFromCoordinate(center) if (p) { if (show !== false) { var h = this.element.clientHeight setTimeout(function () { self.pointer_.style.top = String(Math.min(Math.max(p[1], 0), h)) + 'px' self.pointer_.style.left = "50%" self.pointer_.classList.remove("hidden") }, 800) } map.getView().animate({ center: [center[0], 0] }) } } } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Draw a graticule on the map. * @constructor * @author mike-000 https://github.com/mike-000 * @author Jean-Marc Viglino https://github.com/viglino * @extends {ol.control.CanvasBase} * @param {Object=} _ol_control_ options. * @param {ol.projectionLike} options.projection projection to use for the graticule, default EPSG:4326 * @param {number} options.maxResolution max resolution to display the graticule * @param {ol.style.Style} options.style Style to use for drawing the graticule, default black / white. Line style is used for drawing lines (no line if not defined). Fill style is used to draw the border. Text style is used to draw coords. * @param {number} options.step step between lines (in proj units), default 1 * @param {number} options.stepCoord show a coord every stepCoord, default 1 * @param {number} options.spacing spacing between lines (in px), default 40px * @param {Array} options.intervals array (in desending order) of intervals (in proj units) constraining which lines will be displayed, default is no contraint (any multiple of step can be used) * @param {number} options.precision precision interval (in proj units) of displayed lines, if the line interval exceeds this more calculations will be used to display curved lines more accurately * @param {number} options.borderWidth width of the border (in px), default 5px * @param {number} options.margin margin of the border (in px), default 0px * @param {number} options.formatCoord a function that takes a coordinate and a position and return the formated coordinate */ ol.control.Graticule = class olcontrolGraticule extends ol.control.CanvasBase { constructor(options) { options = options || {} // Initialize parent var elt = document.createElement("div") elt.className = "ol-graticule ol-unselectable ol-hidden" super({ element: elt }) this.set('projection', options.projection || 'EPSG:4326') // Use to limit calculation var p = new ol.proj.Projection({ code: this.get('projection') }) var m = p.getMetersPerUnit() this.fac = 1 while (m / this.fac > 10) { this.fac *= 10 } this.fac = 10000 / this.fac this.set('maxResolution', options.maxResolution || Infinity) this.set('step', options.step || 0.1) this.set('stepCoord', options.stepCoord || 1) this.set('spacing', options.spacing || 40) this.set('intervals', options.intervals) this.set('precision', options.precision) this.set('margin', options.margin || 0) this.set('borderWidth', options.borderWidth || 5) this.set('stroke', options.stroke !== false) this.formatCoord = options.formatCoord || function (c) { return c } if (options.style instanceof ol.style.Style) { this.setStyle(options.style) } else { this.setStyle(new ol.style.Style({ stroke: new ol.style.Stroke({ color: "#000", width: 1 }), fill: new ol.style.Fill({ color: "#fff" }), text: new ol.style.Text({ stroke: new ol.style.Stroke({ color: "#fff", width: 2 }), fill: new ol.style.Fill({ color: "#000" }), }) })) } } setStyle(style) { this._style = style } _draw(e) { if (this.get('maxResolution') < e.frameState.viewState.resolution) return var ctx = this.getContext(e) var canvas = ctx.canvas var ratio = e.frameState.pixelRatio var w = canvas.width / ratio var h = canvas.height / ratio var proj = this.get('projection') var map = this.getMap() var bbox = [map.getCoordinateFromPixel([0, 0]), map.getCoordinateFromPixel([w, 0]), map.getCoordinateFromPixel([w, h]), map.getCoordinateFromPixel([0, h]) ] var xmax = -Infinity var xmin = Infinity var ymax = -Infinity var ymin = Infinity for (var i = 0, c; c = bbox[i]; i++) { bbox[i] = ol.proj.transform(c, map.getView().getProjection(), proj) xmax = Math.max(xmax, bbox[i][0]) xmin = Math.min(xmin, bbox[i][0]) ymax = Math.max(ymax, bbox[i][1]) ymin = Math.min(ymin, bbox[i][1]) } var spacing = this.get('spacing') var step = this.get('step') var step2 = this.get('stepCoord') var borderWidth = this.get('borderWidth') var margin = this.get('margin') // Limit max line draw var ds = (xmax - xmin) / step * spacing if (ds > w) { var dt = Math.round((xmax - xmin) / w * spacing / step) step *= dt if (step > this.fac) step = Math.round(step / this.fac) * this.fac } var intervals = this.get('intervals') if (Array.isArray(intervals)) { var interval = intervals[0] for (var i = 0, ii = intervals.length; i < ii; ++i) { if (step >= intervals[i]) { break } interval = intervals[i] } step = interval } var precision = this.get('precision') var calcStep = step if (precision > 0 && step > precision) { calcStep = step / Math.ceil(step / precision) } xmin = (Math.floor(xmin / step)) * step - step ymin = (Math.floor(ymin / step)) * step - step xmax = (Math.floor(xmax / step)) * step + 2 * step ymax = (Math.floor(ymax / step)) * step + 2 * step var extent = ol.proj.get(proj).getExtent() if (extent) { if (xmin < extent[0]) xmin = extent[0] if (ymin < extent[1]) ymin = extent[1] if (xmax > extent[2]) xmax = extent[2] + step if (ymax > extent[3]) ymax = extent[3] + step } var hasLines = this.getStyle().getStroke() && this.get("stroke") var hasText = this.getStyle().getText() var hasBorder = this.getStyle().getFill() ctx.save() ctx.scale(ratio, ratio) ctx.beginPath() ctx.rect(margin, margin, w - 2 * margin, h - 2 * margin) ctx.clip() ctx.beginPath() var txt = { top: [], left: [], bottom: [], right: [] } var x, y, p, p0, p1 for (x = xmin; x < xmax; x += step) { p0 = ol.proj.transform([x, ymin], proj, map.getView().getProjection()) p0 = map.getPixelFromCoordinate(p0) if (hasLines) ctx.moveTo(p0[0], p0[1]) p = p0 for (y = ymin + calcStep; y <= ymax; y += calcStep) { p1 = ol.proj.transform([x, y], proj, map.getView().getProjection()) p1 = map.getPixelFromCoordinate(p1) if (hasLines) ctx.lineTo(p1[0], p1[1]) if (p[1] > 0 && p1[1] < 0) txt.top.push([x, p]) if (p[1] > h && p1[1] < h) txt.bottom.push([x, p]) p = p1 } } for (y = ymin; y < ymax; y += step) { p0 = ol.proj.transform([xmin, y], proj, map.getView().getProjection()) p0 = map.getPixelFromCoordinate(p0) if (hasLines) ctx.moveTo(p0[0], p0[1]) p = p0 for (x = xmin + calcStep; x <= xmax; x += calcStep) { p1 = ol.proj.transform([x, y], proj, map.getView().getProjection()) p1 = map.getPixelFromCoordinate(p1) if (hasLines) ctx.lineTo(p1[0], p1[1]) if (p[0] < 0 && p1[0] > 0) txt.left.push([y, p]) if (p[0] < w && p1[0] > w) txt.right.push([y, p]) p = p1 } } if (hasLines) { ctx.strokeStyle = this.getStyle().getStroke().getColor() ctx.lineWidth = this.getStyle().getStroke().getWidth() ctx.stroke() } // Draw text if (hasText) { ctx.fillStyle = this.getStyle().getText().getFill().getColor() ctx.strokeStyle = this.getStyle().getText().getStroke().getColor() ctx.lineWidth = this.getStyle().getText().getStroke().getWidth() ctx.font = this.getStyle().getText().getFont() ctx.textAlign = 'center' ctx.textBaseline = 'hanging' var t, tf var offset = (hasBorder ? borderWidth : 0) + margin + 2 for (i = 0; t = txt.top[i]; i++) if (!(Math.round(t[0] / this.get('step')) % step2)) { tf = this.formatCoord(t[0], 'top') ctx.strokeText(tf, t[1][0], offset) ctx.fillText(tf, t[1][0], offset) } ctx.textBaseline = 'alphabetic' for (i = 0; t = txt.bottom[i]; i++) if (!(Math.round(t[0] / this.get('step')) % step2)) { tf = this.formatCoord(t[0], 'bottom') ctx.strokeText(tf, t[1][0], h - offset) ctx.fillText(tf, t[1][0], h - offset) } ctx.textBaseline = 'middle' ctx.textAlign = 'left' for (i = 0; t = txt.left[i]; i++) if (!(Math.round(t[0] / this.get('step')) % step2)) { tf = this.formatCoord(t[0], 'left') ctx.strokeText(tf, offset, t[1][1]) ctx.fillText(tf, offset, t[1][1]) } ctx.textAlign = 'right' for (i = 0; t = txt.right[i]; i++) if (!(Math.round(t[0] / this.get('step')) % step2)) { tf = this.formatCoord(t[0], 'right') ctx.strokeText(tf, w - offset, t[1][1]) ctx.fillText(tf, w - offset, t[1][1]) } } // Draw border if (hasBorder) { var fillColor = this.getStyle().getFill().getColor() var color, stroke if (stroke = this.getStyle().getStroke()) { color = this.getStyle().getStroke().getColor() } else { color = fillColor fillColor = "#fff" } ctx.strokeStyle = color ctx.lineWidth = stroke ? stroke.getWidth() : 1 // for (i = 1; i < txt.top.length; i++) { ctx.beginPath() ctx.rect(txt.top[i - 1][1][0], margin, txt.top[i][1][0] - txt.top[i - 1][1][0], borderWidth) ctx.fillStyle = Math.round(txt.top[i][0] / step) % 2 ? color : fillColor ctx.fill() ctx.stroke() } for (i = 1; i < txt.bottom.length; i++) { ctx.beginPath() ctx.rect(txt.bottom[i - 1][1][0], h - borderWidth - margin, txt.bottom[i][1][0] - txt.bottom[i - 1][1][0], borderWidth) ctx.fillStyle = Math.round(txt.bottom[i][0] / step) % 2 ? color : fillColor ctx.fill() ctx.stroke() } for (i = 1; i < txt.left.length; i++) { ctx.beginPath() ctx.rect(margin, txt.left[i - 1][1][1], borderWidth, txt.left[i][1][1] - txt.left[i - 1][1][1]) ctx.fillStyle = Math.round(txt.left[i][0] / step) % 2 ? color : fillColor ctx.fill() ctx.stroke() } for (i = 1; i < txt.right.length; i++) { ctx.beginPath() ctx.rect(w - borderWidth - margin, txt.right[i - 1][1][1], borderWidth, txt.right[i][1][1] - txt.right[i - 1][1][1]) ctx.fillStyle = Math.round(txt.right[i][0] / step) % 2 ? color : fillColor ctx.fill() ctx.stroke() } ctx.beginPath() ctx.fillStyle = color ctx.rect(margin, margin, borderWidth, borderWidth) ctx.rect(margin, h - borderWidth - margin, borderWidth, borderWidth) ctx.rect(w - borderWidth - margin, margin, borderWidth, borderWidth) ctx.rect(w - borderWidth - margin, h - borderWidth - margin, borderWidth, borderWidth) ctx.fill() } ctx.restore() } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Draw a grid reference on the map and add an index. * * @constructor * @extends {ol.control.CanvasBase} * @fires select * @param {Object=} Control options. * @param {ol.style.Style} options.style Style to use for drawing the grid (stroke and text), default black. * @param {number} options.maxResolution max resolution to display the graticule * @param {ol.extent} options.extent extent of the grid, required * @param {ol.size} options.size number of lines and cols, required * @param {number} options.margin margin to display text (in px), default 0px * @param {ol.source.Vector} options.source source to use for the index, default none (use setIndex to reset the index) * @param {string | function} options.property a property to display in the index or a function that takes a feature and return the name to display in the index, default 'name'. * @param {function|undefined} options.sortFeatures sort function to sort 2 features in the index, default sort on property option * @param {function|undefined} options.indexTitle a function that takes a feature and return the title to display in the index, default the first letter of property option * @param {string} options.filterLabel label to display in the search bar, default 'filter' */ ol.control.GridReference = class olcontrolGridReference extends ol.control.CanvasBase { constructor(options) { options = options || {} // Initialize parent var elt = document.createElement("div") elt.className = (!options.target ? "ol-control " : "") + "ol-gridreference ol-unselectable " + (options.className || "") options.style = options.style || new ol.style.Style({ stroke: new ol.style.Stroke({ color: "#000", width: 1 }), text: new ol.style.Text({ font: "bold 14px Arial", stroke: new ol.style.Stroke({ color: "#fff", width: 2 }), fill: new ol.style.Fill({ color: "#000" }), }) }) super({ element: elt, target: options.target, style: options.style }) if (typeof (options.property) == 'function'){ this.getFeatureName = options.property } if (typeof (options.sortFeatures) == 'function') { this.sortFeatures = options.sortFeatures } if (typeof (options.indexTitle) == 'function') { this.indexTitle = options.indexTitle } // Set index using the source this.source_ = options.source if (options.source) { this.setIndex(options.source.getFeatures(), options) // reload on ready options.source.once('change', function () { if (options.source.getState() === 'ready') { this.setIndex(options.source.getFeatures(), options) } }.bind(this)) } // Options this.set('maxResolution', options.maxResolution || Infinity) this.set('extent', options.extent) this.set('size', options.size) this.set('margin', options.margin || 0) this.set('property', options.property || 'name') this.set('filterLabel', options.filterLabel || 'filter') } /** * Set the map instance the control is associated with. * @param {ol.Map} map The map instance. */ setMap(map) { super.setMap(map) this.setIndex(this.source_.getFeatures()) } /** Returns the text to be displayed in the index * @param {ol.Feature} f the feature * @return {string} the text to be displayed in the index * @api */ getFeatureName(f) { return f.get(this.get('property') || 'name') } /** Sort function * @param {ol.Feature} a first feature * @param {ol.Feature} b second feature * @return {Number} 0 if a==b, -1 if ab * @api */ sortFeatures(a, b) { return (this.getFeatureName(a) == this.getFeatureName(b)) ? 0 : (this.getFeatureName(a) < this.getFeatureName(b)) ? -1 : 1 } /** Get the feature title * @param {ol.Feature} f * @return the first letter of the eature name (getFeatureName) * @api */ indexTitle(f) { return this.getFeatureName(f).charAt(0) } /** Display features in the index * @param { Array | ol.Collection } features */ setIndex(features) { if (!this.getMap()) return var self = this if (features.getArray) features = features.getArray() features.sort(function (a, b) { return self.sortFeatures(a, b) }) this.element.innerHTML = "" var elt = this.element var search = document.createElement("input") search.setAttribute('type', 'search') search.setAttribute('placeholder', this.get('filterLabel') || 'filter') var searchKeyupFunction = function () { var v = this.value.replace(/^\*/, '') // console.log(v) var r = new RegExp(v, 'i') var list = ul.querySelectorAll('li') Array.prototype.forEach.call(list, function (li) { if (li.classList.contains('ol-title')) { li.style.display = '' } else { if (r.test(li.querySelector('.ol-name').textContent)) li.style.display = '' else li.style.display = 'none' } }) Array.prototype.forEach.call(ul.querySelectorAll("li.ol-title"), function (li) { var nextVisible var start = false for (var i = 0; i < list.length; i++) { if (start) { if (list[i].classList.contains('ol-title')) break if (!list[i].style.display) { nextVisible = list[i] break } } if (list[i] === li) start = true } if (nextVisible) li.style.display = '' else li.style.display = 'none' }) } search.addEventListener('search', searchKeyupFunction) search.addEventListener('keyup', searchKeyupFunction) elt.appendChild(search) var ul = document.createElement("ul") elt.appendChild(ul) var r, title features.forEach(function (feat) { r = this.getReference(feat.getGeometry().getFirstCoordinate()) if (r) { var name = this.getFeatureName(feat) var c = this.indexTitle(feat) if (c != title) { var li_title = document.createElement("li") li_title.classList.add('ol-title') li_title.textContent = c ul.appendChild(li_title) } title = c var li_ref_name = document.createElement("li") var span_name = document.createElement("span") span_name.classList.add("ol-name") span_name.textContent = name li_ref_name.appendChild(span_name) var span_ref = document.createElement("span") span_ref.classList.add("ol-ref") span_ref.textContent = r li_ref_name.appendChild(span_ref) var feature = feat li_ref_name.addEventListener("click", function () { this.dispatchEvent({ type: "select", feature: feature }) }.bind(this)) ul.appendChild(li_ref_name) } }.bind(this)) } /** Get reference for a coord * @param {ol.coordinate} coords * @return {string} the reference */ getReference(coords) { if (!this.getMap()) return var extent = this.get('extent') var size = this.get('size') var dx = Math.floor((coords[0] - extent[0]) / (extent[2] - extent[0]) * size[0]) if (dx < 0 || dx >= size[0]) return "" var dy = Math.floor((extent[3] - coords[1]) / (extent[3] - extent[1]) * size[1]) if (dy < 0 || dy >= size[1]) return "" return this.getHIndex(dx) + this.getVIndex(dy) } /** Get vertical index (0,1,2,3...) * @param {number} index * @returns {string} * @api */ getVIndex(index) { return index } /** Get horizontal index (A,B,C...) * @param {number} index * @returns {string} * @api */ getHIndex(index) { return String.fromCharCode(65 + index) } /** Draw the grid * @param {ol.event} e postcompose event * @private */ _draw(e) { if (this.get('maxResolution') < e.frameState.viewState.resolution) return var ctx = this.getContext(e) var canvas = ctx.canvas var ratio = e.frameState.pixelRatio var w = canvas.width / ratio var h = canvas.height / ratio var extent = this.get('extent') var size = this.get('size') var map = this.getMap() var ex = ol.extent.boundingExtent([map.getPixelFromCoordinate([extent[0], extent[1]]), map.getPixelFromCoordinate([extent[2], extent[3]])]) var p0 = [ex[0], ex[1]] var p1 = [ex[2], ex[3]] var dx = (p1[0] - p0[0]) / size[0] var dy = (p1[1] - p0[1]) / size[1] ctx.save() var margin = this.get('margin') ctx.scale(ratio, ratio) ctx.strokeStyle = this.getStroke().getColor() ctx.lineWidth = this.getStroke().getWidth() // Draw grid ctx.beginPath() var i for (i = 0; i <= size[0]; i++) { ctx.moveTo(p0[0] + i * dx, p0[1]) ctx.lineTo(p0[0] + i * dx, p1[1]) } for (i = 0; i <= size[1]; i++) { ctx.moveTo(p0[0], p0[1] + i * dy) ctx.lineTo(p1[0], p0[1] + i * dy) } ctx.stroke() // Draw text ctx.font = this.getTextFont() ctx.fillStyle = this.getTextFill().getColor() ctx.strokeStyle = this.getTextStroke().getColor() var lw = ctx.lineWidth = this.getTextStroke().getWidth() var spacing = margin + lw ctx.textAlign = 'center' var letter, x, y for (i = 0; i < size[0]; i++) { letter = this.getHIndex(i) x = p0[0] + i * dx + dx / 2 y = p0[1] - spacing if (y < 0) { y = spacing ctx.textBaseline = 'hanging' } else ctx.textBaseline = 'alphabetic' ctx.strokeText(letter, x, y) ctx.fillText(letter, x, y) y = p1[1] + spacing if (y > h) { y = h - spacing ctx.textBaseline = 'alphabetic' } else ctx.textBaseline = 'hanging' ctx.strokeText(letter, x, y) ctx.fillText(letter, x, y) } ctx.textBaseline = 'middle' for (i = 0; i < size[1]; i++) { letter = this.getVIndex(i) y = p0[1] + i * dy + dy / 2 ctx.textAlign = 'right' x = p0[0] - spacing if (x < 0) { x = spacing ctx.textAlign = 'left' } else ctx.textAlign = 'right' ctx.strokeText(letter, x, y) ctx.fillText(letter, x, y) x = p1[0] + spacing if (x > w) { x = w - spacing ctx.textAlign = 'right' } else ctx.textAlign = 'left' ctx.strokeText(letter, x, y) ctx.fillText(letter, x, y) } ctx.restore() } } /** Image line control * * @constructor * @extends {ol.control.Control} * @fires select * @fires collapse * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {Array|ol.source.Vector} options.source vector sources that contains the images * @param {Array} options.layers A list of layers to display images. If no source and no layers, all visible layers will be considered. * @param {function} options.getImage a function that gets a feature and return the image url or false if no image to Show, default return the img propertie * @param {function} options.getTitle a function that gets a feature and return the title, default return an empty string * @param {boolean} options.collapsed the line is collapse, default false * @param {boolean} options.collapsible the line is collapsible, default false * @param {number} options.maxFeatures the maximum image element in the line, default 100 * @param {number} options.useExtent only show feature in the current extent * @param {boolean} options.hover select image on hover, default false * @param {string|boolean} options.linkColor link color or false if no link, default false */ ol.control.Imageline = class olcontrolImageline extends ol.control.Control { constructor(options) { var element = ol.ext.element.create('DIV', { className: (options.className || '') + ' ol-imageline' + (options.target ? '' : ' ol-unselectable ol-control') + (options.collapsed && options.collapsible ? 'ol-collapsed' : '') }); // Initialize super({ element: element, target: options.target }); if (!options.target && options.collapsible) { ol.ext.element.create('BUTTON', { type: 'button', click: function () { this.toggle(); }.bind(this), parent: element }); } // Source if (options.source) this._sources = options.source.forEach ? options.source : [options.source]; if (options.layers) { this.setLayers(options.layers); } // Scroll imageline this._setScrolling(); this._scrolldiv.addEventListener("scroll", function () { if (this.getMap()) this.getMap().render(); }.bind(this)); // Parameters if (typeof (options.getImage) === 'function') this._getImage = options.getImage; if (typeof (options.getTitle) === 'function') this._getTitle = options.getTitle; this.set('maxFeatures', options.maxFeatures || 100); this.set('linkColor', options.linkColor || false); this.set('hover', options.hover || false); this.set('useExtent', options.useExtent || false); this.refresh(); } /** * Remove the control from its current map and attach it to the new map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._listener) { this._listener.forEach(function (l) { ol.Observable.unByKey(l); }.bind(this)); } this._listener = null; super.setMap(map); if (map) { this._listener = [ map.on('postcompose', this._drawLink.bind(this)), map.on('moveend', function () { if (this.get('useExtent')) this.refresh(); }.bind(this)) ]; this.refresh(); } } /** Set layers to use in the control * @param {Array} layers */ setLayers(layers) { this._sources = this._getSources(layers); } /** Get source from a set of layers * @param {Array} layers * @returns {Array} * @private */ _getSources(layers) { var sources = []; layers.forEach(function (l) { if (l.getVisible()) { if (l.getSource() && l.getSource().getFeatures) sources.push(l.getSource()); else if (l.getLayers) this._getSources(l.getLayers()); } }.bind(this)); return sources; } /** Set useExtent param and refresh the line * @param {boolean} b */ useExtent(b) { this.set('useExtent', b); this.refresh(); } /** Is the line collapsed * @return {boolean} */ isCollapsed() { return this.element.classList.contains('ol-collapsed'); } /** Collapse the line * @param {boolean} b */ collapse(b) { if (b) this.element.classList.add('ol-collapsed'); else this.element.classList.remove('ol-collapsed'); if (this.getMap()) { setTimeout(function () { this.getMap().render(); }.bind(this), this.isCollapsed() ? 0 : 250); } this.dispatchEvent({ type: 'collapse', collapsed: this.isCollapsed() }); } /** Collapse the line */ toggle() { this.element.classList.toggle('ol-collapsed'); if (this.getMap()) { setTimeout(function () { this.getMap().render(); }.bind(this), this.isCollapsed() ? 0 : 250); } this.dispatchEvent({ type: 'collapse', collapsed: this.isCollapsed() }); } /** Default function to get an image of a feature * @param {ol.Feature} f * @private */ _getImage(f) { return f.get('img'); } /** Default function to get an image title * @param {ol.Feature} f * @private */ _getTitle( /* f */) { return ''; } /** * Get features * @return {Array} */ getFeatures() { var map = this.getMap(); if (!map) return []; var features = []; var sources = this._sources || this._getSources(map.getLayers()); sources.forEach(function (s) { if (features.length < this.get('maxFeatures')) { if (!this.get('useExtent') || !map) { features.push(s.getFeatures()); } else { var extent = map.getView().calculateExtent(map.getSize()); features.push(s.getFeaturesInExtent(extent)); } } }.bind(this)); return features; } /** Set element scrolling with a acceleration effect on desktop * (on mobile it uses the scroll of the browser) * @private */ _setScrolling() { var elt = this._scrolldiv = ol.ext.element.create('DIV', { parent: this.element }); ol.ext.element.scrollDiv(elt, { // Prevent selection when moving onmove: function (b) { this._moving = b; }.bind(this) }); elt.addEventListener('scroll', this._updateScrollBounds.bind(this)); this._updateScrollBounds(); } /** Set element scrolling with a acceleration effect on desktop * (on mobile it uses the scroll of the browser) * @private */ _updateScrollBounds() { var elt = this._scrolldiv; if (elt.scrollLeft < 5) { this.element.classList.add('ol-scroll0'); } else { this.element.classList.remove('ol-scroll0'); } if (elt.scrollWidth - elt.scrollLeft - elt.offsetWidth < 5) { this.element.classList.add('ol-scroll1'); } else { this.element.classList.remove('ol-scroll1'); } } /** * Refresh the imageline with new data */ refresh() { this._scrolldiv.innerHTML = ''; var allFeatures = this.getFeatures(); var current = this._select ? this._select.feature : null; if (this._select) this._select.elt = null; this._iline = []; if (this.getMap()) this.getMap().render(); // Add a new image var addImage = function (f) { if (this._getImage(f)) { var img = ol.ext.element.create('DIV', { className: 'ol-image', parent: this._scrolldiv }); ol.ext.element.create('IMG', { src: this._getImage(f), parent: img }).addEventListener('load', function () { this.classList.add('ol-loaded'); }); ol.ext.element.create('SPAN', { html: this._getTitle(f), parent: img }); // Current image var sel = { elt: img, feature: f }; // On click > dispatch event img.addEventListener('click', function () { if (!this._moving) { this._scrolldiv.scrollLeft = img.offsetLeft + ol.ext.element.getStyle(img, 'width') / 2 - ol.ext.element.getStyle(this.element, 'width') / 2; if (this._select) this._select.elt.classList.remove('select'); this._select = sel; if (this._select) this._select.elt.classList.add('select'); this.dispatchEvent({ type: 'select', feature: f }); } }.bind(this)); // Show link img.addEventListener('mouseover', function (e) { if (this.get('hover')) { if (this._select) this._select.elt.classList.remove('select'); this._select = sel; this._select.elt.classList.add('select'); this.getMap().render(); e.stopPropagation(); } }.bind(this)); // Remove link img.addEventListener('mouseout', function (e) { if (this.get('hover')) { if (this._select) this._select.elt.classList.remove('select'); this._select = false; this.getMap().render(); e.stopPropagation(); } }.bind(this)); // Prevent image dragging img.ondragstart = function () { return false; }; // Add image this._iline.push(sel); if (current === f) { this._select = sel; sel.elt.classList.add('select'); } } }.bind(this); // Add images var nb = this.get('maxFeatures'); allFeatures.forEach(function (features) { for (var i = 0, f; f = features[i]; i++) { if (nb-- < 0) break; addImage(f); } }.bind(this)); // Add the selected one if (this._select && this._select.feature && !this._select.elt) { addImage(this._select.feature); } this._updateScrollBounds(); } /** Center image line on a feature * @param {ol.feature} feature * @param {boolean} scroll scroll the line to center on the image, default true * @api */ select(feature, scroll) { this._select = false; // Find the image this._iline.forEach(function (f) { if (f.feature === feature) { f.elt.classList.add('select'); this._select = f; if (scroll !== false) { this._scrolldiv.scrollLeft = f.elt.offsetLeft + ol.ext.element.getStyle(f.elt, 'width') / 2 - ol.ext.element.getStyle(this.element, 'width') / 2; } } else { f.elt.classList.remove('select'); } }.bind(this)); } /** Draw link on the map * @private */ _drawLink(e) { if (!this.get('linkColor') | this.isCollapsed()) return; var map = this.getMap(); if (map && this._select && this._select.elt) { var ctx = e.context || ol.ext.getMapCanvas(this.getMap()).getContext('2d'); var ratio = e.frameState.pixelRatio; var pt = [ this._select.elt.offsetLeft - this._scrolldiv.scrollLeft + ol.ext.element.getStyle(this._select.elt, 'width') / 2, parseFloat(ol.ext.element.getStyle(this.element, 'top')) || this.getMap().getSize()[1] ]; var geom = this._select.feature.getGeometry().getFirstCoordinate(); geom = this.getMap().getPixelFromCoordinate(geom); ctx.save(); ctx.fillStyle = this.get('linkColor'); ctx.beginPath(); if (geom[0] > pt[0]) { ctx.moveTo((pt[0] - 5) * ratio, pt[1] * ratio); ctx.lineTo((pt[0] + 5) * ratio, (pt[1] + 5) * ratio); } else { ctx.moveTo((pt[0] - 5) * ratio, (pt[1] + 5) * ratio); ctx.lineTo((pt[0] + 5) * ratio, pt[1] * ratio); } ctx.lineTo(geom[0] * ratio, geom[1] * ratio); ctx.fill(); ctx.restore(); } } } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Geoportail isochrone Control. * @see https://geoservices.ign.fr/documentation/geoservices/isochrones.html * @constructor * @extends {ol.control.Control} * @fires isochrone * @fires error * @param {Object=} options * @param {string} options.className control class name * @param {string} [options.apiKey] Geoportail apo key * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {string | undefined} options.inputLabel label for the input, default none * @param {string | undefined} options.noCollapse prevent collapsing on input blur, default false * @param {number | undefined} options.typing a delay on each typing to start searching (ms) use -1 to prevent autocompletion, default 300. * @param {integer | undefined} options.minLength minimum length to start searching, default 1 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * @param {integer | undefined} options.maxHistory maximum number of items to display in history. Set -1 if you don't want history, default maxItems * @param {function} options.getTitle a function that takes a feature and return the name to display in the index. * @param {function} options.autocomplete a function that take a search string and callback function to send an array * * @param {string} options.exclusions Exclusion list separate with a comma 'Toll,Tunnel,Bridge' */ ol.control.IsochroneGeoportail = class olcontrolIsochroneGeoportail extends ol.control.Control { constructor(options) { if (!options) options = {}; if (options.typing == undefined) options.typing = 300; var classNames = (options.className ? options.className : '') + ' ol-isochrone ol-routing'; if (!options.target) classNames += ' ol-unselectable ol-control'; var element = ol.ext.element.create('DIV', { className: classNames }); if (!options.target) { var bt = ol.ext.element.create('BUTTON', { parent: element }); bt.addEventListener('click', function () { element.classList.toggle('ol-collapsed'); }); } // Inherits super({ element: element, target: options.target }); var self = this; this.set('iter', 1); var content = ol.ext.element.create('DIV', { className: 'content', parent: element }); // Search control this._addSearchCtrl(content, options); // Method buttons ol.ext.element.create('BUTTON', { className: 'ol-button ol-method-time selected', title: 'isochrone', parent: content }) .addEventListener('click', function () { this.setMethod('time'); }.bind(this)); ol.ext.element.create('I', { className: 'ol-button ol-method-distance', title: 'isodistance', parent: content }) .addEventListener('click', function () { this.setMethod('distance'); }.bind(this)); // Mode buttons ol.ext.element.create('I', { className: 'ol-button ol-car selected', title: 'by car', parent: content }) .addEventListener('click', function () { this.setMode('car'); }.bind(this)); ol.ext.element.create('I', { className: 'ol-button ol-pedestrian', title: 'by foot', parent: content }) .addEventListener('click', function () { this.setMode('pedestrian'); }.bind(this)); // Direction buttons ol.ext.element.create('I', { className: 'ol-button ol-direction-direct selected', title: 'direct', parent: content }) .addEventListener('click', function () { this.setDirection('direct'); }.bind(this)); ol.ext.element.create('I', { className: 'ol-button ol-direction-reverse', title: 'reverse', parent: content }) .addEventListener('click', function () { this.setDirection('reverse'); }.bind(this)); // Input var div = ol.ext.element.create('DIV', { className: 'ol-time', parent: content }); ol.ext.element.create('DIV', { html: 'isochrone:', parent: div }); ol.ext.element.create('INPUT', { type: 'number', parent: div, min: 0 }) .addEventListener('change', function () { self.set('hour', Number(this.value)); }); ol.ext.element.create('TEXT', { parent: div, html: 'h' }); ol.ext.element.create('INPUT', { type: 'number', parent: div, min: 0 }) .addEventListener('change', function () { self.set('minute', Number(this.value)); }); ol.ext.element.create('TEXT', { parent: div, html: 'mn' }); div = ol.ext.element.create('DIV', { className: 'ol-distance', parent: content }); ol.ext.element.create('DIV', { html: 'isodistance:', parent: div }); ol.ext.element.create('INPUT', { type: 'number', step: 'any', parent: div, min: 0 }) .addEventListener('change', function () { self.set('distance', Number(this.value)); }); ol.ext.element.create('TEXT', { parent: div, html: 'km' }); div = ol.ext.element.create('DIV', { className: 'ol-iter', parent: content }); ol.ext.element.create('DIV', { html: 'Iteration:', parent: div }); ol.ext.element.create('INPUT', { type: 'number', parent: div, value: 1, min: 1 }) .addEventListener('change', function () { self.set('iter', Number(this.value)); }); // OK button ol.ext.element.create('I', { className: 'ol-ok', html: 'ok', parent: content }) .addEventListener('click', function () { var val = 0; switch (this.get('method')) { case 'distance': { val = this.get('distance') * 1000; break; } default: { val = (this.get('hour') || 0) * 3600 + (this.get('minute') || 0) * 60; break; } } if (val && this.get('coordinate')) { this.search(this.get('coordinate'), val); } }.bind(this)); this.set('url', 'https://wxs.ign.fr/' + (options.apiKey || 'essentiels') + '/isochrone/isochrone.json'); this._ajax = new ol.ext.Ajax({ dataType: 'JSON', auth: options.auth }); this._ajax.on('success', this._success.bind(this)); this._ajax.on('error', this._error.bind(this)); // searching this._ajax.on('loadstart', function () { this.element.classList.add('ol-searching'); }.bind(this)); this._ajax.on('loadend', function () { this.element.classList.remove('ol-searching'); }.bind(this)); this.setMethod(options.method); } /** * 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._search.setMap(map); } /** Add a new search input * @private */ _addSearchCtrl(element, options) { var div = ol.ext.element.create("DIV", { parent: element }); var search = this._search = new ol.control.SearchGeoportail({ className: 'IGNF ol-collapsed', apiKey: options.apiKey, target: div }); search.on('select', function (e) { search.setInput(e.search.fulltext); this.set('coordinate', e.coordinate); }.bind(this)); search.on('change:input', function () { this.set('coordinate', false); }.bind(this)); } /** Set the travel method * @param [string] method The method (time or distance) */ setMethod(method) { 7; method = (/distance/.test(method) ? 'distance' : 'time'); this.element.querySelector(".ol-method-time").classList.remove("selected"); this.element.querySelector(".ol-method-distance").classList.remove("selected"); this.element.querySelector(".ol-method-" + method).classList.add("selected"); this.element.querySelector("div.ol-time").classList.remove("selected"); this.element.querySelector("div.ol-distance").classList.remove("selected"); this.element.querySelector("div.ol-" + method).classList.add("selected"); this.set('method', method); } /** Set mode * @param {string} mode The mode: 'car' or 'pedestrian', default 'car' */ setMode(mode) { this.set('mode', mode); this.element.querySelector(".ol-car").classList.remove("selected"); this.element.querySelector(".ol-pedestrian").classList.remove("selected"); this.element.querySelector(".ol-" + mode).classList.add("selected"); } /** Set direction * @param {string} direction The direction: 'direct' or 'reverse', default direct */ setDirection(direction) { this.set('direction', direction); this.element.querySelector(".ol-direction-direct").classList.remove("selected"); this.element.querySelector(".ol-direction-reverse").classList.remove("selected"); this.element.querySelector(".ol-direction-" + direction).classList.add("selected"); } /** Calculate an isochrone * @param {ol.coordinate} coord * @param {number|string} option A number as time (in second) or distance (in meter), depend on method propertie * or a string with a unit (s, mn, h for time or km, m) */ search(coord, option, iter) { var proj = this.getMap() ? this.getMap().getView().getProjection() : 'EPSG:3857'; var method = /distance/.test(this.get('method')) ? 'distance' : 'time'; if (typeof (option) === 'string') { var unit = option.replace(/([0-9|.]*)([a-z]*)$/, '$2'); method = 'time'; option = parseFloat(option); // convert unit switch (unit) { case 'mn': { option = option * 60; break; } case 'h': { option = option * 3600; break; } case 'm': { method = 'distance'; break; } case 'km': { method = 'distance'; option = option * 1000; break; } } } var dt = Math.round(option * (this.get('iter') - (iter || 0)) / this.get('iter')); if (typeof option === 'number') { // Send data var data = { 'gp-access-lib': '2.1.0', location: ol.proj.toLonLat(coord, proj), graphName: (this.get('mode') === 'pedestrian' ? 'Pieton' : 'Voiture'), exclusions: this.get('exclusions') || undefined, method: method, time: method === 'time' ? dt : undefined, distance: method === 'distance' ? dt : undefined, reverse: (this.get('direction') === 'reverse'), smoothing: this.get('smoothing') || true, holes: this.get('holes') || false }; this._ajax.send(this.get('url'), data, { coord: coord, option: option, data: data, iteration: (iter || 0) + 1 }); } } /** Trigger result * @private */ _success(e) { var proj = this.getMap() ? this.getMap().getView().getProjection() : 'EPSG:3857'; // Convert to features var format = new ol.format.WKT(); var evt = e.response; evt.feature = format.readFeature(evt.wktGeometry, { dataProjection: 'EPSG:4326', featureProjection: proj }); evt.feature.set('iteration', e.options.iteration); evt.feature.set('method', e.options.data.method); evt.feature.set(e.options.data.method, e.options.data[e.options.data.method]); delete evt.wktGeometry; evt.type = 'isochrone'; evt.iteration = e.options.iteration - 1; this.dispatchEvent(evt); if (e.options.iteration < this.get('iter')) { this.search(e.options.coord, e.options.option, e.options.iteration); } } /** Trigger error * @private */ _error() { this.dispatchEvent({ type: 'error' }); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * OpenLayers Layer Switcher Control. * * @constructor * @extends {ol.control.LayerSwitcher} * @param {Object=} options Control options. */ ol.control.LayerPopup = class olcontrolLayerPopup extends ol.control.LayerSwitcher { constructor(options) { options = options || {}; options.switcherClass = 'ol-layerswitcher-popup' + (options.switcherClass ? ' ' + options.switcherClass : ''); if (options.mouseover !== false) options.mouseover = true; super(options); } /** Disable overflow */ overflow() { } /** Render a list of layer * @param {elt} element to render * @layers {Array{ol.layer}} list of layer to show * @api stable */ drawList(ul, layers) { var self = this; var setVisibility = function (e) { e.preventDefault(); var l = self._getLayerForLI(this); self.switchLayerVisibility(l, layers); if (e.type === 'touchstart') self.element.classList.add('ol-collapsed'); }; layers.forEach(function (layer) { if (self.displayInLayerSwitcher(layer)) { var d = ol.ext.element.create('LI', { html: layer.get('title') || layer.get('name'), on: { 'click touchstart': setVisibility }, parent: ul }); self._setLayerForLI(d, layer); if (self.testLayerVisibility(layer)) d.classList.add('ol-layer-hidden'); if (layer.getVisible()) d.classList.add('ol-visible'); } }); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** LayerShop a layer switcher with special controls to handle operation on layers. * @fires select * @fires drawlist * @fires toggle * @fires reorder-start * @fires reorder-end * @fires layer:visible * @fires layer:opacity * * @constructor * @extends {ol.control.LayerSwitcher} * @param {Object=} options * @param {boolean} options.selection enable layer selection when click on the title * @param {function} options.displayInLayerSwitcher function that takes a layer and return a boolean if the layer is displayed in the switcher, default test the displayInLayerSwitcher layer attribute * @param {boolean} options.show_progress show a progress bar on tile layers, default false * @param {boolean} options.mouseover show the panel on mouseover, default false * @param {boolean} options.reordering allow layer reordering, default true * @param {boolean} options.trash add a trash button to delete the layer, default false * @param {function} options.oninfo callback on click on info button, if none no info button is shown DEPRECATED: use on(info) instead * @param {boolean} options.extent add an extent button to zoom to the extent of the layer * @param {function} options.onextent callback when click on extent, default fits view to extent * @param {number} options.drawDelay delay in ms to redraw the layer (usefull to prevent flickering when manipulating the layers) * @param {boolean} options.collapsed collapse the layerswitcher at beginning, default true * @param {ol.layer.Group} options.layerGroup a layer group to display in the switcher, default display all layers of the map * @param {boolean} options.noScroll prevent handle scrolling, default false * * Layers attributes that control the switcher * - allwaysOnTop {boolean} true to force layer stay on top of the others while reordering, default false * - displayInLayerSwitcher {boolean} display the layer in switcher, default true * - noSwitcherDelete {boolean} to prevent layer deletion (w. trash option = true), default false */ ol.control.LayerShop = class olcontrolLayerShop extends ol.control.LayerSwitcher { constructor(options) { options = options || {}; options.selection = true; options.noScroll = true; super(options); this.element.classList.add('ol-layer-shop'); // Control title (selected layer) var title = this.element.insertBefore(ol.ext.element.create('DIV', { className: 'ol-title-bar' }), this.getPanel()); this.on('select', function (e) { title.innerText = e.layer ? e.layer.get('title') : ''; this.element.setAttribute('data-layerClass', this.getLayerClass(e.layer)); }.bind(this)); // Top/bottom bar this._topbar = this.element.insertBefore(ol.ext.element.create('DIV', { className: 'ol-bar ol-top-bar' }), this.getPanel()); this._bottombar = ol.ext.element.create('DIV', { className: 'ol-bar ol-bottom-bar', parent: this.element }); this._controls = []; } /** Set the map instance the control is associated with. * @param {_ol_Map_} map The map instance. */ setMap(map) { if (this.getMap()) { // Remove map controls this._controls.forEach(function (c) { this.getMap().removeControl(c); }.bind(this)); } super.setMap(map); if (map) { // Select first layer this.selectLayer(); // Remove a layer this._listener.removeLayer = map.getLayers().on('remove', function (e) { // Select first layer if (e.element === this.getSelection()) { this.selectLayer(); } }.bind(this)); // Add controls this._controls.forEach(function (c) { this.getMap().addControl(c); }.bind(this)); } } /** Get the bar element (to add new element in it) * @param {string} [position='top'] bar position bottom or top, default top * @returns {Element} */ getBarElement(position) { return position === 'bottom' ? this._bottombar : this._topbar; } /** Add a control to the panel * @param {ol.control.Control} control * @param {string} [position='top'] bar position bottom or top, default top */ addControl(control, position) { this._controls.push(control); control.setTarget(position === 'bottom' ? this._bottombar : this._topbar); if (this.getMap()) { this.getMap().addControl(control); } } } /* 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). */ /** * @classdesc OpenLayers Layer Switcher Control. * @require layer.getPreview * * @constructor * @extends {ol.control.LayerSwitcher} * @param {Object=} options Control options. */ ol.control.LayerSwitcherImage = class olcontrolLayerSwitcherImage extends ol.control.LayerSwitcher { constructor(options) { options = options || {}; options.switcherClass = ((options.switcherClass || '') + ' ol-layerswitcher-image').trim(); options.mouseover = (options.mouseover !== false); super(options); } /** Render a list of layer * @param {elt} element to render * @layers {Array{ol.layer}} list of layer to show * @api stable */ drawList(ul, layers) { var self = this; var setVisibility = function (e) { e.preventDefault(); var l = self._getLayerForLI(this); self.switchLayerVisibility(l, layers); if (e.type == "touchstart") self.element.classList.add("ol-collapsed"); }; ol.ext.element.setStyle(ul, { height: 'auto' }); layers.forEach(function (layer) { if (self.displayInLayerSwitcher(layer)) { var preview = layer.getPreview ? layer.getPreview() : ["none"]; var d = ol.ext.element.create('LI', { className: 'ol-imgcontainer' + (layer.getVisible() ? ' ol-visible' : ''), on: { 'touchstart click': setVisibility }, parent: ul }); self._setLayerForLI(d, layer); preview.forEach(function (img) { ol.ext.element.create('IMG', { src: img, parent: d }); }); ol.ext.element.create('p', { html: layer.get("title") || layer.get("name"), parent: d }); if (self.testLayerVisibility(layer)) d.classList.add('ol-layer-hidden'); } }); } /** Disable overflow */ overflow() { } } // eslint-disable-next-line no-unused-vars /** Create a legend for styles * @constructor * @extends {ol.control.CanvasBase} * @fires select * @param {*} options * @param {String} options.className class of the control * @param {String} [options.title="legend"] control title * @param {String} [options.emptyTitle="no legend"] control title when legend is empty * @param {ol.legend.Legend} options.legend * @param {boolean | undefined} options.collapsed Specify if legend should be collapsed at startup. Default is true. * @param {boolean | undefined} options.collapsible Specify if legend can be collapsed, default true. * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. */ ol.control.Legend = class olcontrolLegend extends ol.control.CanvasBase { constructor(options) { options = options || {}; var element = document.createElement('div'); super({ element: element, target: options.target }); if (options.target) { element.className = options.className || 'ol-legend'; } else { element.className = (options.className || 'ol-legend') + ' ol-unselectable ol-control' + (options.collapsible === false ? ' ol-uncollapsible' : ' ol-collapsed'); // Show on click var button = document.createElement('button'); button.setAttribute('type', 'button'); button.addEventListener('click', function () { this.toggle(); }.bind(this)); element.appendChild(button); // Hide on click button = document.createElement('button'); button.setAttribute('type', 'button'); button.className = 'ol-closebox'; button.addEventListener('click', function () { this.toggle(); }.bind(this)); element.appendChild(button); } // The legend this._legend = options.legend; this._legend.getCanvas().className = 'ol-legendImg'; // Legend element element.appendChild(this._legend.getCanvas()); element.appendChild(this._legend.getListElement()); if (options.collapsible !== false && options.collapsed === false) { this.show(); } // Select item on legend this._legend.on('select', function (e) { this.dispatchEvent(e); }.bind(this)); // Refresh legend this._legend.on('refresh', function () { if (this._onCanvas && this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } }.bind(this)); // Legend has items this._legend.on('items', function (e) { if (e.nb) { this.element.classList.remove('ol-empty'); this.element.title = options.title || 'legend'; } else { this.element.classList.add('ol-empty'); this.element.title = options.emptyTitle || 'no legend'; } this.dispatchEvent(e) }.bind(this)); } /** Get the legend associated with the control * @returns {ol.legend.Legend} */ getLegend() { return this._legend; } /** Draw control on canvas * @param {boolean} b draw on canvas. */ setCanvas(b) { this._onCanvas = b; this.element.style.visibility = b ? "hidden" : "visible"; if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Is control on canvas * @returns {boolean} */ onCanvas() { return !!this._onCanvas; } /** Draw legend on canvas * @private */ _draw(e) { if (this._onCanvas && !this.element.classList.contains('ol-collapsed')) { var canvas = this._legend.getCanvas(); var ctx = this.getContext(e); var h = ctx.canvas.height - canvas.height; ctx.save(); ctx.rect(0, h, canvas.width, canvas.height); var col = '#fff'; if (this._legend.getTextStyle().getBackgroundFill()) { col = ol.color.asString(this._legend.getTextStyle().getBackgroundFill().getColor()); } ctx.fillStyle = ctx.strokeStyle = col; ctx.lineWidth = 10; ctx.lineJoin = 'round'; ctx.stroke(); ctx.clearRect(0, h, canvas.width, canvas.height); ctx.fill(); ctx.drawImage(canvas, 0, h); ctx.restore(); } } /** Show control */ show() { if (this.element.classList.contains('ol-collapsed')) { this.element.classList.remove('ol-collapsed'); this.dispatchEvent({ type: 'change:collapse', collapsed: false }); if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } } /** Hide control */ hide() { if (!this.element.classList.contains('ol-collapsed')) { this.element.classList.add('ol-collapsed'); this.dispatchEvent({ type: 'change:collapse', collapsed: true }); if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } } /** Show/hide control * @returns {boolean} */ collapse(b) { if (b === false) this.show(); else this.hide(); } /** Is control collapsed * @returns {boolean} */ isCollapsed() { return (this.element.classList.contains('ol-collapsed')); } /** Toggle control */ toggle() { this.element.classList.toggle('ol-collapsed'); this.dispatchEvent({ type: 'change:collapse', collapsed: this.element.classList.contains('ol-collapsed') }); if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A control to jump from one zone to another. * @constructor * @fires select * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {string} options.className class name * @param {Array} options.zone an array of zone: { name, extent (in EPSG:4326) } * @param {ol.layer.Layer|function} options.layer layer to display in the control or a function that takes a zone and returns a layer to add to the control * @param {ol.ProjectionLike} options.projection projection of the control, Default is EPSG:3857 (Spherical Mercator). * @param {bolean} options.centerOnClick center on click when a zone is clicked (or listen to 'select' event to do something), default true */ ol.control.MapZone = class olcontrolMapZone extends ol.control.Control { constructor(options) { options = options || {} var element = element = ol.ext.element.create('DIV', { className: options.className || 'ol-mapzone' }) super({ element: element, target: options.target }) if (!options.target) { ['ol-unselectable', 'ol-control', 'ol-collapsed'].forEach(function(c) { element.classList.add(c) }) var bt = ol.ext.element.create('BUTTON', { type: 'button', on: { 'click': function () { element.classList.toggle("ol-collapsed") maps.forEach(function (m) { m.updateSize() }) }.bind(this) }, parent: element }) ol.ext.element.create('I', { parent: bt }) } this.set('centerOnClick', options.centerOnClick) // Create maps var maps = this._maps = [] this._projection = options.projection this._layer = options.layer options.zones.forEach(this.addZone.bind(this)) // Refresh the maps setTimeout(function () { maps.forEach(function (m) { m.updateSize() }) }) } /** Collapse the control * @param {boolean} b */ setCollapsed(b) { if (b) { this.element.classList.remove('ol-collapsed') // Force map rendering this.getMaps().forEach(function (m) { m.updateSize() }) } else { this.element.classList.add('ol-collapsed') } } /** Show the control * @param {boolean} b */ setVisible(b) { this.setCollapsed(!b); } /** Get control collapsed * @return {boolean} */ getCollapsed() { return this.element.classList.contains('ol-collapsed') } /** Get associated maps * @return {ol.Map} */ getMaps() { return this._maps } /** Get nb zone */ getLength() { return this._maps.length } /** Add a new zone to the control * @param {Object} z * @param {string} title * @param {ol.extent} extent if map is not defined * @param {ol.Map} map if map is defined use the map extent * @param {ol.layer.Layer} [layer] layer of the zone, default use default control layer */ addZone(z) { var view = new ol.View({ zoom: 6, center: [0, 0], projection: this._projection }) var extent if (z.map) { extent = ol.proj.transformExtent(z.map.getView().calculateExtent(), z.map.getView().getProjection(), view.getProjection()) } else { extent = ol.proj.transformExtent(z.extent, 'EPSG:4326', view.getProjection()) } // console.log(extent, z.extent) var div = ol.ext.element.create('DIV', { className: 'ol-mapzonezone', parent: this.element, click: function () { // Get index var index = -1 this._maps.forEach(function (m, i) { if (m.get('zone') === z) { index = i } }) this.dispatchEvent({ type: 'select', zone: z, index: index, coordinate: ol.extent.getCenter(extent), extent: extent }) if (this.get('centerOnClick') !== false) { this.getMap().getView().fit(extent) } this.setVisible(false) }.bind(this) }) var layer if (z.layer) { layer = z.layer } else if (typeof (this._layer) === 'function') { layer = this._layer(z) } else { // Try to clone the layer layer = new this._layer.constructor({ source: this._layer.getSource() }) } var map = new ol.Map({ target: div, view: view, controls: [], interactions: [], layers: [layer] }) map.set('zone', z) this._maps.push(map) view.fit(extent) // Name ol.ext.element.create('P', { html: z.title, parent: div }) } /** Remove a zone from the control * @param {number} index */ removeZone(index) { var z = this.element.querySelectorAll('.ol-mapzonezone')[index] if (z) { z.remove() this._maps.splice(index, 1) } } } /** Pre-defined zones */ ol.control.MapZone.zones = {}; /** French overseas departments */ ol.control.MapZone.zones.DOM = [{ "title": "Guadeloupe", "extent": [ -61.898594315312444, 15.75623038647845, -60.957887532935324, 16.575317670979473 ] },{ "title": "Guyane", "extent": [ -54.72525931072715, 2.1603763430019, -51.528236062921344, 5.7984307809552575 ] },{ "title": "Martinique", "extent": [ -61.257556528564756, 14.387506317407514, -60.76934912110432, 14.895067461729951 ] },{ "title": "Mayotte", "extent": [ 44.959844536967815, -13.01674138212816, 45.35328866510648, -12.65521942207829 ] },{ "title": "La réunion", "extent": [ 55.17059012967656, -21.407680069231688, 55.88195702001797, -20.85560221637526 ] }]; /** French overseas territories */ ol.control.MapZone.zones.TOM = [{ "title": "Polynésie Française", "extent": [ 206.23664226630862, -22.189040615809787, 221.85920743981987, -10.835039595040698 ] },{ "title": "Nouvelle Calédonie", "extent": [ 163.76420580160925, -22.581641092751838, 167.66984709498706, -19.816411635668445 ] },{ "title": "St-Pierre et Miquelon", "extent": [ -56.453698765748676, 46.74449858188555, -56.0980198121544, 47.14669874229787 ] },{ "title": "Wallis et Futuna", "extent": [ 181.7588623143665, -14.7341169873267, 183.95612353301715, -13.134720799175085 ] },{ "title": "St-Martin St-Barthélemy", "extent": [ -63.1726389501678, 17.806097291313506, -62.7606535945649, 18.13267688837938 ] }]; /** French overseas departments and territories */ ol.control.MapZone.zones.DOMTOM = [{ title: 'Métropole', extent: [ -5.318421740712579, 41.16082274292913, 9.73284186155716, 51.21957336557702 ] }].concat(ol.control.MapZone.zones.DOM, ol.control.MapZone.zones.TOM); /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Control overlay for OL3 * The overlay control is a control that display an overlay over the map * * @constructor * @extends {ol.control.Control} * @fire change:visible * @param {Object=} options Control options. * @param {string} className class of the control * @param {boolean} options.closeBox add a close button * @param {boolean} options.hideOnClick close dialog when click */ ol.control.Notification = class olcontrolNotification extends ol.control.Control { constructor(options) { options = options || {}; var element = document.createElement('DIV'); super({ element: element, target: options.target }); this.contentElement = ol.ext.element.create('DIV', { click: function () { if (this.get('hideOnClick')) this.hide(); }.bind(this), parent: element }); var classNames = (options.className || "") + " ol-notification"; if (!options.target) { classNames += " ol-unselectable ol-control ol-collapsed"; } element.setAttribute('class', classNames); this.set('closeBox', options.closeBox); this.set('hideOnClick', options.hideOnClick); } /** * Display a notification on the map * @param {string|node|undefined} what the notification to show, default get the last one * @param {number} [duration=3000] duration in ms, if -1 never hide */ show(what, duration) { var self = this; var elt = this.element; if (what) { if (what instanceof Node) { this.contentElement.innerHTML = ''; this.contentElement.appendChild(what); } else { this.contentElement.innerHTML = what; } if (this.get('closeBox')) { this.contentElement.classList.add('ol-close'); ol.ext.element.create('SPAN', { className: 'closeBox', click: function () { this.hide(); }.bind(this), parent: this.contentElement }); } else { this.contentElement.classList.remove('ol-close'); } } if (this._listener) { clearTimeout(this._listener); this._listener = null; } elt.classList.add('ol-collapsed'); this._listener = setTimeout(function () { elt.classList.remove('ol-collapsed'); if (!duration || duration >= 0) { self._listener = setTimeout(function () { elt.classList.add('ol-collapsed'); self._listener = null; }, duration || 3000); } else { self._listener = null; } }, 100); } /** * Remove a notification on the map */ hide() { if (this._listener) { clearTimeout(this._listener); this._listener = null; } this.element.classList.add('ol-collapsed'); } /** * Toggle a notification on the map * @param {number} [duration=3000] duration in ms */ toggle(duration) { if (this.element.classList.contains('ol-collapsed')) { this.show(null, duration); } else { this.hide(); } } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Control overlay for OL3 * The overlay control is a control that display an overlay over the map * * @constructor * @extends {ol.control.Control} * @fire change:visible * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {String|Element} options.content * @param {bool} options.hideOnClick hide the control on click, default false * @param {bool} options.closeBox add a closeBox to the control, default false */ ol.control.Overlay = class olcontrolOverlay extends ol.control.Control { constructor(options) { options = options || {}; var element = ol.ext.element.create('DIV', { className: 'ol-unselectable ol-overlay ' + (options.className || ''), html: options.content }); super({ element: element, target: options.target }); var self = this; if (options.hideOnClick) { element.addEventListener('click', function () { self.hide(); }); } this.set('closeBox', options.closeBox); this._timeout = false; this.setContent(options.content); } /** Set the content of the overlay * @param {string|Element} html the html to display in the control */ setContent(html) { var self = this; if (html) { var elt = this.element; if (html instanceof Element) { elt.innerHTML = ''; elt.appendChild(html); } else if (html !== undefined) elt.innerHTML = html; if (this.get("closeBox")) { var cb = document.createElement("div"); cb.classList.add("ol-closebox"); cb.addEventListener("click", function () { self.hide(); }); elt.insertBefore(cb, elt.firstChild); } } } /** Set the control visibility * @param {string|Element} html the html to display in the control * @param {ol.coordinate} coord coordinate of the top left corner of the control to start from */ show(html, coord) { var self = this; var elt = this.element; elt.style.display = 'block'; if (coord) { this.center_ = this.getMap().getPixelFromCoordinate(coord); elt.style.top = this.center_[1] + 'px'; elt.style.left = this.center_[0] + 'px'; } else { //TODO: Do fix from hkollmann pull request this.center_ = false; elt.style.top = ""; elt.style.left = ""; } if (html) this.setContent(html); if (this._timeout) clearTimeout(this._timeout); this._timeout = setTimeout(function () { elt.classList.add("ol-visible"); elt.style.top = ""; elt.style.left = ""; self.dispatchEvent({ type: 'change:visible', visible: true, element: self.element }); }, 10); } /** Show an image * @param {string} src image url * @param {*} options * @param {string} options.title * @param {ol.coordinate} coordinate */ showImage(src, options) { options = options || {}; var content = ol.ext.element.create('DIV', { className: 'ol-fullscreen-image' }); ol.ext.element.create('IMG', { src: src, parent: content }); if (options.title) { content.classList.add('ol-has-title'); ol.ext.element.create('P', { html: options.title, parent: content }); } this.show(content, options.coordinate); } /** Set the control visibility hidden */ hide() { var elt = this.element; this.element.classList.remove("ol-visible"); if (this.center_) { elt.style.top = this.center_[1] + 'px'; elt.style.left = this.center_[0] + 'px'; this.center_ = false; } if (this._timeout) clearTimeout(this._timeout); this._timeout = setTimeout(function () { elt.style.display = 'none'; }, 500); this.dispatchEvent({ type: 'change:visible', visible: false, element: this.element }); } /** Toggle control visibility */ toggle() { if (this.getVisible()) this.hide(); else this.show(); } /** Get the control visibility * @return {boolean} b */ getVisible() { return ol.ext.element.getStyle(this.element, 'display') !== 'none'; } /** Change class name * @param {String} className a class name or a list of class names separated by a space */ setClass(className) { var vis = this.element.classList.contains('ol-visible'); this.element.className = ('ol-unselectable ol-overlay ' + (vis ? 'ol-visible ' : '') + className).trim(); } } /* 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). */ /** * OpenLayers 3 Layer Overview Control. * The overview can rotate with map. * Zoom levels are configurable. * Click on the overview will center the map. * Change width/height of the overview trough css. * * @constructor * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {ol.ProjectionLike} options.projection The projection. Default is EPSG:3857 (Spherical Mercator). * @param {Number} options.minZoom default 0 * @param {Number} options.maxZoom default 18 * @param {boolean} options.rotation enable rotation, default false * @param {top|bottom-left|right} options.align position * @param {Array} options.layers list of layers * @param {ol.style.Style | Array. | undefined} options.style style to draw the map extent on the overveiw * @param {bool|elastic} options.panAnimation use animation to center map on click, default true */ ol.control.Overview = class olcontrolOverview extends ol.control.Control { constructor(options) { options = options || {} var element = document.createElement("div"); super({ element: element, target: options.target }) var self = this // API this.minZoom = options.minZoom || 0 this.maxZoom = options.maxZoom || 18 this.rotation = options.rotation if (options.target) { this.panel_ = options.target } else { element.classList.add('ol-overview', 'ol-unselectable', 'ol-control', 'ol-collapsed') if (/top/.test(options.align)) element.classList.add('ol-control-top') if (/right/.test(options.align)) element.classList.add('ol-control-right') var button = document.createElement("button") button.setAttribute('type', 'button') button.addEventListener("touchstart", function (e) { self.toggleMap(); e.preventDefault() }) button.addEventListener("click", function () { self.toggleMap() }) element.appendChild(button) this.panel_ = document.createElement("div") this.panel_.classList.add("panel") element.appendChild(this.panel_) } // Create a overview map this.ovmap_ = new ol.Map({ controls: new ol.Collection(), interactions: new ol.Collection(), target: this.panel_, view: new ol.View({ zoom: 2, center: [0, 0], projection: options.projection }), layers: options.layers }) this.oview_ = this.ovmap_.getView() // Cache extent this.extentLayer = new ol.layer.Vector({ name: 'Cache extent', source: new ol.source.Vector(), style: options.style || [new ol.style.Style({ image: new ol.style.Circle({ fill: new ol.style.Fill({ color: 'rgba(255,0,0, 1)' }), stroke: new ol.style.Stroke({ width: 7, color: 'rgba(255,0,0, 0.8)' }), radius: 5 }), stroke: new ol.style.Stroke({ width: 5, color: "rgba(255,0,0,0.8)" }) } )] }) this.ovmap_.addLayer(this.extentLayer) /** Elastic bounce * @param {Int} bounce number of bounce * @param {Number} amplitude amplitude of the bounce [0,1] * @return {Number} * / var bounceFn = function (bounce, amplitude){ var a = (2*bounce+1) * Math.PI/2; var b = amplitude>0 ? -1/amplitude : -100; var c = - Math.cos(a) * Math.pow(2, b); return function(t) { t = 1-Math.cos(t*Math.PI/2); return 1 + Math.abs( Math.cos(a*t) ) * Math.pow(2, b*t) + c*t; } } /** Elastic bounce * @param {Int} bounce number of bounce * @param {Number} amplitude amplitude of the bounce [0,1] * @return {Number} */ var elasticFn = function (bounce, amplitude) { var a = 3 * bounce * Math.PI / 2 var b = amplitude > 0 ? -1 / amplitude : -100 var c = Math.cos(a) * Math.pow(2, b) return function (t) { t = 1 - Math.cos(t * Math.PI / 2) return 1 - Math.cos(a * t) * Math.pow(2, b * t) + c * t } } // Click on the preview center the map this.ovmap_.addInteraction(new ol.interaction.Pointer({ handleDownEvent: function (evt) { if (options.panAnimation !== false) { if (options.panAnimation == "elastic" || options.elasticPan) { self.getMap().getView().animate({ center: evt.coordinate, easing: elasticFn(2, 0.3), duration: 1000 }) } else { self.getMap().getView().animate({ center: evt.coordinate, duration: 300 }) } } else self.getMap().getView().setCenter(evt.coordinate) return false } })) } /** Get overview map * @return {ol.Map} */ getOverviewMap() { return this.ovmap_ } /** Toggle overview map */ toggleMap() { this.element.classList.toggle("ol-collapsed") this.ovmap_.updateSize() this.setView() } /** Set overview map position * @param {top|bottom-left|right} */ setPosition(align) { if (/top/.test(align)) this.element.classList.add("ol-control-top") else this.element.classList.remove("ol-control-top") if (/right/.test(align)) this.element.classList.add("ol-control-right") else this.element.classList.remove("ol-control-right") } /** * Set the map instance the control associated with. * @param {ol.Map} map The map instance. */ setMap(map) { if (this._listener) { for (var i in this._listener) { ol.Observable.unByKey(this._listener[i]) } } this._listener = {} super.setMap(map) if (map) { this._listener.map = map.on('change:view', function () { if (this._listener.view) ol.Observable.unByKey(this._listener.view) if (map.getView()) { this._listener.view = map.getView().on('propertychange', this.setView.bind(this)) this.setView() } }.bind(this)) this._listener.view = map.getView().on('propertychange', this.setView.bind(this)) this.setView() } } /** Calculate the extent of the map and draw it on the overview */ calcExtent_(extent) { var map = this.getMap() if (!map) return var source = this.extentLayer.getSource() source.clear() var f = new ol.Feature() var size = map.getSize() var resolution = map.getView().getResolution() var rotation = map.getView().getRotation() var center = map.getView().getCenter() if (!resolution) return var dx = resolution * size[0] / 2 var dy = resolution * size[1] / 2 var res2 = this.oview_.getResolution() if (dx / res2 > 5 || dy / res2 > 5) { var cos = Math.cos(rotation) var sin = Math.sin(rotation) var i, x, y extent = [[-dx, -dy], [-dx, dy], [dx, dy], [dx, -dy]] for (i = 0; i < 4; ++i) { x = extent[i][0] y = extent[i][1] extent[i][0] = center[0] + x * cos - y * sin extent[i][1] = center[1] + x * sin + y * cos } f.setGeometry(new ol.geom.Polygon([extent])) } else { f.setGeometry(new ol.geom.Point(center)) } source.addFeature(f) } /** * @private */ setView(e) { if (!e) { // refresh all this.setView({ key: 'rotation' }) this.setView({ key: 'resolution' }) this.setView({ key: 'center' }) return } // Set the view params switch (e.key) { case 'rotation': { if (this.rotation) this.oview_.setRotation(this.getMap().getView().getRotation()) else if (this.oview_.getRotation()) this.oview_.setRotation(0) break } case 'center': { var mapExtent = this.getMap().getView().calculateExtent(this.getMap().getSize()) var extent = this.oview_.calculateExtent(this.ovmap_.getSize()) if (mapExtent[0] < extent[0] || mapExtent[1] < extent[1] || mapExtent[2] > extent[2] || mapExtent[3] > extent[3]) { this.oview_.setCenter(this.getMap().getView().getCenter()) } break } case 'resolution': { //var z = Math.round(this.getMap().getView().getZoom()/2)*2-4; var z = Math.round(this.oview_.getZoomForResolution(this.getMap().getView().getResolution()) / 2) * 2 - 4 z = Math.min(this.maxZoom, Math.max(this.minZoom, z)) this.oview_.setZoom(z) break } default: break } this.calcExtent_() } } /* Copyright (c) 2015-2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Set an hyperlink that will return the user to the current map view. * Just add a `permalink`property to layers to be handled by the control (and added in the url). * The layer's permalink property is used to name the layer in the url. * The control must be added after all layer are inserted in the map to take them into acount. * * @constructor * @extends {ol.control.Control} * @param {Object=} options * @param {boolean} options.urlReplace replace url or not, default true * @param {boolean|string} [options.localStorage=false] save current map view in localStorage, if 'position' only store map position * @param {boolean} options.geohash use geohash instead of lonlat, default false * @param {integer} options.fixed number of digit in coords, default 6 * @param {boolean} options.anchor use "#" instead of "?" in href * @param {boolean} options.visible hide the button on the map, default true * @param {boolean} options.hidden hide the button on the map, default false DEPRECATED: use visible instead * @param {function} options.onclick a function called when control is clicked */ ol.control.Permalink = class olcontrolPermalink extends ol.control.Control { constructor(opt_options) { var options = opt_options || {} var element = document.createElement('div') super({ element: element, target: options.target }) var self = this var button = document.createElement('button') ol.ext.element.create('I', { parent: button }) this.replaceState_ = (options.urlReplace !== false) this.fixed_ = options.fixed || 6 this.hash_ = options.anchor ? "#" : "?" this._localStorage = options.localStorage if (!this._localStorage) { try { localStorage.removeItem('ol@permalink') } catch (e) { console.warn('Failed to access localStorage...')} } function linkto() { if (typeof (options.onclick) == 'function'){ options.onclick(self.getLink()) } else { self.setUrlReplace(!self.replaceState_) } } button.addEventListener('click', linkto, false) button.addEventListener('touchstart', linkto, false) element.className = (options.className || "ol-permalink") + " ol-unselectable ol-control" element.appendChild(button) if (options.hidden || options.visible === false) ol.ext.element.hide(element) this.set('geohash', options.geohash) this.set('initial', false) this.on('change', this.viewChange_.bind(this)) // Save search params this.search_ = {} var init = {} var hash = document.location.hash || document.location.search || '' // console.log('hash', hash) if (this.replaceState_ && !hash && this._localStorage) { try { hash = localStorage['ol@permalink'] } catch (e) { console.warn('Failed to access localStorage...')} } if (hash) { hash = hash.replace(/(^#|^\?)/, "").split("&") for (var i = 0; i < hash.length; i++) { var t = hash[i].split("=") switch (t[0]) { case 'lon': case 'lat': case 'z': case 'r': { init[t[0]] = t[1] break } case 'gh': { var ghash = t[1].split('-') var lonlat = ol.geohash.toLonLat(ghash[0]) init.lon = lonlat[0] init.lat = lonlat[1] init.z = ghash[1] break } case 'l': break default: this.search_[t[0]] = t[1] } } } if (init.hasOwnProperty('lon')) { this.set('initial', init) } // Decode permalink if (this.replaceState_) this.setPosition() } /** * Get the initial position passed by the url */ getInitialPosition() { return this.get('initial') } /** * Set the map instance the control associated with. * @param {ol.Map} map The map instance. */ setMap(map) { if (this._listener) { ol.Observable.unByKey(this._listener.change) ol.Observable.unByKey(this._listener.moveend) } this._listener = null super.setMap.call(this, map) // Get change if (map) { this._listener = { change: map.getLayerGroup().on('change', this.layerChange_.bind(this)), moveend: map.on('moveend', this.viewChange_.bind(this)) } this.setPosition() } } /** Get layer given a permalink name (permalink propertie in the layer) * @param {string} the permalink to search for * @param {Array|undefined} an array of layer to search in * @return {ol.layer|false} */ getLayerByLink(id, layers) { if (!layers && this.getMap()) layers = this.getMap().getLayers().getArray() for (var i = 0; i < layers.length; i++) { if (layers[i].get('permalink') == id) return layers[i] // Layer Group if (layers[i].getLayers) { var li = this.getLayerByLink(id, layers[i].getLayers().getArray()) if (li) return li } } return false } /** Set coordinates as geohash * @param {boolean} */ setGeohash(b) { this.set('geohash', b) this.setUrlParam() } /** Set map position according to the current link * @param {boolean} [force=false] if true set the position even if urlReplace is disabled */ setPosition(force) { var map = this.getMap() if (!map) return var hash = (this.replaceState_ || force) ? document.location.hash || document.location.search : '' if (!hash && this._localStorage) { try { hash = localStorage['ol@permalink'] } catch (e) { console.warn('Failed to access localStorage...')} } if (!hash) return var i, t, param = {} hash = hash.replace(/(^#|^\?)/, "").split("&") for (i = 0; i < hash.length; i++) { t = hash[i].split("=") param[t[0]] = t[1] } if (param.gh) { var ghash = param.gh.split('-') var lonlat = ol.geohash.toLonLat(ghash[0]) param.lon = lonlat[0] param.lat = lonlat[1] param.z = ghash[1] } var c = ol.proj.transform([Number(param.lon), Number(param.lat)], 'EPSG:4326', map.getView().getProjection()) if (c[0] && c[1]) map.getView().setCenter(c) if (param.z) map.getView().setZoom(Number(param.z)) if (param.r) map.getView().setRotation(Number(param.r)) // Reset layers visibility function resetLayers(layers) { if (!layers) layers = map.getLayers().getArray() for (var i = 0; i < layers.length; i++) { if (layers[i].get('permalink')) { layers[i].setVisible(false) // console.log("hide "+layers[i].get('permalink')); } if (layers[i].getLayers) { resetLayers(layers[i].getLayers().getArray()) } } } if (param.l) { resetLayers() var l = param.l.split("|") for (i = 0; i < l.length; i++) { t = l[i].split(":") var li = this.getLayerByLink(t[0]) var op = Number(t[1]) if (li) { li.setOpacity(op) li.setVisible(true) } } } } /** * Get the parameters added to the url. The object can be changed to add new values. * @return {Object} a key value object added to the url as &key=value * @api stable */ getUrlParams() { return this.search_ } /** * Set a parameter to the url. * @param {string} key the key parameter * @param {string|undefined} value the parameter's value, if undefined or empty string remove the parameter * @api stable */ setUrlParam(key, value) { if (key) { if (value === undefined || value === '') delete (this.search_[encodeURIComponent(key)]) else this.search_[encodeURIComponent(key)] = encodeURIComponent(value) } this.viewChange_() } /** * Get a parameter url. * @param {string} key the key parameter * @return {string} the parameter's value or empty string if not set * @api stable */ getUrlParam(key) { return decodeURIComponent(this.search_[encodeURIComponent(key)] || '') } /** * Has a parameter url. * @param {string} key the key parameter * @return {boolean} * @api stable */ hasUrlParam(key) { return this.search_.hasOwnProperty(encodeURIComponent(key)) } /** Get the permalink * @param {boolean|string} [search=false] false: return full link | true: return the search string only | 'position': return position string * @return {permalink} */ getLink(search) { var map = this.getMap() var c = ol.proj.transform(map.getView().getCenter(), map.getView().getProjection(), 'EPSG:4326') var z = Math.round(map.getView().getZoom() * 10) / 10 var r = map.getView().getRotation() var l = this.layerStr_ // Change anchor var anchor = (r ? "&r=" + (Math.round(r * 10000) / 10000) : "") + (l ? "&l=" + l : "") if (this.get('geohash')) { var ghash = ol.geohash.fromLonLat(c, 8) anchor = "gh=" + ghash + '-' + z + anchor } else { anchor = "lon=" + c[0].toFixed(this.fixed_) + "&lat=" + c[1].toFixed(this.fixed_) + "&z=" + z + anchor } if (search === 'position') return anchor // Add other params for (var i in this.search_) { anchor += "&" + i + (typeof (this.search_[i]) !== 'undefined' ? "=" + this.search_[i] : '') } if (search) return anchor //return document.location.origin+document.location.pathname+this.hash_+anchor; return document.location.protocol + "//" + document.location.host + document.location.pathname + this.hash_ + anchor } /** Check if urlreplace is on * @return {boolean} */ getUrlReplace() { return this.replaceState_ } /** Enable / disable url replacement (replaceSate) * @param {bool} */ setUrlReplace(replace) { try { this.replaceState_ = replace if (!replace) { var s = "" for (var i in this.search_) { s += (s == "" ? "?" : "&") + i + (typeof (this.search_[i]) !== 'undefined' ? "=" + this.search_[i] : '') } window.history.replaceState(null, null, document.location.origin + document.location.pathname + s) } else window.history.replaceState(null, null, this.getLink()) } catch (e) { /* ok */ } /* if (this._localStorage) { localStorage['ol@permalink'] = this.getLink(true); } */ } /** * On view change refresh link * @param {ol.event} The map instance. * @private */ viewChange_() { try { if (this.replaceState_) window.history.replaceState(null, null, this.getLink()) } catch (e) { /* ok */ } if (this._localStorage) { try { localStorage['ol@permalink'] = this.getLink(this._localStorage) } catch (e) { console.warn('Failed to access localStorage...')} } } /** * Layer change refresh link * @private */ layerChange_() { // Prevent multiple change at the same time if (this._tout) { clearTimeout(this._tout) this._tout = null } this._tout = setTimeout(function () { this._tout = null // Get layers var l = "" function getLayers(layers) { for (var i = 0; i < layers.length; i++) { if (layers[i].getVisible() && layers[i].get("permalink")) { if (l) l += "|" l += layers[i].get("permalink") + ":" + layers[i].get("opacity") } // Layer Group if (layers[i].getLayers) getLayers(layers[i].getLayers().getArray()) } } getLayers(this.getMap().getLayers().getArray()) this.layerStr_ = l this.viewChange_() }.bind(this), 200) } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Print control to get an image of the map * @constructor * @fire print * @fire error * @fire printing * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {String} options.title button title * @param {string} options.imageType A string indicating the image format, default image/jpeg * @param {number} options.quality Number between 0 and 1 indicating the image quality to use for image formats that use lossy compression such as image/jpeg and image/webp * @param {string} options.orientation Page orientation (landscape/portrait), default guest the best one * @param {boolean} options.immediate force print even if render is not complete, default false */ ol.control.Print = class olcontrolPrint extends ol.control.Control { constructor(options) { options = options || {}; var element = ol.ext.element.create('DIV', { className: (options.className || 'ol-print') }); super({ element: element, target: options.target }); if (!options.target) { element.classList.add('ol-unselectable', 'ol-control'); ol.ext.element.create('BUTTON', { type: 'button', title: options.title || 'Print', click: function () { this.print(); }.bind(this), parent: element }); } this.set('immediate', options.immediate); this.set('imageType', options.imageType || 'image/jpeg'); this.set('quality', options.quality || .8); this.set('orientation', options.orientation); } /** Helper function to copy result to clipboard * @param {Event} e print event * @return {boolean} * @private */ toClipboard(e, callback) { try { e.canvas.toBlob(function (blob) { try { navigator.clipboard.write([ new window.ClipboardItem( Object.defineProperty({}, blob.type, { value: blob, enumerable: true }) ) ]); if (typeof (callback) === 'function') callback(true); } catch (err) { if (typeof (callback) === 'function') callback(false); } }); } catch (err) { if (typeof (callback) === 'function') callback(false); } } /** Helper function to copy result to clipboard * @param {any} options print options * @param {function} callback a callback function that takes a boolean if copy */ copyMap(options, callback) { this.once('print', function (e) { this.toClipboard(e, callback); }.bind(this)); this.print(options); } /** Get map canvas * @private */ _getCanvas(event, imageType, canvas) { var ctx; // ol <= 5 : get the canvas if (event.context) { canvas = event.context.canvas; } else { // Create a canvas if none if (!canvas) { canvas = document.createElement('canvas'); var size = this.getMap().getSize(); canvas.width = size[0]; canvas.height = size[1]; ctx = canvas.getContext('2d'); if (/jp.*g$/.test(imageType)) { ctx.fillStyle = this.get('bgColor') || 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); } } else { ctx = canvas.getContext('2d'); } // ol6+ : create canvas using layer canvas this.getMap().getViewport().querySelectorAll('.ol-layers canvas, canvas.ol-fixedoverlay').forEach(function (c) { if (c.width) { ctx.save(); // opacity if (c.parentNode.style.opacity === '0') return; ctx.globalAlpha = parseFloat(c.parentNode.style.opacity) || 1; // Blend mode & filter ctx.globalCompositeOperation = ol.ext.element.getStyle(c.parentNode, 'mix-blend-mode'); ctx.filter = ol.ext.element.getStyle(c.parentNode, 'filter'); // transform var tr = ol.ext.element.getStyle(c, 'transform') || ol.ext.element.getStyle(c, '-webkit-transform'); if (/^matrix/.test(tr)) { tr = tr.replace(/^matrix\(|\)$/g, '').split(','); tr.forEach(function (t, i) { tr[i] = parseFloat(t); }); ctx.transform(tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]); ctx.drawImage(c, 0, 0); } else { ctx.drawImage(c, 0, 0, ol.ext.element.getStyle(c, 'width'), ol.ext.element.getStyle(c, 'height')); } ctx.restore(); } }.bind(this)); } return canvas; } /** Fast print * @param {*} options print options * @param {HTMLCanvasElement|undefined} [options.canvas] if none create one, only for ol@6+ * @parama {string} options.imageType */ fastPrint(options, callback) { options = options || {}; if (this._ol6) { requestAnimationFrame(function () { callback(this._getCanvas({}, options.imageType, options.canvas)); }.bind(this)); } else { this.getMap().once('postcompose', function (event) { if (!event.context) this._ol6 = true; callback(this._getCanvas(event, options.imageType, options.canvas)); }.bind(this)); this.getMap().render(); } } /** Print the map * @param {Object} options * @param {string} options.imageType A string indicating the image format, default the control one * @param {number} options.quality Number between 0 and 1 indicating the image quality to use for image formats that use lossy compression such as image/jpeg and image/webp * @param {boolean} options.immediate true to prevent delay for printing * @param {boolean} [options.size=[210,297]] * @param {boolean} [options.format=a4] * @param {boolean} [options.orient] default control orientation * @param {boolean} [options.margin=10] * @param {*} options.any any options passed to the print event when fired * @api */ print(options) { options = options || {}; var imageType = options.imageType || this.get('imageType'); var quality = options.quality || this.get('quality'); if (this.getMap()) { if (options.immediate !== 'silent') { this.dispatchEvent(Object.assign({ type: 'printing', }, options)); } // Start printing after delay to var user show info in the DOM if (!options.immediate) { setTimeout(function () { options = Object.assign({}, options); options.immediate = 'silent'; this.print(options); }.bind(this), 200); return; } // Run printing this.getMap().once(this.get('immediate') ? 'postcompose' : 'rendercomplete', function (event) { var canvas = this._getCanvas(event, imageType); // Calculate print format var size = options.size || [210, 297]; var format = options.format || 'a4'; var w, h, position; var orient = options.orient || this.get('orientation'); var margin = typeof (options.margin) === 'number' ? options.margin : 10; if (canvas) { // Calculate size if (orient !== 'landscape' && orient !== 'portrait') { orient = (canvas.width > canvas.height) ? 'landscape' : 'portrait'; } if (orient === 'landscape') size = [size[1], size[0]]; var sc = Math.min((size[0] - 2 * margin) / canvas.width, (size[1] - 2 * margin) / canvas.height); w = sc * canvas.width; h = sc * canvas.height; // Image position position = [(size[0] - w) / 2, (size[1] - h) / 2]; } // get the canvas image var image; try { image = canvas ? canvas.toDataURL(imageType, quality) : null; } catch (e) { // Fire error event this.dispatchEvent({ type: 'error', canvas: canvas }); return; } // Fire print event var e = Object.assign({ type: 'print', print: { format: format, orientation: orient, unit: 'mm', size: size, position: position, imageWidth: w, imageHeight: h }, image: image, imageType: imageType, quality: quality, canvas: canvas }, options); this.dispatchEvent(e); }.bind(this)); this.getMap().render(); } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Print control to get an image of the map * @constructor * @fire show * @fire print * @fire error * @fire printing * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {string} options.className class of the control * @param {String} options.title button title * @param {string} [options.lang=en] control language, default en * @param {HTMLElement|string|undefined} [options.target] target element to render the control button outside of the map's viewport * @param {HTMLElement|string|undefined} [options.targetDialog] target element for the dialog, default document body * @param {string} options.imageType A string indicating the image format, default image/jpeg * @param {number} options.quality Number between 0 and 1 indicating the image quality to use for image formats that use lossy compression such as image/jpeg and image/webp * @param {string} options.orientation Page orientation (landscape/portrait), default guest the best one * @param {boolean} options.immediate force print even if render is not complete, default false * @param {boolean} [options.openWindow=false] open the file in a new window on print * @param {boolean} [options.copy=true] add a copy select option * @param {boolean} [options.print=true] add a print select option * @param {boolean} [options.pdf=true] add a pdf select option * @param {function} [options.saveAs] a function to save the image as blob * @param {*} [options.jsPDF] jsPDF object to save map as pdf */ ol.control.PrintDialog = class olcontrolPrintDialog extends ol.control.Control { constructor(options) { options = options || {} var element = ol.ext.element.create('DIV', { className: (options.className || 'ol-print') + ' ol-unselectable ol-control' }) super({ element: element }) this._lang = options.lang || 'en' ol.ext.element.create('BUTTON', { type: 'button', title: options.title || 'Print', click: function () { this.print() }.bind(this), parent: element }) // Open in a new window if (options.openWindow) { this.on('print', function (e) { // Print success if (e.canvas) { window.open().document.write('') } }) } // Print control options.target = options.target || ol.ext.element.create('DIV') var printCtrl = this._printCtrl = new ol.control.Print(options) printCtrl.on(['print', 'error', 'printing'], function (e) { content.setAttribute('data-status', e.type) if (!e.clipboard) { this.dispatchEvent(e) } }.bind(this)) // North arrow this._compass = new ol.control.Compass({ src: options.northImage || 'compact', visible: false, className: 'olext-print-compass', style: new ol.style.Stroke({ color: '#333', width: 0 }) }) // Print dialog var printDialog = this._printDialog = new ol.control.Dialog({ target: options.targetDialog || document.body, closeBox: true, className: 'ol-ext-print-dialog' }) var content = printDialog.getContentElement() this._input = {} var param = ol.ext.element.create('DIV', { className: 'ol-print-param', parent: content }) this._pages = [ol.ext.element.create('DIV', { className: 'ol-page' })] var printMap = ol.ext.element.create('DIV', { className: 'ol-map', parent: this._pages[0] }) ol.ext.element.create('DIV', { html: this._pages[0], className: 'ol-print-map', parent: content }) ol.ext.element.create('H2', { html: this.i18n('title'), parent: param }) var ul = ol.ext.element.create('UL', { parent: param }) // Orientation var li = ol.ext.element.create('LI', { /* html: ol.ext.element.create('LABEL', { html: this.18n('orientation') }), */ className: 'ol-orientation', parent: ul }) this._input.orientation = { list: li } var label = ol.ext.element.create('LABEL', { className: 'portrait', parent: li }) this._input.orientation.portrait = ol.ext.element.create('INPUT', { type: 'radio', name: 'ol-print-orientation', value: 'portrait', checked: true, on: { change: function (e) { this.setOrientation(e.target.value) }.bind(this) }, parent: label }) ol.ext.element.create('SPAN', { html: this.i18n('portrait'), parent: label }) label = ol.ext.element.create('LABEL', { className: 'landscape', parent: li }) this._input.orientation.landscape = ol.ext.element.create('INPUT', { type: 'radio', name: 'ol-print-orientation', value: 'landscape', on: { change: function (e) { this.setOrientation(e.target.value) }.bind(this) }, parent: label }) ol.ext.element.create('SPAN', { html: this.i18n('landscape'), parent: label }) // Page size var s li = ol.ext.element.create('LI', { html: ol.ext.element.create('LABEL', { html: this.i18n('size'), }), className: 'ol-size', parent: ul }) var size = this._input.size = ol.ext.element.create('SELECT', { on: { change: function () { this.setSize(size.value || originalSize) }.bind(this) }, parent: li }) for (s in this.paperSize) { ol.ext.element.create('OPTION', { html: s + (this.paperSize[s] ? ' - ' + this.paperSize[s][0] + 'x' + this.paperSize[s][1] + ' mm' : this.i18n('custom')), value: s, parent: size }) } // Margin li = ol.ext.element.create('LI', { html: ol.ext.element.create('LABEL', { html: this.i18n('margin'), }), className: 'ol-margin', parent: ul }) var margin = this._input.margin = ol.ext.element.create('SELECT', { on: { change: function () { this.setMargin(margin.value) }.bind(this) }, parent: li }) for (s in this.marginSize) { ol.ext.element.create('OPTION', { html: this.i18n(s) + ' - ' + this.marginSize[s] + ' mm', value: this.marginSize[s], parent: margin }) } // Scale li = ol.ext.element.create('LI', { html: ol.ext.element.create('LABEL', { html: this.i18n('scale'), }), className: 'ol-scale', parent: ul }) var scale = this._input.scale = ol.ext.element.create('SELECT', { on: { change: function () { this.setScale(parseInt(scale.value)) }.bind(this) }, parent: li }) Object.keys(this.scales).forEach(function (s) { ol.ext.element.create('OPTION', { html: this.scales[s], value: s, parent: scale }) }.bind(this)) // Legend li = ol.ext.element.create('LI', { className: 'ol-legend', parent: ul }) var legend = ol.ext.element.createSwitch({ html: (this.i18n('legend')), checked: false, on: { change: function () { extraCtrl.legend.control.setCanvas(legend.checked) }.bind(this) }, parent: li }) // North li = ol.ext.element.create('LI', { className: 'ol-print-north', parent: ul }) var north = this._input.north = ol.ext.element.createSwitch({ html: this.i18n('north'), checked: 'checked', on: { change: function () { if (north.checked) this._compass.element.classList.add('ol-print-compass') else this._compass.element.classList.remove('ol-print-compass') this.getMap().render() }.bind(this) }, parent: li }) // Title li = ol.ext.element.create('LI', { className: 'ol-print-title', parent: ul }) var title = ol.ext.element.createSwitch({ html: this.i18n('mapTitle'), checked: false, on: { change: function (e) { extraCtrl.title.control.setVisible(e.target.checked) }.bind(this) }, parent: li }) var titleText = ol.ext.element.create('INPUT', { type: 'text', placeholder: this.i18n('mapTitle'), on: { keydown: function (e) { if (e.keyCode === 13) e.preventDefault() }, keyup: function () { extraCtrl.title.control.setTitle(titleText.value) }, change: function () { extraCtrl.title.control.setTitle(titleText.value) }.bind(this) }, parent: li }) // User div element var userElt = ol.ext.element.create('DIV', { className: 'ol-user-param', parent: param }) // Save as li = ol.ext.element.create('LI', { className: 'ol-saveas', parent: ul }) var copied = ol.ext.element.create('DIV', { html: this.i18n('copied'), className: 'ol-clipboard-copy', parent: li }) var save = ol.ext.element.create('SELECT', { on: { change: function () { // Copy to clipboard if (this.formats[save.value].clipboard) { printCtrl.copyMap(this.formats[save.value], function (isok) { if (isok) { copied.classList.add('visible') setTimeout(function () { copied.classList.remove('visible') }, 1000) } }) } else { // Print to file var format = (typeof (this.getSize()) === 'string' ? this.getSize() : null) var opt = Object.assign({ format: format, size: format ? this.paperSize[format] : null, orient: this.getOrientation(), margin: this.getMargin(), }, this.formats[save.value]) printCtrl.print(opt) } save.value = '' }.bind(this) }, parent: li }) ol.ext.element.create('OPTION', { html: this.i18n('saveas'), style: { display: 'none' }, value: '', parent: save }) this.formats.forEach(function (format, i) { if (format.pdf) { if (options.pdf === false) return } else if (format.clipboard) { if (options.copy === false) return } else if (options.save === false) { return } ol.ext.element.create('OPTION', { html: this.i18n(format.title), value: i, parent: save }) }.bind(this)) // Save Legend li = ol.ext.element.create('LI', { className: 'ol-savelegend', parent: ul }) var copylegend = ol.ext.element.create('DIV', { html: this.i18n('copied'), className: 'ol-clipboard-copy', parent: li }) var saveLegend = ol.ext.element.create('SELECT', { on: { change: function () { // Print canvas (with white background) var clegend = extraCtrl.legend.control.getLegend().getCanvas() var canvas = document.createElement('CANVAS') canvas.width = clegend.width canvas.height = clegend.height var ctx = canvas.getContext('2d') ctx.fillStyle = '#fff' ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.drawImage(clegend, 0, 0) // Copy to clipboard if (this.formats[saveLegend.value].clipboard) { canvas.toBlob(function (blob) { try { navigator.clipboard.write([ new window.ClipboardItem( Object.defineProperty({}, blob.type, { value: blob, enumerable: true }) ) ]) copylegend.classList.add('visible') setTimeout(function () { copylegend.classList.remove('visible') }, 1000) } catch (err) { /* errror */ } }, 'image/png') } else { var image try { image = canvas.toDataURL(this.formats[saveLegend.value].imageType, this.formats[saveLegend.value].quality) var format = (typeof (this.getSize()) === 'string' ? this.getSize() : 'A4') var w = canvas.width / 96 * 25.4 var h = canvas.height / 96 * 25.4 var size = this.paperSize[format] if (this.getOrientation() === 'landscape') size = [size[1], size[0]] var position = [ (size[0] - w) / 2, (size[1] - h) / 2 ] this.dispatchEvent({ type: 'print', print: { legend: true, format: format, orientation: this.getOrientation(), unit: 'mm', size: this.paperSize[format], position: position, imageWidth: w, imageHeight: h }, image: image, imageType: this.formats[saveLegend.value].imageType, pdf: this.formats[saveLegend.value].pdf, quality: this.formats[saveLegend.value].quality, canvas: canvas }) } catch (err) { /* error */ } } saveLegend.value = '' }.bind(this) }, parent: li }) ol.ext.element.create('OPTION', { html: this.i18n('saveLegend'), style: { display: 'none' }, value: '', parent: saveLegend }) this.formats.forEach(function (format, i) { ol.ext.element.create('OPTION', { html: this.i18n(format.title), value: i, parent: saveLegend }) }.bind(this)) // Print var prButtons = ol.ext.element.create('DIV', { className: 'ol-ext-buttons', parent: param }) ol.ext.element.create('BUTTON', { html: this.i18n('printBt'), type: 'submit', click: function (e) { e.preventDefault() window.print() }, parent: prButtons }) ol.ext.element.create('BUTTON', { html: this.i18n('cancel'), type: 'button', click: function () { printDialog.hide() }, parent: prButtons }) ol.ext.element.create('DIV', { html: this.i18n('errorMsg'), className: 'ol-error', parent: param }) // Handle dialog show/hide var originalTarget var originalSize var scalelistener var extraCtrl = {} printDialog.on('show', function () { // Dialog is showing this.dispatchEvent({ type: 'show', userElement: userElt, dialog: this._printDialog, page: this.getPage() }) // var map = this.getMap() if (!map) return // Print document document.body.classList.add('ol-print-document') originalTarget = map.getTargetElement() originalSize = map.getSize() if (typeof (this.getSize()) === 'string') this.setSize(this.getSize()) else this.setSize(originalSize) map.setTarget(printMap) // Refresh on move end if (scalelistener) ol.Observable.unByKey(scalelistener) scalelistener = map.on('moveend', function () { this.setScale(ol.sphere.getMapScale(map)) }.bind(this)) this.setScale(ol.sphere.getMapScale(map)) // Get extra controls extraCtrl = {} this.getMap().getControls().forEach(function (c) { if (c instanceof ol.control.Legend) { extraCtrl.legend = { control: c } } if (c instanceof ol.control.CanvasTitle) { extraCtrl.title = { control: c } } if (c instanceof ol.control.Compass) { if (extraCtrl.compass) { c.element.classList.remove('ol-print-compass') } else { if (this._input.north.checked) c.element.classList.add('ol-print-compass') else c.element.classList.remove('ol-print-compass') this._compass = c extraCtrl.compass = { control: c } } } }.bind(this)) // Show hide title if (extraCtrl.title) { title.checked = extraCtrl.title.isVisible = extraCtrl.title.control.getVisible() titleText.value = extraCtrl.title.control.getTitle() title.parentNode.parentNode.classList.remove('hidden') } else { title.parentNode.parentNode.classList.add('hidden') } // Show hide legend if (extraCtrl.legend) { extraCtrl.legend.ison = extraCtrl.legend.control.onCanvas() extraCtrl.legend.collapsed = extraCtrl.legend.control.isCollapsed() extraCtrl.legend.control.collapse(false) saveLegend.parentNode.classList.remove('hidden') legend.parentNode.parentNode.classList.remove('hidden') legend.checked = !extraCtrl.legend.collapsed extraCtrl.legend.control.setCanvas(!extraCtrl.legend.collapsed) } else { saveLegend.parentNode.classList.add('hidden') legend.parentNode.parentNode.classList.add('hidden') } }.bind(this)) printDialog.on('hide', function () { // No print document.body.classList.remove('ol-print-document') if (!originalTarget) return this.getMap().setTarget(originalTarget) originalTarget = null if (scalelistener) ol.Observable.unByKey(scalelistener) // restore if (extraCtrl.title) { extraCtrl.title.control.setVisible(extraCtrl.title.isVisible) } if (extraCtrl.legend) { extraCtrl.legend.control.setCanvas(extraCtrl.legend.ison) extraCtrl.legend.control.collapse(extraCtrl.legend.collapsed) } this.dispatchEvent({ type: 'hide' }) }.bind(this)) // Update preview on resize window.addEventListener('resize', function () { this.setSize() }.bind(this)) // Save or print if (options.saveAs) { this.on('print', function (e) { if (!e.pdf) { // Save image as file e.canvas.toBlob(function (blob) { var name = (e.print.legend ? 'legend.' : 'map.') + e.imageType.replace('image/', '') options.saveAs(blob, name) }, e.imageType, e.quality) } }) } // Save or print if (options.jsPDF) { this.on('print', function (e) { if (e.pdf) { // Export pdf using the print info var pdf = new options.jsPDF({ orientation: e.print.orientation, unit: e.print.unit, format: e.print.size }) pdf.addImage(e.image, 'JPEG', e.print.position[0], e.print.position[0], e.print.imageWidth, e.print.imageHeight) pdf.save(e.print.legend ? 'legend.pdf' : 'map.pdf') } }) } } /** Add a new language * @param {string} lang lang id * @param {Objetct} labels */ static addLang(lang, labels) { ol.control.PrintDialog.prototype._labels[lang] = labels } /** Check if the dialog is oprn * @return {boolean} */ isOpen() { return this._printDialog.isOpen() } /** Translate * @param {string} what * @returns {string} */ i18n(what) { var rep = this._labels.en[what] || 'bad param'; if (this._labels[this._lang] && this._labels[this._lang][what]) { rep = this._labels[this._lang][what] } return rep } /** Get print orientation * @returns {string} */ getOrientation() { return this._orientation || 'portrait' } /** Set print orientation * @param {string} ori landscape or portrait */ setOrientation(ori) { this._orientation = (ori === 'landscape' ? 'landscape' : 'portrait') this._input.orientation[this._orientation].checked = true this.setSize() } /** Get print margin * @returns {number} */ getMargin() { return this._margin || 0 } /** Set print margin * @param {number} */ setMargin(margin) { this._margin = margin this._input.margin.value = margin this.setSize() } /** Get print size * @returns {ol.size} */ getSize() { return this._size } /** Set map print size * @param {ol/size|string} size map size as ol/size or A4, etc. */ setSize(size) { // reset status this._printDialog.getContentElement().setAttribute('data-status', '') if (size) this._size = size else size = this._size if (!size) return if (typeof (size) === 'string') { // Test uppercase for (var k in this.paperSize) { if (k && new RegExp(k, 'i').test(size)) { size = k } } // Default if (!this.paperSize[size]) size = this._size = 'A4' this._input.size.value = size size = [ Math.trunc(this.paperSize[size][0] * 96 / 25.4), Math.trunc(this.paperSize[size][1] * 96 / 25.4) ] if (this.getOrientation() === 'landscape') { size = [size[1], size[0]] } this.getPage().classList.remove('margin') } else { this._input.size.value = '' this.getPage().classList.add('margin') } var printElement = this.getPage() var s = printElement.parentNode.getBoundingClientRect() var scx = (s.width - 40) / size[0] var scy = (s.height - 40) / size[1] var sc = Math.min(scx, scy, 1) printElement.style.width = size[0] + 'px' printElement.style.height = size[1] + 'px' printElement.style['-webkit-transform'] = printElement.style.transform = 'translate(-50%,-50%) scale(' + sc + ')' var px = Math.round(5 / sc) printElement.style['-webkit-box-shadow'] = printElement.style['box-shadow'] = px + 'px ' + px + 'px ' + px + 'px rgba(0,0,0,.6)' printElement.style['padding'] = (this.getMargin() * 96 / 25.4) + 'px' if (this.getMap()) { this.getMap().updateSize() } this.dispatchEvent({ type: 'dialog:refresh' }) } /** Get dialog content element * @return {Element} */ getContentElement() { return this._printDialog.getContentElement() } /** Get dialog user element * @return {Element} */ getUserElement() { return this._printDialog.getContentElement().querySelector('.ol-user-param') } /** Get page element * @return {Element} */ getPage() { return this._pages[0] } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) { this.getMap().removeControl(this._compass) this.getMap().removeControl(this._printCtrl) this.getMap().removeControl(this._printDialog) } super.setMap(map) if (this.getMap()) { this.getMap().addControl(this._compass) this.getMap().addControl(this._printCtrl) this.getMap().addControl(this._printDialog) } } /** Set the current scale (will change the scale of the map) * @param {number|string} value the scale factor or a scale string as 1/xxx */ setScale(value) { ol.sphere.setMapScale(this.getMap(), value) this._input.scale.value = ' ' + (Math.round(value / 100) * 100) } /** Get the current map scale factor * @return {number} */ getScale() { return ol.sphere.getMapScale(this.getMap()) } /** Show print dialog * @param {*} * @param {ol/size|string} options.size map size as ol/size or A4, etc. * @param {number|string} options.value the scale factor or a scale string as 1/xxx * @param {string} options.orientation landscape or portrait * @param {number} options.margin */ print(options) { options = options || {} if (options.size) this.setSize(options.size) if (options.scale) this.setScale(options.scale) if (options.orientation) this.setOrientation(options.orientation) if (options.margin) this.setMargin(options.margin) this._printDialog.show() } /** Get print control * @returns {ol.control.Print} */ getrintControl() { return this._printCtrl } } /** Print dialog labels (for customisation) */ ol.control.PrintDialog.prototype._labels = { en: { title: 'Print', orientation: 'Orientation', portrait: 'Portrait', landscape: 'Landscape', size: 'Page size', custom: 'screen size', margin: 'Margin', scale: 'Scale', legend: 'Legend', north: 'North arrow', mapTitle: 'Map title', saveas: 'Save as...', saveLegend: 'Save legend...', copied: '✔ Copied to clipboard', errorMsg: 'Can\'t save map canvas...', printBt: 'Print...', clipboardFormat: 'copy to clipboard...', jpegFormat: 'save as jpeg', pngFormat: 'save as png', pdfFormat: 'save as pdf', none: 'none', small: 'small', large: 'large', cancel: 'cancel' }, fr: { title: 'Imprimer', orientation: 'Orientation', portrait: 'Portrait', landscape: 'Paysage', size: 'Taille du papier', custom: 'taille écran', margin: 'Marges', scale: 'Echelle', legend: 'Légende', north: 'Flèche du nord', mapTitle: 'Titre de la carte', saveas: 'Enregistrer sous...', saveLegend: 'Enregistrer la légende...', copied: '✔ Carte copiée', errorMsg: 'Impossible d\'enregistrer la carte', printBt: 'Imprimer', clipboardFormat: 'copier dans le presse-papier...', jpegFormat: 'enregistrer un jpeg', pngFormat: 'enregistrer un png', pdfFormat: 'enregistrer un pdf', none: 'aucune', small: 'petites', large: 'larges', cancel: 'annuler' }, de: { title: 'Drucken', orientation: 'Ausrichtung', portrait: 'Hochformat', landscape: 'Querformat', size: 'Papierformat', custom: 'Bildschirmgröße', margin: 'Rand', scale: 'Maßstab', legend: 'Legende', north: 'Nordpfeil', mapTitle: 'Kartentitel', saveas: 'Speichern als...', saveLegend: 'Legende speichern...', copied: '✔ In die Zwischenablage kopiert', errorMsg: 'Kann Karte nicht speichern...', printBt: 'Drucken...', clipboardFormat: 'in die Zwischenablage kopieren...', jpegFormat: 'speichern als jpeg', pngFormat: 'speichern als png', pdfFormat: 'speichern als pdf', none: 'kein', small: 'klein', large: 'groß', cancel: 'abbrechen' }, zh:{ title: '打印', orientation: '方向', portrait: '纵向', landscape: '横向', size: '页面大小', custom: '屏幕大小', margin: '外边距', scale: '尺度', legend: '图例', north: '指北针', mapTitle: '地图名字', saveas: '保存为...', saveLegend: '保存图例为...', copied: '✔ 已复制到剪贴板', errorMsg: '无法保存地图...', printBt: '打印...', cancel: '取消' } }; /** List of paper size */ ol.control.PrintDialog.prototype.paperSize = { '': null, 'A0': [841,1189], 'A1': [594,841], 'A2': [420,594], 'A3': [297,420], 'A4': [210,297], 'US Letter': [215.9,279.4], 'A5': [148,210], 'B4': [257,364], 'B5': [182,257] }; /** List of margin size */ ol.control.PrintDialog.prototype.marginSize = { none: 0, small: 5, large: 10 }; /** List of legeng options * / ol.control.PrintDialog.prototype.legendOptions = { off: 'Hide legend', on: 'Show legend' }; /** List of print image file formats */ ol.control.PrintDialog.prototype.formats = [{ title: 'clipboardFormat', imageType: 'image/png', clipboard: true }, { title: 'jpegFormat', imageType: 'image/jpeg', quality: .8 }, { title: 'pngFormat', imageType: 'image/png', quality: .8 }, { title: 'pdfFormat', imageType: 'image/jpeg', pdf: true } ]; /** List of print scale */ ol.control.PrintDialog.prototype.scales = { ' 5000': '1/5.000', ' 10000': '1/10.000', ' 25000': '1/25.000', ' 50000': '1/50.000', ' 100000': '1/100.000', ' 250000': '1/250.000', ' 1000000': '1/1.000.000' }; /* 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). */ /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ /** * @classdesc OpenLayers 3 Profil Control. * Draw a profile of a feature (with a 3D geometry) * * @constructor * @extends {ol.control.Control} * @fires over * @fires out * @fires show * @fires dragstart * @fires dragging * @fires dragend * @fires dragcancel * @param {Object=} options * @param {string} options.className * @param {String} options.title button title * @param {ol.style.Style} [options.style] style to draw the profil, default darkblue * @param {ol.style.Style} [options.selectStyle] style for selection, default darkblue fill * @param {*} options.info keys/values for i19n * @param {number} [options.width=300] * @param {number} [options.height=150] * @param {ol.Feature} [options.feature] the feature to draw profil * @param {boolean} [options.selectable=false] enable selection on the profil, default false * @param {boolean} [options.zoomable=false] can zoom in the profil */ ol.control.Profil = class olcontrolProfil extends ol.control.Control { constructor(options) { options = options || {} var element = document.createElement('div') super({ element: element, target: options.target }) var self = this this.info = options.info || ol.control.Profil.prototype.info if (options.target) { element.classList.add(options.className || 'ol-profil') } else { element.className = ((options.className || 'ol-profil') + ' ol-unselectable ol-control ol-collapsed').trim() this.button = document.createElement('button') this.button.title = options.title || 'Profile'; this.button.setAttribute('type', 'button') var click_touchstart_function = function (e) { self.toggle() e.preventDefault() } this.button.addEventListener('click', click_touchstart_function) this.button.addEventListener('touchstart', click_touchstart_function) element.appendChild(this.button) ol.ext.element.create('I', { parent: this.button }) } // Drawing style if (options.style instanceof ol.style.Style) { this._style = options.style } else { this._style = new ol.style.Style({ text: new ol.style.Text(), stroke: new ol.style.Stroke({ width: 1.5, color: '#369' }) }) } if (!this._style.getText()) this._style.setText(new ol.style.Text()) // Selection style if (options.selectStyle instanceof ol.style.Style) { this._selectStyle = options.selectStyle } else { this._selectStyle = new ol.style.Style({ fill: new ol.style.Fill({ color: '#369' }) }) } var div_inner = document.createElement("div") div_inner.classList.add("ol-inner") element.appendChild(div_inner) var div = document.createElement("div") div.style.position = "relative" div_inner.appendChild(div) var ratio = this.ratio = 2 this.canvas_ = document.createElement('canvas') this.canvas_.width = (options.width || 300) * ratio this.canvas_.height = (options.height || 150) * ratio var styles = { "msTransform": "scale(0.5,0.5)", "msTransformOrigin": "0 0", "webkitTransform": "scale(0.5,0.5)", "webkitTransformOrigin": "0 0", "mozTransform": "scale(0.5,0.5)", "mozTransformOrigin": "0 0", "transform": "scale(0.5,0.5)", "transformOrigin": "0 0" } Object.keys(styles).forEach(function (style) { if (style in self.canvas_.style) { self.canvas_.style[style] = styles[style] } }) var div_to_canvas = document.createElement("div") div.appendChild(div_to_canvas) div_to_canvas.style.width = this.canvas_.width / ratio + "px" div_to_canvas.style.height = this.canvas_.height / ratio + "px" div_to_canvas.appendChild(this.canvas_) div_to_canvas.addEventListener('pointerdown', this.onMove.bind(this)) document.addEventListener('pointerup', this.onMove.bind(this)) div_to_canvas.addEventListener('mousemove', this.onMove.bind(this)) div_to_canvas.addEventListener('touchmove', this.onMove.bind(this)) this.set('selectable', options.selectable) // Offset in px this.margin_ = { top: 10 * ratio, left: 45 * ratio, bottom: 30 * ratio, right: 10 * ratio } if (!this.info.ytitle) this.margin_.left -= 20 * ratio if (!this.info.xtitle) this.margin_.bottom -= 20 * ratio // Cursor this.bar_ = document.createElement("div") this.bar_.classList.add("ol-profilbar") this.bar_.style.top = (this.margin_.top / ratio) + "px" this.bar_.style.height = (this.canvas_.height - this.margin_.top - this.margin_.bottom) / ratio + "px" div.appendChild(this.bar_) this.cursor_ = document.createElement("div") this.cursor_.classList.add("ol-profilcursor") div.appendChild(this.cursor_) this.popup_ = document.createElement("div") this.popup_.classList.add("ol-profilpopup") this.cursor_.appendChild(this.popup_) // Track information var t = document.createElement("table") t.cellPadding = '0' t.cellSpacing = '0' t.style.clientWidth = this.canvas_.width / ratio + "px" div.appendChild(t) var firstTr = document.createElement("tr") firstTr.classList.add("track-info") t.appendChild(firstTr) var div_zmin = document.createElement("td") div_zmin.innerHTML = (this.info.zmin || "Zmin") + ': ' firstTr.appendChild(div_zmin) var div_zmax = document.createElement("td") div_zmax.innerHTML = (this.info.zmax || "Zmax") + ': ' firstTr.appendChild(div_zmax) var div_distance = document.createElement("td") div_distance.innerHTML = (this.info.distance || "Distance") + ': ' firstTr.appendChild(div_distance) var div_time = document.createElement("td") div_time.innerHTML = (this.info.time || "Time") + ': ' firstTr.appendChild(div_time) var secondTr = document.createElement("tr") secondTr.classList.add("point-info") t.appendChild(secondTr) var div_altitude = document.createElement("td") div_altitude.innerHTML = (this.info.altitude || "Altitude") + ': ' secondTr.appendChild(div_altitude) var div_distance2 = document.createElement("td") div_distance2.innerHTML = (this.info.distance || "Distance") + ': ' secondTr.appendChild(div_distance2) var div_time2 = document.createElement("td") div_time2.innerHTML = (this.info.time || "Time") + ': ' secondTr.appendChild(div_time2) // Array of data this.tab_ = [] // Show feature if (options.feature) { this.setGeometry(options.feature) } // Zoom on profile if (options.zoomable) { this.set('selectable', true) var start, geom this.on('change:geometry', function () { geom = null }) this.on('dragstart', function (e) { start = e.index }) this.on('dragend', function (e) { if (Math.abs(start - e.index) > 10) { if (!geom) { var bt = ol.ext.element.create('BUTTON', { parent: element, className: 'ol-zoom-out', click: function (e) { e.stopPropagation() e.preventDefault() if (geom) { this.dispatchEvent({ type: 'zoom' }) this.setGeometry(geom, this._geometry[1]) } element.removeChild(bt) }.bind(this) }) } var saved = geom || this._geometry[0] var g = new ol.geom.LineString(this.getSelection(start, e.index)) this.setGeometry(g, this._geometry[1]) geom = saved this.dispatchEvent({ type: 'zoom', geometry: g, start: start, end: e.index }) } }.bind(this)) } } /** Show popup info * @param {string} info to display as a popup * @api stable */ popup(info) { this.popup_.innerHTML = info } /** Show point on profil * @param {*} p * @param {number} dx * @private */ _drawAt(p, dx) { if (p) { this.cursor_.style.left = dx + "px" this.cursor_.style.top = (this.canvas_.height - this.margin_.bottom + p[1] * this.scale_[1] + this.dy_) / this.ratio + "px" this.cursor_.style.display = "block" this.bar_.parentElement.classList.add("over") this.bar_.style.left = dx + "px" this.bar_.style.display = "block" this.element.querySelector(".point-info .z").textContent = p[1] + this.info.altitudeUnits this.element.querySelector(".point-info .dist").textContent = (p[0] / 1000).toFixed(1) + this.info.distanceUnitsKM this.element.querySelector(".point-info .time").textContent = p[2] if (dx > this.canvas_.width / this.ratio / 2) this.popup_.classList.add('ol-left') else this.popup_.classList.remove('ol-left') } else { this.cursor_.style.display = "none" this.bar_.style.display = 'none' this.cursor_.style.display = 'none' this.bar_.parentElement.classList.remove("over") } } /** Show point at coordinate or a distance on the profil * @param { ol.coordinates|number } where a coordinate or a distance from begining, if none it will hide the point * @return { ol.coordinates } current point */ showAt(where) { var i, p, p0, d0 = Infinity if (typeof (where) === 'undefined') { if (this.bar_.parentElement.classList.contains("over")) { // Remove it this._drawAt() } } else if (where.length) { // Look for closest the point for (i = 1; p = this.tab_[i]; i++) { var d = ol.coordinate.dist2d(p[3], where) if (d < d0) { p0 = p d0 = d } } } else { for (i = 0; p = this.tab_[i]; i++) { p0 = p if (p[0] >= where) { break } } } if (p0) { var dx = (p0[0] * this.scale_[0] + this.margin_.left) / this.ratio this._drawAt(p0, dx) return p0[3] } return null } /** Show point at a time on the profil * @param { Date|number } time a Date or a DateTime (in s) to show the profile on, if none it will hide the point * @param { booelan } delta true if time is a delta from the start, default false * @return { ol.coordinates } current point */ showAtTime(time, delta) { var i, p, p0 if (time instanceof Date) { time = time.getTime() / 1000 } else if (delta) { time += this.tab_[0][3][3] } if (typeof (time) === 'undefined') { if (this.bar_.parentElement.classList.contains("over")) { // Remove it this._drawAt() } } else { for (i = 0; p = this.tab_[i]; i++) { p0 = p if (p[3][3] >= time) { break } } } if (p0) { var dx = (p0[0] * this.scale_[0] + this.margin_.left) / this.ratio this._drawAt(p0, dx) return p0[3] } return null } /** Get the point at a given time on the profil * @param { number } time time at which to show the point * @return { ol.coordinates } current point */ pointAtTime(time) { var i, p // Look for closest the point for (i = 1; p = this.tab_[i]; i++) { var t = p[3][3] if (t >= time) { // Previous one ? var pt = this.tab_[i - 1][3] if ((pt[3] + t) / 2 < time) return pt else return p } } return this.tab_[this.tab_.length - 1][3] } /** Mouse move over canvas */ onMove(e) { if (!this.tab_.length) return var box_canvas = this.canvas_.getBoundingClientRect() var pos = { top: box_canvas.top + window.pageYOffset - document.documentElement.clientTop, left: box_canvas.left + window.pageXOffset - document.documentElement.clientLeft } var pageX = e.pageX || (e.touches && e.touches.length && e.touches[0].pageX) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageX) var pageY = e.pageY || (e.touches && e.touches.length && e.touches[0].pageY) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY) var dx = pageX - pos.left var dy = pageY - pos.top var ratio = this.ratio if (dx > this.margin_.left / ratio - 20 && dx < (this.canvas_.width - this.margin_.right) / ratio + 8 && dy > this.margin_.top / ratio && dy < (this.canvas_.height - this.margin_.bottom) / ratio) { var d = (dx * ratio - this.margin_.left) / this.scale_[0] var p0 = this.tab_[0] var index, p for (index = 1; p = this.tab_[index]; index++) { if (p[0] >= d) { if (d < (p[0] + p0[0]) / 2) { index = 0 p = p0 } break } } if (!p) p = this.tab_[this.tab_.length - 1] dx = Math.max(this.margin_.left / ratio, Math.min(dx, (this.canvas_.width - this.margin_.right) / ratio)) this._drawAt(p, dx) this.dispatchEvent({ type: 'over', click: e.type === 'click', index: index, coord: p[3], time: p[2], distance: p[0] }) // Handle drag / click switch (e.type) { case 'pointerdown': { this._dragging = { event: { type: 'dragstart', index: index, coord: p[3], time: p[2], distance: p[0] }, pageX: pageX, pageY: pageY } break } case 'pointerup': { if (this._dragging && this._dragging.pageX) { if (Math.abs(this._dragging.pageX - pageX) < 3 && Math.abs(this._dragging.pageY - pageY) < 3) { this.dispatchEvent({ type: 'click', index: index, coord: p[3], time: p[2], distance: p[0] }) this.refresh() } } else { this.dispatchEvent({ type: 'dragend', index: index, coord: p[3], time: p[2], distance: p[0] }) } this._dragging = false break } default: { if (this._dragging) { if (this._dragging.pageX) { if (Math.abs(this._dragging.pageX - pageX) > 3 || Math.abs(this._dragging.pageY - pageY) > 3) { this._dragging.pageX = this._dragging.pageY = false this.dispatchEvent(this._dragging.event) } } else { this.dispatchEvent({ type: 'dragging', index: index, coord: p[3], time: p[2], distance: p[0] }) var min = Math.min(this._dragging.event.index, index) var max = Math.max(this._dragging.event.index, index) this.refresh() if (this.get('selectable')) this._drawGraph(this.tab_.slice(min, max), this._selectStyle) } } break } } } else { if (this.bar_.parentElement.classList.contains('over')) { this._drawAt() this.dispatchEvent({ type: 'out' }) } if (e.type === 'pointerup' && this._dragging) { this.dispatchEvent({ type: 'dragcancel' }) this._dragging = false } } } /** Show panel * @api stable */ show() { this.element.classList.remove("ol-collapsed") this.dispatchEvent({ type: 'show', show: true }) } /** Hide panel * @api stable */ hide() { this.element.classList.add("ol-collapsed") this.dispatchEvent({ type: 'show', show: false }) } /** Toggle panel * @api stable */ toggle() { this.element.classList.toggle("ol-collapsed") var b = this.element.classList.contains("ol-collapsed") this.dispatchEvent({ type: 'show', show: !b }) } /** Is panel visible */ isShown() { return (!this.element.classList.contains("ol-collapsed")) } /** Get selection * @param {number} starting point * @param {number} ending point * @return {Array} */ getSelection(start, end) { var sel = [] var min = Math.max(Math.min(start, end), 0) var max = Math.min(Math.max(start, end), this.tab_.length - 1) for (var i = min; i <= max; i++) { sel.push(this.tab_[i][3]) } return sel } /** Draw the graph * @private */ _drawGraph(t, style) { if (!t.length) return var ctx = this.canvas_.getContext('2d') var scx = this.scale_[0] var scy = this.scale_[1] var dy = this.dy_ var ratio = this.ratio var i, p // Draw Path ctx.beginPath() for (i = 0; p = t[i]; i++) { if (i == 0) ctx.moveTo(p[0] * scx, p[1] * scy + dy) else ctx.lineTo(p[0] * scx, p[1] * scy + dy) } if (style.getStroke()) { ctx.strokeStyle = style.getStroke().getColor() || '#000' ctx.lineWidth = style.getStroke().getWidth() * ratio ctx.setLineDash([]) ctx.stroke() } // Fill path if (style.getFill()) { ctx.fillStyle = style.getFill().getColor() || '#000' ctx.Style = style.getFill().getColor() || '#000' ctx.lineTo(t[t.length - 1][0] * scx, 0) ctx.lineTo(t[0][0] * scx, 0) ctx.fill() } } /** * Set the geometry to draw the profil. * @param {ol.Feature|ol.geom.Geometry} f the feature. * @param {Object=} options * @param {ol.ProjectionLike} [options.projection] feature projection, default projection of the map * @param {string} [options.zunit='m'] 'm' or 'km', default m * @param {string} [options.unit='km'] 'm' or 'km', default km * @param {Number|undefined} [options.zmin=0] default 0 * @param {Number|undefined} options.zmax default max Z of the feature * @param {integer|undefined} [options.zDigits=0] number of digits for z graduation, default 0 * @param {integer|undefined} [options.zMaxChars] maximum number of chars to be used for z graduation before switching to scientific notation * @param {Number|undefined} [options.graduation=100] z graduation default 100 * @param {integer|undefined} [options.amplitude] amplitude of the altitude, default zmax-zmin * @api stable */ setGeometry(g, options) { if (!options) options = {} if (g instanceof ol.Feature) g = g.getGeometry() this._geometry = [g, options] // No Z if (!/Z/.test(g.getLayout())) return // No time if (/M/.test(g.getLayout())) this.element.querySelector(".time").parentElement.style.display = 'block' else this.element.querySelector(".time").parentElement.style.display = 'none' // Coords var c = g.getCoordinates() switch (g.getType()) { case "LineString": break case "MultiLineString": c = c[0]; break default: return } // Distance beetween 2 coords var proj = options.projection || this.getMap().getView().getProjection() function dist2d(p1, p2) { return ol.sphere.getDistance( ol.proj.transform(p1, proj, 'EPSG:4326'), ol.proj.transform(p2, proj, 'EPSG:4326') ) } function getTime(t0, t1) { if (!t0 || !t1) return "-" var dt = (t1 - t0) / 60 // mn var ti = Math.trunc(dt / 60) var mn = Math.trunc(dt - ti * 60) return ti + "h" + (mn < 10 ? "0" : "") + mn + "mn" } // Calculate [distance, altitude, time, point] for each points var zmin = Infinity, zmax = -Infinity var i, p, d, z, ti, t = this.tab_ = [] for (i = 0, p; p = c[i]; i++) { z = p[2] if (z < zmin) zmin = z if (z > zmax) zmax = z if (i == 0) d = 0 else d += dist2d(c[i - 1], p) ti = getTime(c[0][3], p[3]) t.push([d, z, ti, p]) } this._z = [zmin, zmax] this.set('graduation', options.graduation || 100) this.set('zmin', options.zmin) this.set('zmax', options.zmax) this.set('amplitude', options.amplitude) this.set('unit', options.unit) this.set('zunit', options.zunit) this.set('zDigits', options.zDigits) this.set('zMaxChars', options.zMaxChars) this.dispatchEvent({ type: 'change:geometry', geometry: g }) this.refresh() } /** Refresh the profil */ refresh() { var canvas = this.canvas_ var ctx = canvas.getContext('2d') var w = canvas.width var h = canvas.height ctx.setTransform(1, 0, 0, 1, 0, 0) ctx.clearRect(0, 0, w, h) var zmin = this._z[0] var zmax = this._z[1] var t = this.tab_ var d = t[t.length - 1][0] var ti = t[t.length - 1][2] var i if (!d) { console.error('[ol/control/Profil] no data...', t) return } // Margin ctx.setTransform(1, 0, 0, 1, this.margin_.left, h - this.margin_.bottom) var ratio = this.ratio w -= this.margin_.right + this.margin_.left h -= this.margin_.top + this.margin_.bottom // Draw axes ctx.strokeStyle = this._style.getText().getFill().getColor() || '#000' ctx.lineWidth = 0.5 * ratio ctx.beginPath() ctx.moveTo(0, 0); ctx.lineTo(0, -h) ctx.moveTo(0, 0); ctx.lineTo(w, 0) ctx.stroke() // Info this.element.querySelector(".track-info .zmin").textContent = zmin.toFixed(2) + this.info.altitudeUnits this.element.querySelector(".track-info .zmax").textContent = zmax.toFixed(2) + this.info.altitudeUnits if (d > 1000) { this.element.querySelector(".track-info .dist").textContent = (d / 1000).toFixed(1) + this.info.distanceUnitsKM } else { this.element.querySelector(".track-info .dist").textContent = (d).toFixed(1) + this.info.distanceUnitsM } this.element.querySelector(".track-info .time").textContent = ti // Set graduation var grad = this.get('graduation') while (true) { zmax = Math.ceil(zmax / grad) * grad zmin = Math.floor(zmin / grad) * grad var nbgrad = (zmax - zmin) / grad if (h / nbgrad < 15 * ratio) { grad *= 2 } else break } // Set amplitude if (typeof (this.get('zmin')) == 'number' && zmin > this.get('zmin')) zmin = this.get('zmin') if (typeof (this.get('zmax')) == 'number' && zmax < this.get('zmax')) zmax = this.get('zmax') var amplitude = this.get('amplitude') if (amplitude) { zmax = Math.max(zmin + amplitude, zmax) } // Scales lines var scx = w / d var scy = -h / (zmax - zmin) var dy = this.dy_ = -zmin * scy this.scale_ = [scx, scy] this._drawGraph(t, this._style) // Draw ctx.textAlign = 'right' ctx.textBaseline = 'top' ctx.fillStyle = this._style.getText().getFill().getColor() || '#000' // Scale Z ctx.beginPath() var fix = this.get('zDigits') || 0 var exp = null if (typeof (this.get('zMaxChars')) == 'number') { var usedChars if (this.get('zunit') != 'km') usedChars = Math.max(zmin.toFixed(fix).length, zmax.toFixed(fix).length) else usedChars = Math.max((zmin / 1000).toFixed(1).length, (zmax / 1000).toFixed(1).length) if (this.get('zMaxChars') < usedChars) { exp = Math.floor(Math.log10(Math.max(Math.abs(zmin), Math.abs(zmax), Number.MIN_VALUE))) ctx.font = 'bold ' + (9 * ratio) + 'px arial' ctx.fillText(exp.toString(), -8 * ratio, 8 * ratio) var expMetrics = ctx.measureText(exp.toString()) var expWidth = expMetrics.width var expHeight = expMetrics.actualBoundingBoxAscent + expMetrics.actualBoundingBoxDescent ctx.font = 'bold ' + (12 * ratio) + 'px arial' ctx.fillText("10", -8 * ratio - expWidth, 8 * ratio + 0.5 * expHeight) } } ctx.font = (10 * ratio) + 'px arial' ctx.textBaseline = 'middle' for (i = zmin; i <= zmax; i += grad) { if (exp !== null) { var baseNumber = i / Math.pow(10, exp) if (this.get('zunit') == 'km') baseNumber /= 1000 var nbDigits = this.get('zMaxChars') - Math.floor(Math.log10(Math.max(Math.abs(baseNumber), 1)) + 1) - 1 if (baseNumber < 0) nbDigits -= 1 if (this.get('zunit') != 'km') ctx.fillText(baseNumber.toFixed(Math.max(nbDigits, 0)), -4 * ratio, i * scy + dy) else ctx.fillText(baseNumber.toFixed(Math.max(nbDigits, 0)), -4 * ratio, i * scy + dy) } else { if (this.get('zunit') != 'km') ctx.fillText(i.toFixed(fix), -4 * ratio, i * scy + dy) else ctx.fillText((i / 1000).toFixed(1), -4 * ratio, i * scy + dy) } ctx.moveTo(-2 * ratio, i * scy + dy) if (i != 0) ctx.lineTo(d * scx, i * scy + dy) else ctx.lineTo(0, i * scy + dy) } // Scale X ctx.textAlign = "center" ctx.textBaseline = "top" ctx.setLineDash([ratio, 3 * ratio]) var unit = this.get('unit') || "km" var step if (d > 1000) { step = Math.round(d / 1000) * 100 if (step > 1000) step = Math.ceil(step / 1000) * 1000 } else { unit = "m" if (d > 100) step = Math.round(d / 100) * 10 else if (d > 10) step = Math.round(d / 10) else if (d > 1) step = Math.round(d) / 10 else step = d } for (i = 0; i <= d; i += step) { var txt = (unit == "m") ? i : (i / 1000) //if (i+step>d) txt += " "+ (options.zunits || "km"); ctx.fillText(Math.round(txt * 10) / 10, i * scx, 4 * ratio) ctx.moveTo(i * scx, 2 * ratio); ctx.lineTo(i * scx, 0) } ctx.font = (12 * ratio) + "px arial" ctx.fillText(this.info.xtitle.replace("(km)", "(" + unit + ")"), w / 2, 18 * ratio) ctx.save() ctx.rotate(-Math.PI / 2) ctx.fillText(this.info.ytitle, h / 2, -this.margin_.left) ctx.restore() ctx.stroke() } /** Get profil image * @param {string|undefined} type image format or 'canvas' to get the canvas image, default image/png. * @param {Number|undefined} encoderOptions between 0 and 1 indicating image quality image/jpeg or image/webp, default 0.92. * @return {string} requested data uri * @api stable */ getImage(type, encoderOptions) { if (type === "canvas") return this.canvas_ return this.canvas_.toDataURL(type, encoderOptions) } } /** Custom infos list * @api stable */ ol.control.Profil.prototype.info = { "zmin": "Zmin", "zmax": "Zmax", "ytitle": "Altitude (m)", "xtitle": "Distance (km)", "time": "Time", "altitude": "Altitude", "distance": "Distance", "altitudeUnits": "m", "distanceUnitsM": "m", "distanceUnitsKM": "km", }; /* 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). */ /** Add a progress bar to a map. * Use the layers option listen to tileload event and show the layer loading progress. * @constructor * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {String} [options.className] class of the control * @param {String} [options.label] waiting label * @param {ol.layer.Layer|Array} [options.layers] tile layers with tileload events */ ol.control.ProgressBar = class olcontrolProgressBar extends ol.control.Control { constructor(options) { options = options || {} var element = ol.ext.element.create('DIV', { className: ((options.className || '') + ' ol-progress-bar ol-unselectable ol-control').trim() }) super({ element: element, target: options.target }) this._waiting = ol.ext.element.create('DIV', { html: options.label || '', className: 'ol-waiting', parent: element }) this._bar = ol.ext.element.create('DIV', { className: 'ol-bar', parent: element }) this._layerlistener = [] this.setLayers(options.layers) } /** Set the control visibility * @param {Number} [n] progress percentage, a number beetween 0,1, default hide progress bar */ setPercent(n) { this._bar.style.width = ((Number(n) || 0) * 100) + '%' if (n === undefined) { ol.ext.element.hide(this.element) } else { ol.ext.element.show(this.element) } } /** Set waiting text * @param {string} label */ setLabel(label) { this._waiting.innerHTML = label } /** Use a list of tile layer to shown tile load * @param {ol.layer.Layer|Array} layers a layer or a list of layer */ setLayers(layers) { // reset this._layerlistener.forEach(function (l) { ol.Observable.unByKey(l) }) this._layerlistener = [] this.setPercent() var loading = 0, loaded = 0 if (layers instanceof ol.layer.Layer) layers = [layers] if (!layers || !layers.forEach) return var tout // Listeners layers.forEach(function (layer) { if (layer instanceof ol.layer.Layer) { this._layerlistener.push(layer.getSource().on('tileloadstart', function () { loading++ this.setPercent(loaded / loading) clearTimeout(tout) }.bind(this))) this._layerlistener.push(layer.getSource().on(['tileloadend', 'tileloaderror'], function () { loaded++ if (loaded === loading) { loading = loaded = 0 this.setPercent(1) tout = setTimeout(this.setPercent.bind(this), 300) } else { this.setPercent(loaded / loading) } }.bind(this))) } }.bind(this)) } } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Geoportail routing Control. * @constructor * @extends {ol.control.Control} * @fires select * @fires change:input * @fires routing:start * @fires routing * @fires step:select * @fires step:hover * @fires error * @fires abort * @param {Object=} options * @param {string} options.className control class name * @param {string | undefined} [options.apiKey] the service api key. * @param {string | undefined} options.authentication: basic authentication for the service API as btoa("login:pwd") * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {string | undefined} options.inputLabel label for the input, default none * @param {string | undefined} options.noCollapse prevent collapsing on input blur, default false * @param {number | undefined} options.typing a delay on each typing to start searching (ms) use -1 to prevent autocompletion, default 300. * @param {integer | undefined} options.minLength minimum length to start searching, default 1 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * @param {integer | undefined} options.maxHistory maximum number of items to display in history. Set -1 if you don't want history, default maxItems * @param {function} options.getTitle a function that takes a feature and return the name to display in the index. * @param {function} options.autocomplete a function that take a search string and callback function to send an array * @param {number} options.timeout default 20s */ ol.control.RoutingGeoportail = class olcontrolRoutingGeoportail extends ol.control.Control { constructor(options) { options = options || {}; if (options.typing == undefined) options.typing = 300; options.apiKey = options.apiKey || 'itineraire'; if (!options.search) options.search = {}; options.search.apiKey = options.search.apiKey || 'essentiels'; var element = document.createElement("DIV"); super({ element: element, target: options.target }); var self = this; // Class name for history this._classname = options.className || 'search'; this._source = new ol.source.Vector(); // Authentication this._auth = options.authentication; var classNames = (options.className || "") + " ol-routing"; if (!options.target) { classNames += " ol-unselectable ol-control"; } element.setAttribute('class', classNames); if (!options.target) { var bt = ol.ext.element.create('BUTTON', { parent: element }); bt.addEventListener('click', function () { element.classList.toggle('ol-collapsed'); }); } this.set('url', 'https://wxs.ign.fr/calcul/geoportail/' + options.apiKey + '/rest/1.0.0/route'); var content = ol.ext.element.create('DIV', { className: 'content', parent: element }); var listElt = ol.ext.element.create('DIV', { className: 'search-input', parent: content }); this._search = []; this.addSearch(listElt, options); this.addSearch(listElt, options); ol.ext.element.create('I', { className: 'ol-car', title: options.carlabel || 'by car', parent: content }) .addEventListener("click", function () { self.setMode('car'); }); ol.ext.element.create('I', { className: 'ol-pedestrian', title: options.pedlabel || 'pedestrian', parent: content }) .addEventListener("click", function () { self.setMode('pedestrian'); }); ol.ext.element.create('I', { className: 'ol-ok', title: options.runlabel || 'search', html: 'OK', parent: content }) .addEventListener("click", function () { self.calculate(); }); ol.ext.element.create('I', { className: 'ol-cancel', html: 'cancel', parent: content }) .addEventListener("click", function () { this.resultElement.innerHTML = ''; }.bind(this)); this.resultElement = document.createElement("DIV"); this.resultElement.setAttribute('class', 'ol-result'); element.appendChild(this.resultElement); this.setMode(options.mode || 'car'); this.set('timeout', options.timeout || 20000); } setMode(mode, silent) { this.set('mode', mode); this.element.querySelector(".ol-car").classList.remove("selected"); this.element.querySelector(".ol-pedestrian").classList.remove("selected"); this.element.querySelector(".ol-" + mode).classList.add("selected"); if (!silent) this.calculate(); } setMethod(method, silent) { this.set('method', method); if (!silent) this.calculate(); } addButton(className, title, info) { var bt = document.createElement("I"); bt.setAttribute("class", className); bt.setAttribute("type", "button"); bt.setAttribute("title", title); bt.innerHTML = info || ''; this.element.appendChild(bt); return bt; } /** Get point source * @return {ol.source.Vector } */ getSource() { return this._source; } _resetArray(element) { this._search = []; var q = element.parentNode.querySelectorAll('.search-input > div'); q.forEach(function (d) { if (d.olsearch) { if (d.olsearch.get('feature')) { d.olsearch.get('feature').set('step', this._search.length); if (this._search.length === 0) d.olsearch.get('feature').set('pos', 'start'); else if (this._search.length === q.length - 1) d.olsearch.get('feature').set('pos', 'end'); else d.olsearch.get('feature').set('pos', ''); } this._search.push(d.olsearch); } }.bind(this)); } /** Remove a new search input * @private */ removeSearch(element, options, after) { element.removeChild(after); if (after.olsearch.get('feature')) this._source.removeFeature(after.olsearch.get('feature')); if (this.getMap()) this.getMap().removeControl(after.olsearch); this._resetArray(element); } /** Add a new search input * @private */ addSearch(element, options, after) { var self = this; var div = ol.ext.element.create('DIV'); if (after) element.insertBefore(div, after.nextSibling); else element.appendChild(div); ol.ext.element.create('BUTTON', { title: options.startlabel || "add/remove", parent: div }) .addEventListener('click', function (e) { if (e.ctrlKey) { if (this._search.length > 2) this.removeSearch(element, options, div); } else if (e.shiftKey) { this.addSearch(element, options, div); } }.bind(this)); var search = div.olsearch = new ol.control.SearchGeoportail({ className: 'IGNF ol-collapsed', apiKey: options.search.apiKey, authentication: options.search.authentication, target: div, reverse: true }); search._changeCounter = 0; this._resetArray(element); search.on('select', function (e) { search.setInput(e.search.fulltext); var f = search.get('feature'); if (!f) { f = new ol.Feature(new ol.geom.Point(e.coordinate)); search.set('feature', f); this._source.addFeature(f); // Check geometry change search.checkgeom = true; f.getGeometry().on('change', function () { if (search.checkgeom) this.onGeometryChange(search, f); }.bind(this)); } else { search.checkgeom = false; if (!e.silent) f.getGeometry().setCoordinates(e.coordinate); search.checkgeom = true; } f.set('name', search.getTitle(e.search)); f.set('step', this._search.indexOf(search)); if (f.get('step') === 0) f.set('pos', 'start'); else if (f.get('step') === this._search.length - 1) f.set('pos', 'end'); search.set('selection', e.search); }.bind(this)); search.element.querySelector('input').addEventListener('change', function () { search.set('selection', null); self.resultElement.innerHTML = ''; }); if (this.getMap()) this.getMap().addControl(search); } /** Geometry has changed * @private */ onGeometryChange(search, f, delay) { // Set current geom var lonlat = ol.proj.transform(f.getGeometry().getCoordinates(), this.getMap().getView().getProjection(), 'EPSG:4326'); search._handleSelect({ x: lonlat[0], y: lonlat[1], fulltext: lonlat[0].toFixed(6) + ',' + lonlat[1].toFixed(6) }, true, { silent: true }); // Try to revers geocode if (delay) { search._changeCounter--; if (!search._changeCounter) { search.reverseGeocode(f.getGeometry().getCoordinates(), { silent: true }); return; } } else { search._changeCounter++; setTimeout(function () { this.onGeometryChange(search, f, true); }.bind(this), 1000); } } /** * 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); for (var i = 0; i < this._search.length; i++) { var c = this._search[i]; c.setMap(map); } } /** Get request data * @private */ requestData(steps) { var start = steps[0]; var end = steps[steps.length - 1]; var waypoints = ''; for (var i = 1; i < steps.length - 1; i++) { waypoints += (waypoints ? ';' : '') + steps[i].x + ',' + steps[i].y; } return { resource: 'bdtopo-osrm', profile: this.get('mode') === 'pedestrian' ? 'pedestrian' : 'car', optimization: this.get('method') || 'fastest', start: start.x + ',' + start.y, end: end.x + ',' + end.y, intermediates: waypoints, geometryFormat: 'geojson' }; } /** Gets time as string * @param {*} routing routing response * @return {string} * @api */ getTimeString(t) { t /= 60; return (t < 1) ? '' : (t < 60) ? t.toFixed(0) + ' min' : (t / 60).toFixed(0) + ' h ' + (t % 60).toFixed(0) + ' min'; } /** Gets distance as string * @param {number} d distance * @return {string} * @api */ getDistanceString(d) { return (d < 1000) ? d.toFixed(0) + ' m' : (d / 1000).toFixed(2) + ' km'; } /** Show routing as a list * @private */ listRouting(routing) { this.resultElement.innerHTML = ''; var t = this.getTimeString(routing.duration); t += ' (' + this.getDistanceString(routing.distance) + ')'; var iElement = document.createElement('i'); iElement.textContent = t; this.resultElement.appendChild(iElement); var ul = document.createElement('ul'); this.resultElement.appendChild(ul); var info = { 'none': 'Prendre sur ', 'R': 'Tourner à droite sur ', 'FR': 'Tourner légèrement à droite sur ', 'L': 'Tourner à gauche sur ', 'FL': 'Tourner légèrement à gauche sur ', 'F': 'Continuer tout droit sur ', }; routing.features.forEach(function (f, i) { var d = this.getDistanceString(f.get('distance')); t = this.getTimeString(f.get('durationT')); ol.ext.element.create('LI', { className: f.get('instruction'), html: (info[f.get('instruction') || 'none'] || '#') + ' ' + f.get('name') + '' + d + (t ? ' - ' + t : '') + '', on: { pointerenter: function () { this.dispatchEvent({ type: 'step:hover', hover: false, index: i, feature: f }); }.bind(this), pointerleave: function () { this.dispatchEvent({ type: 'step:hover', hover: false, index: i, feature: f }); }.bind(this) }, click: function () { this.dispatchEvent({ type: 'step:select', index: i, feature: f }); }.bind(this), parent: ul }); }.bind(this)); } /** Handle routing response * @private */ handleResponse(data, start, end) { if (data.status === 'ERROR') { this.dispatchEvent({ type: 'errror', status: '200', statusText: data.message }); return; } // console.log(data) var routing = { type: 'routing' }; routing.features = []; var distance = 0; var duration = 0; var f; var parser = new ol.format.GeoJSON(); var lastPt; for (var i = 0, l; l = data.portions[i]; i++) { for (var j = 0, s; s = l.steps[j]; j++) { /* var options = { geometry: geom.transform('EPSG:4326',this.getMap().getView().getProjection()), name: s.name, instruction: s.navInstruction, distance: parseFloat(s.distanceMeters), duration: parseFloat(s.durationSeconds) } //console.log(duration, options.duration, s) distance += options.distance; duration += options.duration; options.distanceT = distance; options.durationT = duration; f = new ol.Feature(options); */ s.type = 'Feature'; s.properties = s.attributes.name || s.attributes; s.properties.distance = s.distance; s.properties.duration = Math.round(s.duration * 60); // Route info if (s.instruction) { s.properties.instruction_type = s.instruction.type; s.properties.instruction_modifier = s.instruction.modifier; } // Distance / time distance += s.distance; duration += s.duration; s.properties.distanceT = Math.round(distance * 100) / 100; s.properties.durationT = Math.round(duration * 60); s.properties.name = s.properties.cpx_toponyme_route_nommee || s.properties.cpx_toponyme || s.properties.cpx_numero || s.properties.nom_1_droite || s.properties.nom_1_gauche || ''; // TODO: BUG ? var lp = s.geometry.coordinates[s.geometry.coordinates.length - 1]; if (lastPt && !ol.coordinate.equal(lp, s.geometry.coordinates[s.geometry.coordinates.length - 1])) { s.geometry.coordinates.unshift(lastPt); } lastPt = s.geometry.coordinates[s.geometry.coordinates.length - 1]; // f = parser.readFeature(s, { featureProjection: this.getMap().getView().getProjection() }); routing.features.push(f); } } routing.distance = parseFloat(data.distance); routing.duration = parseFloat(data.duration) / 60; // Full route var route = parser.readGeometry(data.geometry, { featureProjection: this.getMap().getView().getProjection() }); routing.feature = new ol.Feature({ geometry: route, start: this._search[0].getTitle(start), end: this._search[0].getTitle(end), distance: routing.distance, duration: routing.duration }); // console.log(data, routing); this.dispatchEvent(routing); this.path = routing; return routing; } /** Abort request */ abort() { // Abort previous request if (this._request) { this._request.abort(); this._request = null; this.dispatchEvent({ type: 'abort' }); } } /** Calculate route * @param {Array|undefined} steps an array of steps in EPSG:4326, default use control input values * @return {boolean} true is a new request is send (more than 2 points to calculate) */ calculate(steps) { this.resultElement.innerHTML = ''; if (steps) { var convert = []; steps.forEach(function (s) { convert.push({ x: s[0], y: s[1] }); }); steps = convert; } else { steps = []; for (var i = 0; i < this._search.length; i++) { if (this._search[i].get('selection')) steps.push(this._search[i].get('selection')); } } if (steps.length < 2) return false; var start = steps[0]; var end = steps[steps.length - 1]; var data = this.requestData(steps); var url = encodeURI(this.get('url')); var parameters = ''; for (var index in data) { parameters += (parameters) ? '&' : '?'; if (data.hasOwnProperty(index)) parameters += index + '=' + data[index]; } var self = this; this.dispatchEvent({ type: 'routing:start' }); this.ajax(url + parameters, function (resp) { if (resp.status >= 200 && resp.status < 400) { self.listRouting(self.handleResponse(JSON.parse(resp.response), start, end)); } else { //console.log(url + parameters, arguments); this.dispatchEvent({ type: 'error', status: resp.status, statusText: resp.statusText }); } }.bind(this), function (resp) { // console.log('ERROR', resp) this.dispatchEvent({ type: 'error', status: resp.status, statusText: resp.statusText }); }.bind(this) ); return true; } /** Send an ajax request (GET) * @param {string} url * @param {function} onsuccess callback * @param {function} onerror callback */ ajax(url, onsuccess, onerror) { var self = this; // Abort previous request if (this._request) { this._request.abort(); } // New request var ajax = this._request = new XMLHttpRequest(); ajax.open('GET', url, true); ajax.timeout = this.get('timeout') || 20000; if (this._auth) { ajax.setRequestHeader("Authorization", "Basic " + this._auth); } this.element.classList.add('ol-searching'); // Load complete ajax.onload = function () { self._request = null; self.element.classList.remove('ol-searching'); onsuccess.call(self, this); }; // Timeout ajax.ontimeout = function () { self._request = null; self.element.classList.remove('ol-searching'); if (onerror) onerror.call(self, this); }; // Oops, TODO do something ? ajax.onerror = function () { self._request = null; self.element.classList.remove('ol-searching'); if (onerror) onerror.call(self, this); }; // GO! ajax.send(); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Scale Control. * A control to display the scale of the center on the map * * @constructor * @extends {ol.control.Control} * @fires select * @fires change:input * @param {Object=} options * @param {string} options.className control class name * @param {string} options.ppi screen ppi, default 96 * @param {string} options.editable make the control editable, default true */ ol.control.Scale = class olcontrolScale extends ol.control.Control { constructor(options) { options = options || {}; if (options.typing === undefined) options.typing = 300; var element = document.createElement("DIV"); var classNames = (options.className || "") + " ol-scale"; if (!options.target) { classNames += " ol-unselectable ol-control"; } super({ element: element, target: options.target }); this._input = document.createElement("INPUT"); this._input.value = '-'; element.setAttribute('class', classNames); if (options.editable === false) this._input.readOnly = true; element.appendChild(this._input); this._input.addEventListener("change", this.setScale.bind(this)); this.set('ppi', options.ppi || 96); } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._listener) ol.Observable.unByKey(this._listener); this._listener = null; super.setMap(map); // Get change (new layer added or removed) if (map) { this._listener = map.on('moveend', this.getScale.bind(this)); } } /** Display the scale */ getScale() { var map = this.getMap(); if (map) { var d = ol.sphere.getMapScale(map, this.get('ppi')); this._input.value = this.formatScale(d); return d; } } /** Format the scale 1/d * @param {Number} d * @return {string} formated string */ formatScale(d) { if (d > 100) d = Math.round(d / 100) * 100; else d = Math.round(d); return '1 / ' + d.toLocaleString(); } /** Set the current scale (will change the scale of the map) * @param {Number} value the scale factor */ setScale(value) { var map = this.getMap(); if (map && value) { if (value.target) value = value.target.value; ol.sphere.setMapScale(map, value, this.get('ppi')); } this.getScale(); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search places using the French National Base Address (BAN) API. * * @constructor * @extends {ol.control.Search} * @fires select * @param {Object=} Control options. * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.reverseTitle Title to use for the reverse geocoding button tooltip, default "Click on the map..." * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 500. * @param {integer | undefined} options.minLength minimum length to start searching, default 3 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * * @param {string|undefined} options.url Url to BAN api, default "https://api-adresse.data.gouv.fr/search/" * @param {boolean} options.position Search, with priority to geo position, default false * @param {function} options.getTitle a function that takes a feature and return the text to display in the menu, default return label attribute * @param {string|undefined} options.citycode limit search to an administrative area defined by its city code (code commune insee) * @param {string|undefined} options.postcode limit search to a postal code * @param {string|undefined} options.type type of result: 'housenumber' | 'street' * @see {@link https://adresse.data.gouv.fr/api/} */ ol.control.SearchBAN = class olcontrolSearchBAN extends ol.control.SearchPhoton { constructor(options) { options = options || {}; options.typing = options.typing || 500; options.url = options.url || 'https://api-adresse.data.gouv.fr/search/'; options.className = options.className || 'BAN'; options.copy = '© BAN-data.gouv.fr'; super(options); this.set('postcode', options.postcode); this.set('citycode', options.citycode); this.set('type', options.type); } /** Returns the text to be displayed in the menu * @param {ol.Feature} f the feature * @return {string} the text to be displayed in the index * @api */ getTitle(f) { var p = f.properties; return (p.label); } /** A ligne has been clicked in the menu > dispatch event * @param {any} f the feature, as passed in the autocomplete * @api */ select(f) { var c = f.geometry.coordinates; // Add coordinate to the event try { c = ol.proj.transform(f.geometry.coordinates, 'EPSG:4326', this.getMap().getView().getProjection()); } catch (e) { /* ok */ } this.dispatchEvent({ type: "select", search: f, coordinate: c }); } requestData(s) { var data = super.requestData(s); data.postcode = this.get('postcode'); data.citycode = this.get('citycode'); data.type = this.get('type'); return data; } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search on DFCI grid. * * @constructor * @extends {ol.control.Search} * @fires select * @param {Object=} Control options. * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 300. * @param {integer | undefined} options.minLength minimum length to start searching, default 1 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * * @param {string | undefined} options.property a property to display in the index, default 'name'. * @param {function} options.getTitle a function that takes a feature and return the name to display in the index, default return the property * @param {function | undefined} options.getSearchString a function that take a feature and return a text to be used as search string, default geTitle() is used as search string */ ol.control.SearchDFCI = class olcontrolSearchDFCI extends ol.control.Search { constructor(options) { options = options || {}; options.className = options.className || 'dfci'; options.placeholder = options.placeholder || 'Code DFCI'; super(options); } /** Autocomplete function * @param {string} s search string * @return {Array|false} an array of search solutions or false if the array is send with the cback argument (asnchronous) * @api */ autocomplete(s) { s = s.toUpperCase(); s = s.replace(/[^0-9,^A-H,^K-N]/g, ''); if (s.length < 2) { this.setInput(s); return []; } var i; var proj = this.getMap().getView().getProjection(); var result = []; var c = ol.coordinate.fromDFCI(s, proj); var level = Math.floor(s.length / 2) - 1; var dfci = ol.coordinate.toDFCI(c, level, proj); dfci = dfci.replace(/[^0-9,^A-H,^K-N]/g, ''); // Valid DFCI ? if (!/NaN/.test(dfci) && dfci) { console.log('ok', dfci); this.setInput(dfci + s.substring(dfci.length, s.length)); result.push({ coordinate: ol.coordinate.fromDFCI(dfci, proj), name: dfci }); if (s.length === 5) { c = ol.coordinate.fromDFCI(s + 0, proj); dfci = (ol.coordinate.toDFCI(c, level + 1, proj)).substring(0, 5); for (i = 0; i < 10; i++) { result.push({ coordinate: ol.coordinate.fromDFCI(dfci + i, proj), name: dfci + i }); } } if (level === 2) { for (i = 0; i < 6; i++) { result.push({ coordinate: ol.coordinate.fromDFCI(dfci + '.' + i, proj), name: dfci + '.' + i }); } } } return result; } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search features. * * @constructor * @extends {ol.control.Search} * @fires select * @param {Object=} Control options. * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 300. * @param {integer | undefined} options.minLength minimum length to start searching, default 1 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * * @param {string | undefined} options.property a property to display in the index, default 'name'. * @param {function} options.getTitle a function that takes a feature and return the name to display in the index, default return the property * @param {function | undefined} options.getSearchString a function that take a feature and return a text to be used as search string, default geTitle() is used as search string * @param {function | undefined} options.sort a function to sort autocomplete list. Takes 2 features and return 0, -1 or 1. */ ol.control.SearchFeature = class olcontrolSearchFeature extends ol.control.Search { constructor(options) { options = options || {}; options.className = options.className || 'feature'; super(options); if (typeof (options.getSearchString) == "function") { this.getSearchString = options.getSearchString; } this.set('property', options.property || 'name'); this.source_ = options.source; this._sort = options.sort; } /** No history avaliable on features */ restoreHistory() { this.set('history', []); } /** No history avaliable on features */ saveHistory() { try { localStorage.removeItem("ol@search-" + this._classname); } catch (e) { console.warn('Failed to access localStorage...'); } } /** Returns the text to be displayed in the menu * @param {ol.Feature} f the feature * @return {string} the text to be displayed in the index * @api */ getTitle(f) { return f.get(this.get('property') || 'name'); } /** Return the string to search in * @param {ol.Feature} f the feature * @return {string} the text to be used as search string * @api */ getSearchString(f) { return this.getTitle(f); } /** Get the source * @return {ol.source.Vector} * @api */ getSource() { return this.source_; } /** Get the source * @param {ol.source.Vector} source * @api */ setSource(source) { this.source_ = source; } /** Set function to sort autocomplete results * @param {function} sort a sort function that takes 2 features and returns 0, -1 or 1 */ setSortFunction(sort) { this._sort = sort } /** Autocomplete function * @param {string} s search string * @param {int} max max * @param {function} cback a callback function that takes an array to display in the autocomplete field (for asynchronous search) * @return {Array|false} an array of search solutions or false if the array is send with the cback argument (asnchronous) * @api */ autocomplete(s) { var result = []; if (this.source_) { // regexp s = s.replace(/^\*/, ''); var rex = new RegExp(s, 'i'); // The source var features = this.source_.getFeatures(); var max = this.get('maxItems'); for (var i = 0, f; f = features[i]; i++) { var att = this.getSearchString(f); if (att !== undefined && rex.test(att)) { result.push(f); if ((--max) <= 0) break; } } } if (typeof(this._sort) === 'function') { result = result.sort(this._sort) } return result; } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search on GPS coordinate. * * @constructor * @extends {ol.control.Search} * @fires select * @param {Object=} Control options. * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 300. * @param {integer | undefined} options.minLength minimum length to start searching, default 1 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 */ ol.control.SearchGPS = class olcontrolSearchGPS extends ol.control.Search { constructor(options) { options = options || {}; options.className = (options.className || '') + ' ol-searchgps'; options.placeholder = options.placeholder || 'lon,lat'; super(options); // Geolocation this.geolocation = new ol.Geolocation({ projection: "EPSG:4326", trackingOptions: { maximumAge: 10000, enableHighAccuracy: true, timeout: 600000 } }); ol.ext.element.create('BUTTON', { className: 'ol-geoloc', title: 'Locate with GPS', parent: this.element, click: function () { this.geolocation.setTracking(true); }.bind(this) }); // DMS switcher ol.ext.element.createSwitch({ html: 'decimal', after: 'DMS', change: function (e) { if (e.target.checked) this.element.classList.add('ol-dms'); else this.element.classList.remove('ol-dms'); }.bind(this), parent: this.element }); this._createForm(); // Move list to the end var ul = this.element.querySelector("ul.autocomplete"); this.element.appendChild(ul); } /** Create input form * @private */ _createForm() { // Value has change var onchange = function (e) { if (e.target.classList.contains('ol-dms')) { lon.value = (lond.value < 0 ? -1 : 1) * Number(lond.value) + Number(lonm.value) / 60 + Number(lons.value) / 3600; lon.value = (lond.value < 0 ? -1 : 1) * Math.round(lon.value * 10000000) / 10000000; lat.value = (latd.value < 0 ? -1 : 1) * Number(latd.value) + Number(latm.value) / 60 + Number(lats.value) / 3600; lat.value = (latd.value < 0 ? -1 : 1) * Math.round(lat.value * 10000000) / 10000000; } if (lon.value || lat.value) { this._input.value = lon.value + ',' + lat.value; } else { this._input.value = ''; } if (!e.target.classList.contains('ol-dms')) { var s = ol.coordinate.toStringHDMS([Number(lon.value), Number(lat.value)]); var c = s.replace(/(N|S|E|W)/g, '').split('″'); c[1] = c[1].trim().split(' '); lond.value = (/W/.test(s) ? -1 : 1) * parseInt(c[1][0]); lonm.value = parseInt(c[1][1]); lons.value = parseInt(c[1][2]); c[0] = c[0].trim().split(' '); latd.value = (/W/.test(s) ? -1 : 1) * parseInt(c[0][0]); latm.value = parseInt(c[0][1]); lats.value = parseInt(c[0][2]); } this.search(); }.bind(this); function createInput(className, unit) { var input = ol.ext.element.create('INPUT', { className: className, type: 'number', step: 'any', lang: 'en', parent: div, on: { 'change keyup': onchange } }); if (unit) { ol.ext.element.create('SPAN', { className: 'ol-dms', html: unit, parent: div, }); } return input; } // Longitude var div = ol.ext.element.create('DIV', { className: 'ol-longitude', parent: this.element }); ol.ext.element.create('LABEL', { html: 'Longitude', parent: div }); var lon = createInput('ol-decimal'); var lond = createInput('ol-dms', '°'); var lonm = createInput('ol-dms', '\''); var lons = createInput('ol-dms', '"'); // Latitude div = ol.ext.element.create('DIV', { className: 'ol-latitude', parent: this.element }); ol.ext.element.create('LABEL', { html: 'Latitude', parent: div }); var lat = createInput('ol-decimal'); var latd = createInput('ol-dms', '°'); var latm = createInput('ol-dms', '\''); var lats = createInput('ol-dms', '"'); // Focus on open if (this.button) { this.button.addEventListener("click", function () { lon.focus(); }); } // Change value on click this.on('select', function (e) { lon.value = e.search.gps[0]; lat.value = e.search.gps[1]; }.bind(this)); // Change value on geolocation this.geolocation.on('change', function () { this.geolocation.setTracking(false); var coord = this.geolocation.getPosition(); lon.value = coord[0]; lat.value = coord[1]; this._triggerCustomEvent('keyup', lon); }.bind(this)); } /** Autocomplete function * @param {string} s search string * @return {Array|false} an array of search solutions * @api */ autocomplete(s) { var result = []; var c = s.split(','); c[0] = Number(c[0]); c[1] = Number(c[1]); // Name s = ol.coordinate.toStringHDMS(c); if (s) s = s.replace(/(°|′|″) /g, '$1'); // var coord = ol.proj.transform([c[0], c[1]], 'EPSG:4326', this.getMap().getView().getProjection()); result.push({ gps: c, coordinate: coord, name: s }); return result; } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search places using the French National Base Address (BAN) API. * * @constructor * @extends {ol.control.SearchJSON} * @fires select * @param {any} options extend ol.control.SearchJSON options * @param {string} options.className control class name * @param {boolean | undefined} [options.apiKey] the service api key. * @param {string | undefined} options.authentication: basic authentication for the service API as btoa("login:pwd") * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 500. * @param {integer | undefined} options.minLength minimum length to start searching, default 3 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * * @param {Number} options.pageSize item per page for parcelle list paging, use -1 for no paging, default 5 * @see {@link https://geoservices.ign.fr/documentation/geoservices/geocodage.html} */ ol.control.SearchGeoportailParcelle = class olcontrolSearchGeoportailParcelle extends ol.control.SearchGeoportail { constructor(options) { options.type = 'Commune'; options.className = (options.className ? options.className : "") + " IGNF-parcelle ol-collapsed-list ol-collapsed-num"; options.inputLabel = 'Commune'; options.noCollapse = true; options.placeholder = options.placeholder || "Choisissez une commune..."; super(options); this.set('copy', null); var element = this.element; var self = this; // Add parcel form var div = document.createElement("DIV"); element.appendChild(div); var label = document.createElement("LABEL"); label.innerText = 'Préfixe'; div.appendChild(label); label = document.createElement("LABEL"); label.innerText = 'Section'; div.appendChild(label); label = document.createElement("LABEL"); label.innerText = 'Numéro'; div.appendChild(label); div.appendChild(document.createElement("BR")); // Input this._inputParcelle = { prefix: document.createElement("INPUT"), section: document.createElement("INPUT"), numero: document.createElement("INPUT") }; this._inputParcelle.prefix.setAttribute('maxlength', 3); this._inputParcelle.section.setAttribute('maxlength', 2); this._inputParcelle.numero.setAttribute('maxlength', 4); // Delay search var tout; var doSearch = function () { if (tout) clearTimeout(tout); tout = setTimeout(function () { self.autocompleteParcelle(); }, options.typing || 0); }; // Add inputs for (var i in this._inputParcelle) { div.appendChild(this._inputParcelle[i]); this._inputParcelle[i].addEventListener('keyup', doSearch); this._inputParcelle[i].addEventListener('blur', function () { tout = setTimeout(function () { element.classList.add('ol-collapsed-num'); }, 200); }); this._inputParcelle[i].addEventListener('focus', function () { clearTimeout(tout); element.classList.remove('ol-collapsed-num'); }); } this.activateParcelle(false); // Autocomplete list var auto = document.createElement('DIV'); auto.className = 'autocomplete-parcelle'; element.appendChild(auto); var ul = document.createElement('UL'); ul.classList.add('autocomplete-parcelle'); auto.appendChild(ul); ul = document.createElement('UL'); ul.classList.add('autocomplete-page'); auto.appendChild(ul); // Show/hide list on fcus/blur this._input.addEventListener('blur', function () { setTimeout(function () { element.classList.add('ol-collapsed-list'); }, 200); }); this._input.addEventListener('focus', function () { element.classList.remove('ol-collapsed-list'); self._listParcelle([]); if (self._commune) { self._commune = null; self._input.value = ''; self.drawList_(); } self.activateParcelle(false); }); this.on('select', this.selectCommune.bind(this)); this.set('pageSize', options.pageSize || 5); } /** Select a commune => start searching parcelle * @param {any} e * @private */ selectCommune(e) { this._commune = e.search.insee || e.sear; this._input.value = e.search.insee + ' - ' + e.search.fulltext; this.activateParcelle(true); this._inputParcelle.numero.focus(); this.autocompleteParcelle(); } /** Set the input parcelle * @param {*} p parcel * @param {string} p.Commune * @param {string} p.CommuneAbsorbee * @param {string} p.Section * @param {string} p.Numero * @param {boolean} search start a search */ setParcelle(p, search) { this._inputParcelle.prefix.value = (p.Commune || '') + (p.CommuneAbsorbee || ''); this._inputParcelle.section.value = p.Section || ''; this._inputParcelle.numero.value = p.Numero || ''; if (search) { this._triggerCustomEvent("keyup", this._inputParcelle.prefix); } } /** Activate parcelle inputs * @param {bolean} b */ activateParcelle(b) { for (var i in this._inputParcelle) { this._inputParcelle[i].readOnly = !b; } if (b) { this._inputParcelle.section.parentElement.classList.add('ol-active'); } else { this._inputParcelle.section.parentElement.classList.remove('ol-active'); } } /** Send search request for the parcelle * @private */ autocompleteParcelle() { // Add 0 to fit the format function complete(s, n, c) { if (!s) return s; c = c || '0'; while (s.length < n) { s = c + s; } return s.replace(/\*/g, '_'); } // The selected commune var commune = this._commune; var prefix = complete(this._inputParcelle.prefix.value, 3); if (prefix === '000') { prefix = '___'; } // Get parcelle number var section = complete(this._inputParcelle.section.value, 2); var numero = complete(this._inputParcelle.numero.value, 4, '0'); this.searchParcelle(commune, prefix, section, numero, function (jsonResp) { this._listParcelle(jsonResp); }.bind(this), function () { console.log('oops'); }); } /** Send search request for a parcelle number * @param {string} search search parcelle number * @param {function} success callback function called on success * @param {function} error callback function called on error */ searchParcelle(commune, prefix, section, numero, success /*, error */) { // Search url var url = this.get('url').replace('ols/apis/completion', 'geoportail/ols').replace('completion', 'search'); // v2 ? if (/ols/.test(url)) { var search = commune + (prefix || '___') + (section || "__") + (numero ? numero : section ? "____" : "0001"); // Request var request = '' + '' + '' + '' + '' + '
' + '' + search + '+' + '
' + '
' + '
' + '
'; // Geocode this.ajax( this.get('url').replace('ols/apis/completion', 'geoportail/ols'), { xls: request }, function (xml) { // XML to JSON var parser = new DOMParser(); var xmlDoc = parser.parseFromString(xml, "text/xml"); var parcelles = xmlDoc.getElementsByTagName('GeocodedAddress'); var jsonResp = []; for (var i = 0, parc; parc = parcelles[i]; i++) { var node = parc.getElementsByTagName('gml:pos')[0] || parc.getElementsByTagName('pos')[0]; var p = node.childNodes[0].nodeValue.split(' '); var att = parc.getElementsByTagName('Place'); var json = { lon: Number(p[1]), lat: Number(p[0]) }; for (var k = 0, a; a = att[k]; k++) { json[a.attributes.type.value] = a.childNodes[0].nodeValue; } jsonResp.push(json); } console.log(jsonResp) success(jsonResp); }, { dataType: 'XML' } ); } else { this.ajax(url + '?index=parcel&q=' + '&departmentcode=' + commune.substr(0,2) + '&municipalitycode=' + commune.substr(-3) + (prefix ? '&oldmunicipalitycode=' + prefix.replace(/_/g, '0') : '') + (section ? '§ion=' + section : '') + (numero ? '&number=' + numero : '') + '&limit=20', {}, function(resp) { var jsonResp = []; resp.features.forEach(function(f) { var prop = f.properties; jsonResp.push({ id: prop.id, INSEE: prop.departmentcode + prop.municipalitycode, Commune: prop.municipalitycode, Departement: prop.departmentcode, CommuneAbsorbee: prop.oldmunicipalitycode, Section: prop.section, Numero: prop.number, Municipality: prop.city, Feuille: prop.sheet, lon: f.geometry.coordinates[0], lat: f.geometry.coordinates[1], }) }) success(jsonResp); } ) } } /** * Draw the autocomplete list * @param {*} resp * @private */ _listParcelle(resp) { var self = this; var ul = this.element.querySelector("ul.autocomplete-parcelle"); ul.innerHTML = ''; var page = this.element.querySelector("ul.autocomplete-page"); page.innerHTML = ''; this._listParc = []; // Show page i function showPage(i) { var l = ul.children; var visible = "ol-list-" + i; var k; for (k = 0; k < l.length; k++) { l[k].style.display = (l[k].className === visible) ? '' : 'none'; } l = page.children; for (k = 0; k < l.length; k++) { l[k].className = (l[k].innerText == i) ? 'selected' : ''; } page.style.display = l.length > 1 ? '' : 'none'; } // Sort table resp.sort(function (a, b) { var na = a.INSEE + a.CommuneAbsorbee + a.Section + a.Numero; var nb = b.INSEE + b.CommuneAbsorbee + b.Section + b.Numero; return na === nb ? 0 : na < nb ? -1 : 1; }); // Show list var n = this.get('pageSize'); for (var i = 0, r; r = resp[i]; i++) { var li = document.createElement("LI"); li.setAttribute("data-search", i); if (n > 0) li.classList.add("ol-list-" + Math.floor(i / n)); this._listParc.push(r); li.addEventListener("click", function (e) { self._handleParcelle(self._listParc[e.currentTarget.getAttribute("data-search")]); }); li.innerHTML = r.INSEE + r.CommuneAbsorbee + r.Section + r.Numero; ul.appendChild(li); // if (n > 0 && !(i % n)) { li = document.createElement("LI"); li.innerText = Math.floor(i / n); li.addEventListener("click", function (e) { showPage(e.currentTarget.innerText); }); page.appendChild(li); } } if (n > 0) showPage(0); } /** * Handle parcelle section * @param {*} parc * @private */ _handleParcelle(parc) { this.dispatchEvent({ type: "parcelle", search: parc, coordinate: ol.proj.fromLonLat([parc.lon, parc.lat], this.getMap().getView().getProjection()) }); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search places using the Nominatim geocoder from the OpenStreetmap project. * * @constructor * @extends {ol.control.Search} * @fires select * @param {Object=} Control options. * @param {string} options.className control class name * @param {boolean | undefined} options.polygon To get output geometry of results (in geojson format), default false. * @param {Array | undefined} options.viewbox The preferred area to find search results. Any two corner points of the box are accepted in any order as long as they span a real box, default none. * @param {boolean | undefined} options.bounded Restrict the results to only items contained with the bounding box. Restricting the results to the bounding box also enables searching by amenity only. default false * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.title Title to use for the search button tooltip, default "Search" * @param {string | undefined} options.reverseTitle Title to use for the reverse geocoding button tooltip, default "Click on the map..." * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start autocompletion (ms), default -1 (disabled). NB: default nominatim policy forbids auto-complete usage... * @param {integer | undefined} options.minLength minimum length to start searching, default 3 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * * @param {string|undefined} options.url URL to Nominatim API, default "https://nominatim.openstreetmap.org/search" * @see {@link https://wiki.openstreetmap.org/wiki/Nominatim} */ ol.control.SearchNominatim = class olcontrolSearchNominatim extends ol.control.SearchJSON { constructor(options) { options = options || {}; options.className = options.className || 'nominatim'; options.typing = options.typing || -1; options.url = options.url || 'https://nominatim.openstreetmap.org/search'; options.copy = '© OpenStreetMap contributors'; super(options); this.set('polygon', options.polygon); this.set('viewbox', options.viewbox); this.set('bounded', options.bounded); } /** Returns the text to be displayed in the menu * @param {ol.Feature} f the feature * @return {string} the text to be displayed in the index * @api */ getTitle(f) { var info = []; if (f.class) info.push(f.class); if (f.type) info.push(f.type); var title = f.display_name + (info.length ? "" + info.join(' - ') + "" : ''); if (f.icon) title = "" + title; return (title); } /** * @param {string} s the search string * @return {Object} request data (as key:value) * @api */ requestData(s) { var data = { format: "json", addressdetails: 1, q: s, polygon_geojson: this.get('polygon') ? 1 : 0, bounded: this.get('bounded') ? 1 : 0, limit: this.get('maxItems') }; if (this.get('viewbox')) data.viewbox = this.get('viewbox'); return data; } /** A ligne has been clicked in the menu > dispatch event * @param {any} f the feature, as passed in the autocomplete * @api */ select(f) { var c = [Number(f.lon), Number(f.lat)]; // Add coordinate to the event try { c = ol.proj.transform(c, 'EPSG:4326', this.getMap().getView().getProjection()); } catch (e) { /* ok */ } this.dispatchEvent({ type: "select", search: f, coordinate: c }); } /** Reverse geocode * @param {ol.coordinate} coord * @api */ reverseGeocode(coord, cback) { var lonlat = ol.proj.transform(coord, this.getMap().getView().getProjection(), 'EPSG:4326'); this.ajax( this.get('url').replace('search', 'reverse'), { lon: lonlat[0], lat: lonlat[1], format: 'json' }, function (resp) { if (cback) { cback.call(this, [resp]); } else { if (resp && !resp.error) { this._handleSelect(resp, true); } //this.setInput('', true); } }.bind(this) ); } /** * Handle server response to pass the features array to the display list * @param {any} response server response * @return {Array} an array of feature * @api */ handleResponse(response) { return response.results || response; } /**/ } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Search places using the MediaWiki API. * @see https://www.mediawiki.org/wiki/API:Main_page * * @constructor * @extends {ol.control.SearchJSON} * @fires select * @param {Object=} Control options. * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.label Text label to use for the search button, default "search" * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 1000. * @param {integer | undefined} options.minLength minimum length to start searching, default 3 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * @param {function | undefined} options.handleResponse Handle server response to pass the features array to the list * * @param {string|undefined} options.lang API language, default none */ ol.control.SearchWikipedia = class olcontrolSearchWikipedia extends ol.control.SearchJSON { constructor(options) { options = options || {}; options.lang = options.lang || 'en'; options.className = options.className || 'ol-search-wikipedia'; options.url = 'https://' + options.lang + '.wikipedia.org/w/api.php'; options.placeholder = options.placeholder || 'search string, File:filename'; options.copy = 'Wikipedia® - CC-By-SA'; super(options); this.set('lang', options.lang); } /** Returns the text to be displayed in the menu * @param {ol.Feature} f the feature * @return {string} the text to be displayed in the index * @api */ getTitle(f) { return ol.ext.element.create('DIV', { html: f.title, title: f.desc }); //return f.desc; } /** Set the current language * @param {string} lang the current language as ISO string (en, fr, de, es, it, ja, ...) */ setLang(lang) { this.set('lang', lang); this.set('url', 'https://' + lang + '.wikipedia.org/w/api.php'); } /** * @param {string} s the search string * @return {Object} request data (as key:value) * @api */ requestData(s) { var data = { action: 'opensearch', search: s, lang: this.get('lang'), format: 'json', origin: '*', limit: this.get('maxItems') }; return data; } /** * Handle server response to pass the features array to the list * @param {any} response server response * @return {Array} an array of feature */ handleResponse(response) { var features = []; for (var i = 0; i < response[1].length; i++) { features.push({ title: response[1][i], desc: response[2][i], uri: response[3][i] }); } return features; } /** A ligne has been clicked in the menu query for more info and disatch event * @param {any} f the feature, as passed in the autocomplete * @api */ select(f) { var title = decodeURIComponent(f.uri.split('/').pop()).replace(/'/, '%27'); // Search for coords ol.ext.Ajax.get({ url: f.uri.split('wiki/')[0] + 'w/api.php', data: { action: 'query', prop: 'pageimages|coordinates|extracts', exintro: 1, explaintext: 1, piprop: 'original', origin: '*', format: 'json', redirects: 1, titles: title }, options: { encode: false }, success: function (e) { var page = e.query.pages[Object.keys(e.query.pages).pop()]; console.log(page); var feature = { title: f.title, desc: page.extract || f.desc, url: f.uri, img: page.original ? page.original.source : undefined, pageid: page.pageid }; var c; if (page.coordinates) { feature.lon = page.coordinates[0].lon; feature.lat = page.coordinates[0].lat; c = [feature.lon, feature.lat]; c = ol.proj.transform(c, 'EPSG:4326', this.getMap().getView().getProjection()); } this.dispatchEvent({ type: "select", search: feature, coordinate: c }); }.bind(this) }); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Select Control. * A control to select features by attributes * * @constructor * @extends {ol.control.SelectBase} * @fires select * @param {Object=} options * @param {string} options.className control class name * @param {Element | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {ol.source.Vector | Array} options.source the source to search in * @param {string} [options.selectLabel=select] select button label * @param {string} [options.addLabel=add] add button label * @param {string} [options.caseLabel=case sensitive] case checkbox label * @param {string} [options.allLabel=match all] match all checkbox label * @param {string} [options.attrPlaceHolder=attribute] * @param {string} [options.valuePlaceHolder=value] */ ol.control.Select = class olcontrolSelect extends ol.control.SelectBase { constructor(options) { options = options || {}; // Container var div = options.content = document.createElement('div'); super(options); var bt = div.querySelector('button'); // Autocompletion list this._ul = ol.ext.element.create('UL', { parent: div }); // All conditions this._all = ol.ext.element.create('INPUT', { type: 'checkbox', checked: true }); var label_match_all = ol.ext.element.create('LABEL', { html: this._all, parent: div }); ol.ext.element.appendText(label_match_all, options.allLabel || 'match all'); // Use case this._useCase = ol.ext.element.create('INPUT', { type: 'checkbox' }); var label_case_sensitive = ol.ext.element.create('LABEL', { html: this._useCase, parent: div }); ol.ext.element.appendText(label_case_sensitive, options.caseLabel || 'case sensitive'); // Add ok button at the end div.appendChild(bt); // Add button ol.ext.element.create('BUTTON', { className: 'ol-append', html: options.addLabel || 'add rule', click: function () { this.addCondition(); }.bind(this), parent: div }); this._conditions = []; this.set('attrPlaceHolder', options.attrPlaceHolder || 'attribute'); this.set('valuePlaceHolder', options.valuePlaceHolder || 'value'); this.addCondition(); } /** Add a new condition * @param {*} options * @param {string} options.attr attribute name * @param {string} options.op operator * @param {string} options.val attribute value */ addCondition(options) { options = options || {}; this._conditions.push({ attr: options.attr || '', op: options.op || '=', val: options.val || '' }); this._drawlist(); } /** Get the condition list */ getConditions() { return { usecase: this._useCase.checked, all: this._all.checked, conditions: this._conditions }; } /** Set the condition list */ setConditions(cond) { this._useCase.checked = cond.usecase; this._all.checked = cond.all; this._conditions = cond.conditions; this._drawlist(); } /** Get the conditions as string */ getConditionsString(cond) { var st = ''; for (var i = 0, c; c = cond.conditions[i]; i++) { if (c.attr) { st += (st ? (cond.all ? ' AND ' : ' OR ') : '') + c.attr + this.operationsList[c.op] + c.val; } } return st; } /** Draw the liste * @private */ _drawlist() { this._ul.innerHTML = ''; for (var i = 0; i < this._conditions.length; i++) { this._ul.appendChild(this._getLiCondition(i)); } } /** Get a line * @return {*} * @private */ _autocomplete(val, ul) { ul.classList.remove('ol-hidden'); ul.innerHTML = ''; var attributes = {}; var sources = this.get('source'); for (var i = 0, s; s = sources[i]; i++) { var features = s.getFeatures(); for (var j = 0, f; f = features[j]; j++) { Object.assign(attributes, f.getProperties()); if (j > 100) break; } } var rex = new RegExp(val, 'i'); for (var a in attributes) { if (a === 'geometry') continue; if (rex.test(a)) { var li = document.createElement('li'); li.textContent = a; li.addEventListener("click", function () { ul.previousElementSibling.value = this.textContent; var event = document.createEvent('HTMLEvents'); event.initEvent('change', true, false); ul.previousElementSibling.dispatchEvent(event); ul.classList.add('ol-hidden'); }); ul.appendChild(li); } } } /** Get a line * @return {*} * @private */ _getLiCondition(i) { var self = this; var li = document.createElement('li'); // Attribut var autocomplete = document.createElement('div'); autocomplete.classList.add('ol-autocomplete'); autocomplete.addEventListener("mouseleave", function () { this.querySelector('ul').classList.add('ol-hidden'); }); li.appendChild(autocomplete); var input_attr = document.createElement('input'); input_attr.classList.add('ol-attr'); input_attr.setAttribute('type', 'search'); input_attr.setAttribute('placeholder', this.get('attrPlaceHolder')); input_attr.addEventListener('keyup', function () { self._autocomplete(this.value, this.nextElementSibling); }); input_attr.addEventListener('focusout', function () { setTimeout(function () { autocomplete.querySelector('ul').classList.add('ol-hidden'); }, 300); }); input_attr.addEventListener('click', function () { setTimeout(function () { self._autocomplete(this.value, this.nextElementSibling); this.nextElementSibling.classList.remove('ol-hidden'); }.bind(this)); }); input_attr.addEventListener('change', function () { self._conditions[i].attr = this.value; }); input_attr.value = self._conditions[i].attr; autocomplete.appendChild(input_attr); // Autocomplete list var ul_autocomplete = document.createElement('ul'); ul_autocomplete.classList.add('ol-hidden'); autocomplete.appendChild(ul_autocomplete); // Operation var select = document.createElement('select'); li.appendChild(select); for (var k in this.operationsList) { var option = document.createElement('option'); option.value = k; option.textContent = this.operationsList[k]; select.appendChild(option); } select.value = self._conditions[i].op; select.addEventListener('change', function () { self._conditions[i].op = this.value; }); // Value var input_value = document.createElement('input'); input_value.setAttribute('type', 'text'); input_value.setAttribute('placeholder', this.get('valuePlaceHolder')); input_value.addEventListener('change', function () { self._conditions[i].val = this.value; }); input_value.value = self._conditions[i].val; li.appendChild(input_value); if (this._conditions.length > 1) { var div_delete = document.createElement('div'); div_delete.classList.add('ol-delete'); div_delete.addEventListener("click", function () { self.removeCondition(i); }); li.appendChild(div_delete); } // return li; } /** Remove the ith condition * @param {int} i condition index */ removeCondition(i) { this._conditions.splice(i, 1); this._drawlist(); } /** Select features by attributes * @param {*} options * @param {Array|undefined} options.sources source to apply rules, default the select sources * @param {bool} options.useCase case sensitive, default checkbox state * @param {bool} options.matchAll match all conditions, , default checkbox state * @param {Array} options.conditions array of conditions * @fires select */ doSelect(options) { options = options || {}; options.useCase = options.useCase || this._useCase.checked; options.matchAll = options.matchAll || this._all.checked; options.conditions = options.conditions || this._conditions; return super.doSelect(options); } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Select features by property using a popup * * @constructor * @extends {ol.control.SelectBase} * @fires select * @param {Object=} options * @param {string} options.className control class name * @param {Element | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {ol/source/Vector | Array
    } options.source the source to search in * @param {string} options.property property to select on * @param {string} options.label control label * @param {number} options.max max feature to test to get the values, default 10000 * @param {number} options.selectAll select all features if no option selected * @param {string} options.type check type: checkbox or radio, default checkbox * @param {number} options.defaultLabel label for the default radio button * @param {function|undefined} options.onchoice function triggered when an option is clicked, default doSelect */ ol.control.SelectCheck = class olcontrolSelectCheck extends ol.control.SelectBase { constructor(options) { options = options || {}; // Container var div = options.content = ol.ext.element.create('DIV'); if (options.label) { ol.ext.element.create('LABEL', { html: options.label, parent: div }); } options.className = options.className || 'ol-select-check'; super(options); var bt = div.querySelector('button'); // Input div this._input = ol.ext.element.create('DIV', { parent: div }); // Add ok button at the end div.appendChild(bt); this.set('property', options.property || 'name'); this.set('max', options.max || 10000); this.set('defaultLabel', options.defaultLabel); this.set('type', options.type); this._selectAll = options.selectAll; this._onchoice = options.onchoice; // Set select options if (options.values) { this.setValues({ values: options.values, sort: true }); } else { this.setValues(); } } /** * Set the map instance the control associated with. * @param {o.Map} map The map instance. */ setMap(map) { super.setMap(map); this.setValues(); } /** Select features by attributes */ doSelect(options) { options = options || {}; var conditions = []; this._checks.forEach(function (c) { if (c.checked) { if (c.value) { conditions.push({ attr: this.get('property'), op: '=', val: c.value }); } } }.bind(this)); if (!conditions.length) { return super.doSelect({ features: options.features, matchAll: this._selectAll }); } else { return super.doSelect({ features: options.features, conditions: conditions }); } } /** Set the popup values * @param {Object} options * @param {Object} options.values a key/value list with key = property value, value = title shown in the popup, default search values in the sources * @param {boolean} options.sort sort values */ setValues(options) { options = options || {}; var values, vals; if (options.values) { if (options.values instanceof Array) { vals = {}; options.values.forEach(function (v) { vals[v] = v; }); } else { vals = options.values; } } else { vals = {}; var prop = this.get('property'); this.getSources().forEach(function (s) { var features = s.getFeatures(); var max = Math.min(features.length, this.get('max')); for (var i = 0; i < max; i++) { var p = features[i].get(prop); if (p) vals[p] = p; } }.bind(this)); } if (!Object.keys(vals).length) return; if (options.sort) { values = {}; Object.keys(vals).sort().forEach(function (key) { values[key] = vals[key]; }); } else { values = vals; } ol.ext.element.setHTML(this._input, ''); this._checks = []; var id = 'radio_' + (new Date().getTime()); var addCheck = function (val, info) { this._checks.push(ol.ext.element.createCheck({ after: info, name: id, val: val, type: this.get('type'), change: function () { if (this._onchoice) this._onchoice(); else this.doSelect(); }.bind(this), parent: this._input })); }.bind(this); if (this.get('defaultLabel') && this.get('type') === 'radio') { addCheck('', this.get('defaultLabel')); } for (var k in values) addCheck(k, values[k]); } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Select features by property using a condition * * @constructor * @extends {ol.control.SelectBase} * @fires select * @param {Object=} options * @param {string} options.className control class name * @param {Element | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {ol/source/Vector | Array
      } options.source the source to search in * @param {string} options.label control label, default 'condition' * @param {number} options.selectAll select all features if no option selected * @param {condition|Array} options.condition conditions * @param {function|undefined} options.onchoice function triggered when an option is clicked, default doSelect */ ol.control.SelectCondition = class olcontrolSelectCondition extends ol.control.SelectBase { constructor(options) { options = options || {}; // Container var div = options.content = ol.ext.element.create('DIV'); options.className = options.className || 'ol-select-condition'; super(options); var bt = div.querySelector('button'); this._check = ol.ext.element.createSwitch({ after: options.label || 'condition', change: function () { if (this._onchoice) this._onchoice(); else this.doSelect(); }.bind(this), parent: div }); // Input div this._input = ol.ext.element.create('DIV', { parent: div }); // Add ok button at the end div.appendChild(bt); this.setCondition(options.condition); this._selectAll = options.selectAll; this._onchoice = options.onchoice; } /** Set condition to select on * @param {condition | Array} condition * @param {string} attr property to select on * @param {string} op operator (=, !=, <; <=, >, >=, contain, !contain, regecp) * @param {*} val value to select on */ setCondition(condition) { if (!condition) this._conditions = []; else this._conditions = (condition instanceof Array ? condition : [condition]); } /** Add a condition to select on * @param {condition} condition * @param {string} attr property to select on * @param {string} op operator (=, !=, <; <=, >, >=, contain, !contain, regecp) * @param {*} val value to select on */ addCondition(condition) { this._conditions.push(condition); } /** Select features by condition */ doSelect(options) { options = options || {}; var conditions = this._conditions; if (!this._check.checked) { return super.doSelect({ features: options.features, matchAll: this._selectAll }); } else { return super.doSelect({ features: options.features, conditions: conditions }); } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Select features by property using a simple text input * * @constructor * @extends {ol.control.SelectBase} * @fires select * @param {Object=} options * @param {string} options.className control class name * @param {Element | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {ol/source/Vector | Array
        } options.source the source to search in * @param {string} options.property property to select on * @param {function|undefined} options.onchoice function triggered the text change, default nothing */ ol.control.SelectFulltext = class olcontrolSelectFulltext extends ol.control.SelectBase { constructor(options) { options = options || {}; // Container var div = options.content = ol.ext.element.create('DIV'); if (options.label) { ol.ext.element.create('LABEL', { html: options.label, parent: div }); } super(options); var bt = div.querySelector('button'); this._input = ol.ext.element.create('INPUT', { placeHolder: options.placeHolder || 'search...', change: function () { if (this._onchoice) this._onchoice(); }.bind(this), parent: div }); // Add ok button at the end div.appendChild(bt); this._onchoice = options.onchoice; this.set('property', options.property || 'name'); } /** Select features by condition */ doSelect(options) { options = options || {}; return super.doSelect({ features: options.features, useCase: false, conditions: [{ attr: this.get('property'), op: 'contain', val: this._input.value }] }); } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * A multiselect control. * A container that manage other control Select * * @constructor * @extends {ol.control.SelectBase} * @fires select * @param {Object=} options * @param {string} options.className control class name * @param {Element | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {ol/source/Vector | Array
          } options.source the source to search in * @param {Array} options.controls an array of controls */ ol.control.SelectMulti = class olcontrolSelectMulti extends ol.control.SelectBase { constructor(options) { options = options || {}; // Container options.content = ol.ext.element.create('DIV'); var container = ol.ext.element.create('UL', { parent: options.content }); options.className = options.className || 'ol-select-multi'; super(options); this._container = container; this._controls = []; options.controls.forEach(this.addControl.bind(this)); } /** * Set the map instance the control associated with. * @param {o.Map} map The map instance. */ setMap(map) { if (this.getMap()) { this._controls.forEach(function (c) { this.getMap().remveControl(c); }.bind(this)); } super.setMap(map); if (this.getMap()) { this._controls.forEach(function (c) { this.getMap().addControl(c); }.bind(this)); } } /** Add a new control * @param {ol.control.SelectBase} c */ addControl(c) { if (c instanceof ol.control.SelectBase) { this._controls.push(c); c.setTarget(ol.ext.element.create('LI', { parent: this._container })); c._selectAll = true; c._onchoice = this.doSelect.bind(this); if (this.getMap()) { this.getMap().addControl(c); } } } /** Get select controls * @return {Aray} */ getControls() { return this._controls; } /** Select features by condition */ doSelect() { var features = []; this.getSources().forEach(function (s) { features = features.concat(s.getFeatures()); }); this._controls.forEach(function (c) { features = c.doSelect({ features: features }); }); this.dispatchEvent({ type: "select", features: features }); return features; } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Select features by property using a popup * * @constructor * @extends {ol.control.SelectBase} * @fires select * @param {Object=} options * @param {string} options.className control class name * @param {Element | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {ol/source/Vector | Array
            } options.source the source to search in * @param {string} options.property property to select on * @param {number} options.max max feature to test to get the values, default 10000 * @param {number} options.selectAll select all features if no option selected * @param {string} options.defaultLabel label for the default selection * @param {function|undefined} options.onchoice function triggered when an option is clicked, default doSelect */ ol.control.SelectPopup = class olcontrolSelectPopup extends ol.control.SelectBase { constructor(options) { options = options || {}; options.className = options.className || 'ol-select-popup'; // Container var div = options.content = ol.ext.element.create('DIV'); super(options); var bt = div.querySelector('button'); if (options.label) { ol.ext.element.create('LABEL', { html: options.label, parent: div }); } this._input = ol.ext.element.create('SELECT', { on: { change: function () { if (this._onchoice) this._onchoice(); else this.doSelect(); }.bind(this) }, parent: div }); // Add ok button at the end div.appendChild(bt); this.set('property', options.property || 'name'); this.set('max', options.max || 10000); this.set('defaultLabel', options.defaultLabel); this._selectAll = options.selectAll; this._onchoice = options.onchoice; // Set select options this.setValues(); } /** * Set the map instance the control associated with. * @param {o.Map} map The map instance. */ setMap(map) { super.setMap(map); this.setValues(); } /** Select features by attributes */ doSelect(options) { options = options || {}; if (!this._input.value) { return super.doSelect({ features: options.features, matchAll: this._selectAll }); } else { return super.doSelect({ features: options.features, conditions: [{ attr: this.get('property'), op: '=', val: this._input.value }] }); } } /** Set the popup values * @param {Object} values a key/value list with key = property value, value = title shown in the popup, default search values in the sources */ setValues(options) { options = options || {}; var values, vals; if (options.values) { if (options.values instanceof Array) { vals = {}; options.values.forEach(function (v) { vals[v] = v; }); } else { vals = options.values; } } else { vals = {}; var prop = this.get('property'); this.getSources().forEach(function (s) { var features = s.getFeatures(); var max = Math.min(features.length, this.get('max')); for (var i = 0; i < max; i++) { var p = features[i].get(prop); if (p) vals[p] = p; } }.bind(this)); } if (options.sort) { values = {}; Object.keys(vals).sort().forEach(function (key) { values[key] = vals[key]; }); } else { values = vals; } ol.ext.element.setHTML(this._input, ''); ol.ext.element.create('OPTION', { className: 'ol-default', html: this.get('defaultLabel') || '', value: '', parent: this._input }); for (var k in values) { ol.ext.element.create('OPTION', { html: values[k], value: k, parent: this._input }); } } } /** A control to display status information on top of the map * @constructor * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {string} options.status status, default none * @param {string} options.position position of the status 'top', 'left', 'bottom' or 'right', default top * @param {boolean} options.visible default true */ ol.control.Status = class olcontrolStatus extends ol.control.Control { constructor(options) { options = options || {}; // New element var element = ol.ext.element.create('DIV', { className: (options.className || '') + ' ol-status' + (options.target ? '' : ' ol-unselectable ol-control') }); // Initialize super({ element: element, target: options.target }); this.setVisible(options.visible !== false); if (options.position) this.setPosition(options.position); this.status(options.status || ''); } /** Set visiblitity * @param {boolean} visible */ setVisible(visible) { if (visible) this.element.classList.add('ol-visible'); else this.element.classList.remove('ol-visible'); } /** Show status on the map * @param {string|Element} html status text or DOM element */ status(html) { var s = html || ''; if (s) { ol.ext.element.show(this.element); if (s instanceof Element || typeof (s) === 'string') { ol.ext.element.setHTML(this.element, s); } else { s = ''; for (var i in html) { s += ' ' + html[i] + '
            '; } } ol.ext.element.setHTML(this.element, s); } else { ol.ext.element.hide(this.element); } } /** Set status position * @param {string} position position of the status 'top', 'left', 'bottom' or 'right', default top */ setPosition(position) { this.element.classList.remove('ol-left'); this.element.classList.remove('ol-right'); this.element.classList.remove('ol-bottom'); this.element.classList.remove('ol-center'); if (/^left$|^right$|^bottom$|^center$/.test(position)) { this.element.classList.add('ol-' + position); } } /** Show the status * @param {boolean} show show or hide the control, default true */ show(show) { if (show === false) ol.ext.element.hide(this.element); else ol.ext.element.show(this.element); } /** Hide the status */ hide() { ol.ext.element.hide(this.element); } /** Toggle the status */ toggle() { ol.ext.element.toggle(this.element); } /** Is status visible */ isShown() { return this.element.style.display === 'none'; } } // Add flyTo /** A control with scroll-driven navigation to create narrative maps * * @constructor * @extends {ol.control.Control} * @fires scrollto * @fires clickimage * @param {Object=} options Control options. * @param {String} options.className class of the control (scrollLine, scrollBox or any) * @param {Element | string | undefined} [options.html] The storymap content * @param {Element | string | undefined} [options.target] The target element to place the story. If no html is provided the content of the target will be used. * @param {boolean} [options.minibar=false] add a mini scroll bar */ ol.control.Storymap = class olcontrolStorymap extends ol.control.Control { constructor(options) { // Remove or get target content if (options.target) { if (!options.html) { options.html = options.target.innerHTML } else if (options.html instanceof Element) { options.html = options.html.innerHTML } options.target.innerHTML = '' } // New element var element = ol.ext.element.create('DIV', { className: (options.className || '') + ' ol-storymap' + (options.target ? '' : ' ol-unselectable ol-control'), }) // Initialize super({ element: element, target: options.target }) this.content = ol.ext.element.create('DIV', { parent: element }) // Make a scroll div ol.ext.element.scrollDiv(this.content, { vertical: true, mousewheel: true, minibar: options.minibar }) this.setStory(options.html) } /** Scroll to a chapter * @param {string} name Name of the chapter to scroll to */ setChapter(name) { var chapter = this.content.querySelectorAll('.chapter') for (var i = 0, s; s = chapter[i]; i++) { if (s.getAttribute('name') === name) { this.content.scrollTop = s.offsetTop - 30 } } } /** Scroll to a chapter * @param {string} name Name of the chapter to scroll to */ setStory(html) { if (html instanceof Element) { this.content.innerHTML = '' this.content.appendChild(html) } else { this.content.innerHTML = html } this.content.querySelectorAll('.chapter').forEach(function (c) { c.addEventListener('click', function (e) { if (!c.classList.contains('ol-select')) { this.content.scrollTop = c.offsetTop - 30 e.preventDefault() } else { if (e.target.tagName === 'IMG' && e.target.dataset.title) { this.dispatchEvent({ coordinate: this.getMap() ? this.getMap().getCoordinateFromPixel([e.layerX, e.layerY]) : null, type: 'clickimage', img: e.target, title: e.target.dataset.title, element: c, name: c.getAttribute('name'), originalEvent: e }) } } }.bind(this)) }.bind(this)) // Scroll to the next chapter var sc = this.content.querySelectorAll('.ol-scroll-next') sc.forEach(function (s) { s.addEventListener('click', function (e) { if (s.parentElement.classList.contains('ol-select')) { var chapter = this.content.querySelectorAll('.chapter') var scrollto = s.offsetTop for (var i = 0, c; c = chapter[i]; i++) { if (c.offsetTop > scrollto) { scrollto = c.offsetTop break } } this.content.scrollTop = scrollto - 30 e.stopPropagation() e.preventDefault() } }.bind(this)) }.bind(this)) // Scroll top sc = this.content.querySelectorAll('.ol-scroll-top') sc.forEach(function (i) { i.addEventListener('click', function (e) { this.content.scrollTop = 0 e.stopPropagation() e.preventDefault() }.bind(this)) }.bind(this)) var getEvent = function (currentDiv) { var lonlat = [parseFloat(currentDiv.getAttribute('data-lon')), parseFloat(currentDiv.getAttribute('data-lat'))] var coord = ol.proj.fromLonLat(lonlat, this.getMap().getView().getProjection()) var zoom = parseFloat(currentDiv.getAttribute('data-zoom')) return { type: 'scrollto', element: currentDiv, name: currentDiv.getAttribute('name'), coordinate: coord, lon: lonlat, zoom: zoom } }.bind(this) // Handle scrolling var currentDiv = this.content.querySelectorAll('.chapter')[0] setTimeout(function () { currentDiv.classList.add('ol-select') this.dispatchEvent(getEvent(currentDiv)) }.bind(this)) // Trigger change event on scroll this.content.addEventListener('scroll', function () { var current, chapter = this.content.querySelectorAll('.chapter') var height = ol.ext.element.getStyle(this.content, 'height') if (!this.content.scrollTop) { current = chapter[0] } else { for (var i = 0, s; s = chapter[i]; i++) { var p = s.offsetTop - this.content.scrollTop if (p > height / 3) break current = s } } if (current && current !== currentDiv) { if (currentDiv) currentDiv.classList.remove('ol-select') currentDiv = current currentDiv.classList.add('ol-select') var e = getEvent(currentDiv) var view = this.getMap().getView() view.cancelAnimations() switch (currentDiv.getAttribute('data-animation')) { case 'flyto': { view.flyTo({ center: e.coordinate, zoomAt: Math.min(view.getZoom(), e.zoom) - 1, zoom: e.zoom }) break } default: break } this.dispatchEvent(e) } }.bind(this)) } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * @classdesc Swipe Control. * @fires moving * @constructor * @extends {ol.control.Control} * @param {Object=} Control options. * @param {ol.layer|Array} options.layers layers to swipe * @param {ol.layer|Array} options.rightLayers layers to swipe on right side * @param {string} options.className control class name * @param {number} options.position position property of the swipe [0,1], default 0.5 * @param {string} options.orientation orientation property (vertical|horizontal), default vertical */ ol.control.Swipe = class olcontrolSwipe extends ol.control.Control { constructor(options) { options = options || {}; var element = document.createElement('div'); super({ element: element }); element.className = (options.className || 'ol-swipe') + ' ol-unselectable ol-control'; var button = document.createElement('button'); element.appendChild(button); element.addEventListener('mousedown', this.move.bind(this)); element.addEventListener('touchstart', this.move.bind(this)); // An array of listener on layer postcompose this.precomposeRight_ = this.precomposeRight.bind(this); this.precomposeLeft_ = this.precomposeLeft.bind(this); this.postcompose_ = this.postcompose.bind(this); this.layers = []; if (options.layers) this.addLayer(options.layers, false); if (options.rightLayers) this.addLayer(options.rightLayers, true); this.on('propertychange', function (e) { if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } if (this.get('orientation') === "horizontal") { this.element.style.top = this.get('position') * 100 + "%"; this.element.style.left = ""; } else { if (this.get('orientation') !== "vertical") this.set('orientation', "vertical"); this.element.style.left = this.get('position') * 100 + "%"; this.element.style.top = ""; } if (e.key === 'orientation') { this.element.classList.remove("horizontal", "vertical"); this.element.classList.add(this.get('orientation')); } // Force VectorImage to refresh if (!this.isMoving) { this.layers.forEach(function (l) { if (l.layer.getImageRatio) l.layer.changed(); }); } }.bind(this)); this.set('position', options.position || 0.5); this.set('orientation', options.orientation || 'vertical'); } /** * Set the map instance the control associated with. * @param {_ol_Map_} map The map instance. */ setMap(map) { var i; var l; if (this.getMap()) { for (i = 0; i < this.layers.length; i++) { l = this.layers[i]; if (l.right) l.layer.un(['precompose', 'prerender'], this.precomposeRight_); else l.layer.un(['precompose', 'prerender'], this.precomposeLeft_); l.layer.un(['postcompose', 'postrender'], this.postcompose_); } try { this.getMap().renderSync(); } catch (e) { /* ok */ } } super.setMap(map); if (map) { this._listener = []; for (i = 0; i < this.layers.length; i++) { l = this.layers[i]; if (l.right) l.layer.on(['precompose', 'prerender'], this.precomposeRight_); else l.layer.on(['precompose', 'prerender'], this.precomposeLeft_); l.layer.on(['postcompose', 'postrender'], this.postcompose_); } try { map.renderSync(); } catch (e) { /* ok */ } } } /** @private */ isLayer_(layer) { for (var k = 0; k < this.layers.length; k++) { if (this.layers[k].layer === layer) return k; } return -1; } /** Add a layer to clip * @param {ol.layer|Array} layer to clip * @param {bool} add layer in the right part of the map, default left. */ addLayer(layers, right) { if (!(layers instanceof Array)) layers = [layers]; for (var i = 0; i < layers.length; i++) { var l = layers[i]; if (this.isLayer_(l) < 0) { this.layers.push({ layer: l, right: right }); if (this.getMap()) { if (right) l.on(['precompose', 'prerender'], this.precomposeRight_); else l.on(['precompose', 'prerender'], this.precomposeLeft_); l.on(['postcompose', 'postrender'], this.postcompose_); try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } } } /** Remove all layers */ removeLayers() { var layers = []; this.layers.forEach(function (l) { layers.push(l.layer); }); this.removeLayer(layers); } /** Remove a layer to clip * @param {ol.layer|Array} layer to clip */ removeLayer(layers) { if (!(layers instanceof Array)) layers = [layers]; for (var i = 0; i < layers.length; i++) { var k = this.isLayer_(layers[i]); if (k >= 0 && this.getMap()) { if (this.layers[k].right) layers[i].un(['precompose', 'prerender'], this.precomposeRight_); else layers[i].un(['precompose', 'prerender'], this.precomposeLeft_); layers[i].un(['postcompose', 'postrender'], this.postcompose_); this.layers.splice(k, 1); } } if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Get visible rectangle * @returns {ol.extent} */ getRectangle() { var s; if (this.get('orientation') === 'vertical') { s = this.getMap().getSize(); return [0, 0, s[0] * this.get('position'), s[1]]; } else { s = this.getMap().getSize(); return [0, 0, s[0], s[1] * this.get('position')]; } } /** @private */ move(e) { var self = this; var l; if (!this._movefn) this._movefn = this.move.bind(this); switch (e.type) { case 'touchcancel': case 'touchend': case 'mouseup': { self.isMoving = false; ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"] .forEach(function (eventName) { document.removeEventListener(eventName, self._movefn); }); // Force VectorImage to refresh this.layers.forEach(function (l) { if (l.layer.getImageRatio) l.layer.changed(); }); break; } case 'mousedown': case 'touchstart': { self.isMoving = true; ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"] .forEach(function (eventName) { document.addEventListener(eventName, self._movefn); }); } // fallthrough case 'mousemove': case 'touchmove': { if (self.isMoving) { if (self.get('orientation') === 'vertical') { var pageX = e.pageX || (e.touches && e.touches.length && e.touches[0].pageX) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageX); if (!pageX) break; pageX -= self.getMap().getTargetElement().getBoundingClientRect().left + window.pageXOffset - document.documentElement.clientLeft; l = self.getMap().getSize()[0]; var w = l - Math.min(Math.max(0, l - pageX), l); l = w / l; self.set('position', l); self.dispatchEvent({ type: 'moving', size: [w, self.getMap().getSize()[1]], position: [l, 0] }); } else { var pageY = e.pageY || (e.touches && e.touches.length && e.touches[0].pageY) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY); if (!pageY) break; pageY -= self.getMap().getTargetElement().getBoundingClientRect().top + window.pageYOffset - document.documentElement.clientTop; l = self.getMap().getSize()[1]; var h = l - Math.min(Math.max(0, l - pageY), l); l = h / l; self.set('position', l); self.dispatchEvent({ type: 'moving', size: [self.getMap().getSize()[0], h], position: [0, l] }); } } break; } default: break; } } /** @private */ _transformPt(e, pt) { var tr = e.inversePixelTransform; var x = pt[0]; var y = pt[1]; pt[0] = tr[0] * x + tr[2] * y + tr[4]; pt[1] = tr[1] * x + tr[3] * y + tr[5]; return pt; } /** @private */ _drawRect(e, pts) { var tr = e.inversePixelTransform; if (tr) { var r = [ [pts[0][0], pts[0][1]], [pts[0][0], pts[1][1]], [pts[1][0], pts[1][1]], [pts[1][0], pts[0][1]], [pts[0][0], pts[0][1]] ]; e.context.save(); // Rotate VectorImages if (e.target.getImageRatio) { var rot = -Math.atan2(e.frameState.pixelToCoordinateTransform[1], e.frameState.pixelToCoordinateTransform[0]); e.context.translate(e.frameState.size[0] / 2, e.frameState.size[1] / 2); e.context.rotate(rot); e.context.translate(-e.frameState.size[0] / 2, -e.frameState.size[1] / 2); } r.forEach(function (pt, i) { pt = [ (pt[0] * tr[0] - pt[1] * tr[1] + tr[4]), (-pt[0] * tr[2] + pt[1] * tr[3] + tr[5]) ]; if (!i) { e.context.moveTo(pt[0], pt[1]); } else { e.context.lineTo(pt[0], pt[1]); } }); e.context.restore(); } else { var ratio = e.frameState.pixelRatio; e.context.rect(pts[0][0] * ratio, pts[0][1] * ratio, pts[1][0] * ratio, pts[1][1] * ratio); } } /** @private */ precomposeLeft(e) { var ctx = e.context; if (ctx instanceof WebGLRenderingContext) { if (e.type === 'prerender') { // Clear ctx.clearColor(0, 0, 0, 0); ctx.clear(ctx.COLOR_BUFFER_BIT); // Clip ctx.enable(ctx.SCISSOR_TEST); var mapSize = this.getMap().getSize(); // [width, height] in CSS pixels // get render coordinates and dimensions given CSS coordinates var bottomLeft = this._transformPt(e, [0, mapSize[1]]); var topRight = this._transformPt(e, [mapSize[0], 0]); var fullWidth = topRight[0] - bottomLeft[0]; var fullHeight = topRight[1] - bottomLeft[1]; var width, height; if (this.get('orientation') === "vertical") { width = Math.round(fullWidth * this.get('position')); height = fullHeight; } else { width = fullWidth; height = Math.round((fullHeight * this.get('position'))); bottomLeft[1] += fullHeight - height; } ctx.scissor(bottomLeft[0], bottomLeft[1], width, height); } } else { var size = e.frameState.size; ctx.save(); ctx.beginPath(); var pts = [[0, 0], [size[0], size[1]]]; if (this.get('orientation') === "vertical") { pts[1] = [ size[0] * .5 + this.getMap().getSize()[0] * (this.get('position') - .5), size[1] ]; } else { pts[1] = [ size[0], size[1] * .5 + this.getMap().getSize()[1] * (this.get('position') - .5) ]; } this._drawRect(e, pts); ctx.clip(); } } /** @private */ precomposeRight(e) { var ctx = e.context; if (ctx instanceof WebGLRenderingContext) { if (e.type === 'prerender') { // Clear ctx.clearColor(0, 0, 0, 0); ctx.clear(ctx.COLOR_BUFFER_BIT); // Clip ctx.enable(ctx.SCISSOR_TEST); var mapSize = this.getMap().getSize(); // [width, height] in CSS pixels // get render coordinates and dimensions given CSS coordinates var bottomLeft = this._transformPt(e, [0, mapSize[1]]); var topRight = this._transformPt(e, [mapSize[0], 0]); var fullWidth = topRight[0] - bottomLeft[0]; var fullHeight = topRight[1] - bottomLeft[1]; var width, height; if (this.get('orientation') === "vertical") { height = fullHeight; width = Math.round(fullWidth * (1 - this.get('position'))); bottomLeft[0] += fullWidth - width; } else { width = fullWidth; height = Math.round(fullHeight * (1 - this.get('position'))); } ctx.scissor(bottomLeft[0], bottomLeft[1], width, height); } } else { var size = e.frameState.size; ctx.save(); ctx.beginPath(); var pts = [[0, 0], [size[0], size[1]]]; if (this.get('orientation') === "vertical") { pts[0] = [ size[0] * .5 + this.getMap().getSize()[0] * (this.get('position') - .5), 0 ]; } else { pts[0] = [ 0, size[1] * .5 + this.getMap().getSize()[1] * (this.get('position') - .5) ]; } this._drawRect(e, pts); ctx.clip(); } } /** @private */ postcompose(e) { if (e.context instanceof WebGLRenderingContext) { if (e.type === 'postrender') { var gl = e.context; gl.disable(gl.SCISSOR_TEST); } } else { // restore context when decluttering is done (ol>=6) // https://github.com/openlayers/openlayers/issues/10096 if (e.target.getClassName && e.target.getClassName() !== 'ol-layer' && e.target.get('declutter')) { setTimeout(function () { e.context.restore(); }, 0); } else { e.context.restore(); } } } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A control that use a CSS clip rect to swipe the map * @classdesc Swipe Control. * @fires moving * @constructor * @extends {ol.control.Control} * @param {Object=} Control options. * @param {string} options.className control class name * @param {number} options.position position property of the swipe [0,1], default 0.5 * @param {string} options.orientation orientation property (vertical|horizontal), default vertical * @param {boolean} options.right true to position map on right side (resp. bottom for horizontal orientation), default false */ ol.control.SwipeMap = class olcontrolSwipeMap extends ol.control.Control { constructor(options) { options = options || {}; var button = document.createElement('button'); var element = document.createElement('div'); element.className = (options.className || "ol-swipe") + " ol-unselectable ol-control"; super({ element: element }); element.appendChild(button); element.addEventListener("mousedown", this.move.bind(this)); element.addEventListener("touchstart", this.move.bind(this)); this.on('propertychange', function (e) { if (this.get('orientation') === "horizontal") { this.element.style.top = this.get('position') * 100 + "%"; this.element.style.left = ""; } else { if (this.get('orientation') !== "vertical") this.set('orientation', "vertical"); this.element.style.left = this.get('position') * 100 + "%"; this.element.style.top = ""; } if (e.key === 'orientation') { this.element.classList.remove("horizontal", "vertical"); this.element.classList.add(this.get('orientation')); } this._clip(); }.bind(this)); this.on('change:active', this._clip.bind(this)); this.set('position', options.position || 0.5); this.set('orientation', options.orientation || 'vertical'); this.set('right', options.right); } /** Set the map instance the control associated with. * @param {ol.Map} map The map instance. */ setMap(map) { if (this.getMap()) { if (this._listener) ol.Observable.unByKey(this._listener); var layerDiv = this.getMap().getViewport().querySelector('.ol-layers'); layerDiv.style.clip = ''; } super.setMap(map); if (map) { this._listener = map.on('change:size', this._clip.bind(this)); } } /** Clip * @private */ _clip() { if (this.getMap()) { var layerDiv = this.getMap().getViewport().querySelector('.ol-layers'); var rect = this.getRectangle(); layerDiv.style.clip = 'rect(' + rect[1] + 'px,' // top + rect[2] + 'px,' // right + rect[3] + 'px,' // bottom + rect[0] + 'px' //left + ')'; } } /** Get visible rectangle * @returns {ol.extent} */ getRectangle() { var s = this.getMap().getSize(); if (this.get('orientation') === 'vertical') { if (this.get('right')) { return [s[0] * this.get('position'), 0, s[0], s[1]]; } else { return [0, 0, s[0] * this.get('position'), s[1]]; } } else { if (this.get('right')) { return [0, s[1] * this.get('position'), s[0], s[1]]; } else { return [0, 0, s[0], s[1] * this.get('position')]; } } } /** @private */ move(e) { var self = this; var l; if (!this._movefn) this._movefn = this.move.bind(this); switch (e.type) { case 'touchcancel': case 'touchend': case 'mouseup': { self.isMoving = false; ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"] .forEach(function (eventName) { document.removeEventListener(eventName, self._movefn); }); break; } case 'mousedown': case 'touchstart': { self.isMoving = true; ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"] .forEach(function (eventName) { document.addEventListener(eventName, self._movefn); }); } // fallthrough case 'mousemove': case 'touchmove': { if (self.isMoving) { if (self.get('orientation') === 'vertical') { var pageX = e.pageX || (e.touches && e.touches.length && e.touches[0].pageX) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageX); if (!pageX) break; pageX -= self.getMap().getTargetElement().getBoundingClientRect().left + window.pageXOffset - document.documentElement.clientLeft; l = self.getMap().getSize()[0]; var w = l - Math.min(Math.max(0, l - pageX), l); l = w / l; self.set('position', l); self.dispatchEvent({ type: 'moving', size: [w, self.getMap().getSize()[1]], position: [l, 0] }); } else { var pageY = e.pageY || (e.touches && e.touches.length && e.touches[0].pageY) || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY); if (!pageY) break; pageY -= self.getMap().getTargetElement().getBoundingClientRect().top + window.pageYOffset - document.documentElement.clientTop; l = self.getMap().getSize()[1]; var h = l - Math.min(Math.max(0, l - pageY), l); l = h / l; self.set('position', l); self.dispatchEvent({ type: 'moving', size: [self.getMap().getSize()[0], h], position: [0, l] }); } } break; } default: break; } } } /* 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). */ /** ol.control.Target draw a target at the center of the map. * @constructor * @extends {ol.control.CanvasBase} * @param {Object} options * @param {ol.style.Style|Array} options.style * @param {string} options.composite composite operation = difference|multiply|xor|screen|overlay|darken|lighter|lighten|... */ ol.control.Target = class olcontrolTarget extends ol.control.CanvasBase { constructor(options) { options = options || {}; var style = options.style || [ new ol.style.Style({ image: new ol.style.RegularShape({ points: 4, radius: 11, radius1: 0, radius2: 0, snapToPixel: true, stroke: new ol.style.Stroke({ color: "#fff", width: 3 }) }) }), new ol.style.Style({ image: new ol.style.RegularShape({ points: 4, radius: 11, radius1: 0, radius2: 0, snapToPixel: true, stroke: new ol.style.Stroke({ color: "#000", width: 1 }) }) }) ]; if (!(style instanceof Array)) style = [style]; var div = document.createElement('div'); div.className = "ol-target ol-unselectable ol-control"; super({ element: div, style: style, target: options.target }); this.composite = options.composite || ''; this.setVisible(options.visible !== false); } /** Set Style * @api */ setStyle(style) { if (!(style instanceof Array)) style = [style]; super.setStyle(style) } /** Set the control visibility * @paraam {boolean} b */ setVisible(b) { this.set('visible', b); if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Get the control visibility * @return {boolean} b */ getVisible() { return this.get('visible'); } /** Draw the target * @private */ _draw(e) { var ctx = this.getContext(e); if (!ctx || !this.getMap() || !this.getVisible()) return; var ratio = e.frameState.pixelRatio; ctx.save(); ctx.scale(ratio, ratio); var cx = ctx.canvas.width / 2 / ratio; var cy = ctx.canvas.height / 2 / ratio; var geom = new ol.geom.Point(this.getMap().getCoordinateFromPixel([cx, cy])); if (this.composite) ctx.globalCompositeOperation = this.composite; for (var i = 0; i < this._style.length; i++) { var style = this._style[i]; if (style instanceof ol.style.Style) { var vectorContext = e.vectorContext; if (!vectorContext) { var event = { inversePixelTransform: [1, 0, 0, 1, 0, 0], context: ctx, frameState: { pixelRatio: ratio, extent: e.frameState.extent, coordinateToPixelTransform: e.frameState.coordinateToPixelTransform, viewState: e.frameState.viewState } }; vectorContext = ol.render.getVectorContext(event); } vectorContext.setStyle(style); vectorContext.drawGeometry(geom); } } ctx.restore(); } } /* 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). */ /** A simple push button control drawn as text * @constructor * @extends {ol.control.Button} * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {String} options.title title of the control * @param {String} options.html html to insert in the control * @param {function} options.handleClick callback when control is clicked (or use change:active event) */ ol.control.TextButton = class olcontrolTextButton extends ol.control.Button { constructor(options) { options = options || {}; options.className = (options.className || '') + ' ol-text-button'; super(options); } } /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ /** Timeline control * * @constructor * @extends {ol.control.Control} * @fires select * @fires scroll * @fires collapse * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {Array} options.features Features to show in the timeline * @param {ol.SourceImageOptions.vector} options.source class of the control * @param {Number} options.interval time interval length in ms or a text with a format d, h, mn, s (31 days = '31d'), default none * @param {String} options.maxWidth width of the time line in px, default 2000px * @param {String} options.minDate minimum date * @param {String} options.maxDate maximum date * @param {Number} options.minZoom Minimum zoom for the line, default .2 * @param {Number} options.maxZoom Maximum zoom for the line, default 4 * @param {boolean} options.zoomButton Are zoom buttons avaliable, default false * @param {function} options.getHTML a function that takes a feature and returns the html to display * @param {function} options.getFeatureDate a function that takes a feature and returns its date, default the date propertie * @param {function} options.endFeatureDate a function that takes a feature and returns its end date, default no end date * @param {String} options.graduation day|month to show month or day graduation, default show only years * @param {String} options.scrollTimeout Time in milliseconds to get a scroll event, default 15ms */ ol.control.Timeline = class olcontrolTimeline extends ol.control.Control { constructor(options) { var element = ol.ext.element.create('DIV', { className: (options.className || '') + ' ol-timeline' + (options.target ? '' : ' ol-unselectable ol-control') + (options.zoomButton ? ' ol-hasbutton' : '') }); // Initialize super({ element: element, target: options.target }); // Scroll div this._scrollDiv = ol.ext.element.create('DIV', { className: 'ol-scroll', parent: this.element }); // Add a button bar this._buttons = ol.ext.element.create('DIV', { className: 'ol-buttons', parent: this.element }); // Zoom buttons if (options.zoomButton) { // Zoom in this.addButton({ className: 'ol-zoom-in', handleClick: function () { var zoom = this.get('zoom'); if (zoom >= 1) { zoom++; } else { zoom = Math.min(1, zoom + 0.1); } zoom = Math.round(zoom * 100) / 100; this.refresh(zoom); }.bind(this) }); // Zoom out this.addButton({ className: 'ol-zoom-out', handleClick: function () { var zoom = this.get('zoom'); if (zoom > 1) { zoom--; } else { zoom -= 0.1; } zoom = Math.round(zoom * 100) / 100; this.refresh(zoom); }.bind(this) }); } // Draw center date this._intervalDiv = ol.ext.element.create('DIV', { className: 'ol-center-date', parent: this.element }); // Remove selection this.element.addEventListener('mouseover', function () { if (this._select) this._select.elt.classList.remove('ol-select'); }.bind(this)); // Trigger scroll event var scrollListener = null; this._scrollDiv.addEventListener('scroll', function () { this._setScrollLeft(); if (scrollListener) { clearTimeout(scrollListener); scrollListener = null; } scrollListener = setTimeout(function () { this.dispatchEvent({ type: 'scroll', date: this.getDate(), dateStart: this.getDate('start'), dateEnd: this.getDate('end') }); }.bind(this), options.scrollTimeout || 15); }.bind(this)); // Magic to give "live" scroll events on touch devices // this._scrollDiv.addEventListener('gesturechange', function() {}); // Scroll timeline ol.ext.element.scrollDiv(this._scrollDiv, { onmove: function (b) { // Prevent selection on moving this._moving = b; }.bind(this) }); this._tline = []; // Parameters this._scrollLeft = 0; this.set('maxWidth', options.maxWidth || 2000); this.set('minDate', options.minDate || Infinity); this.set('maxDate', options.maxDate || -Infinity); this.set('graduation', options.graduation); this.set('minZoom', options.minZoom || .2); this.set('maxZoom', options.maxZoom || 4); this.setInterval(options.interval); if (options.getHTML) this._getHTML = options.getHTML; if (options.getFeatureDate) this._getFeatureDate = options.getFeatureDate; if (options.endFeatureDate) this._endFeatureDate = options.endFeatureDate; // Feature source this.setFeatures(options.features || options.source, options.zoom); } /** * Set the map instance the control is associated with * and add interaction attached to it to this map. * @param {_ol_Map_} map The map instance. */ setMap(map) { super.setMap(map); this.refresh(this.get('zoom') || 1, true); } /** Add a button on the timeline * @param {*} button * @param {string} button.className * @param {title} button.className * @param {Element|string} button.html Content of the element * @param {function} button.click a function called when the button is clicked */ addButton(button) { this.element.classList.add('ol-hasbutton'); ol.ext.element.create('BUTTON', { className: button.className || undefined, title: button.title, html: button.html, click: button.handleClick, parent: this._buttons }); } /** Set an interval * @param {number|string} length the interval length in ms or a farmatted text ie. end with y, 1d, h, mn, s (31 days = '31d'), default none */ setInterval(length) { if (typeof (length) === 'string') { if (/s$/.test(length)) { length = parseFloat(length) * 1000; } else if (/mn$/.test(length)) { length = parseFloat(length) * 1000 * 60; } else if (/h$/.test(length)) { length = parseFloat(length) * 1000 * 3600; } else if (/d$/.test(length)) { length = parseFloat(length) * 1000 * 3600 * 24; } else if (/y$/.test(length)) { length = parseFloat(length) * 1000 * 3600 * 24 * 365; } else { length = 0; } } this.set('interval', length || 0); if (length) this.element.classList.add('ol-interval'); else this.element.classList.remove('ol-interval'); this.refresh(this.get('zoom')); } /** Default html to show in the line * @param {ol.Feature} feature * @return {DOMElement|string} * @private */ _getHTML(feature) { return feature.get('name') || ''; } /** Default function to get the date of a feature, returns the date attribute * @param {ol.Feature} feature * @return {Data|string} * @private */ _getFeatureDate(feature) { return (feature && feature.get) ? feature.get('date') : null; } /** Default function to get the end date of a feature, return undefined * @param {ol.Feature} feature * @return {Data|string} * @private */ _endFeatureDate( /* feature */) { return undefined; } /** Is the line collapsed * @return {boolean} */ isCollapsed() { return this.element.classList.contains('ol-collapsed'); } /** Collapse the line * @param {boolean} b */ collapse(b) { if (b) this.element.classList.add('ol-collapsed'); else this.element.classList.remove('ol-collapsed'); this.dispatchEvent({ type: 'collapse', collapsed: this.isCollapsed() }); } /** Collapse the line */ toggle() { this.element.classList.toggle('ol-collapsed'); this.dispatchEvent({ type: 'collapse', collapsed: this.isCollapsed() }); } /** Set the features to display in the timeline * @param {Array|ol.source.Vector} features An array of features or a vector source * @param {number} zoom zoom to draw the line default 1 */ setFeatures(features, zoom) { this._features = this._source = null; if (features instanceof ol.source.Vector) this._source = features; else if (features instanceof Array) this._features = features; else this._features = []; this.refresh(zoom); } /** * Get features * @return {Array} */ getFeatures() { return this._features || this._source.getFeatures(); } /** * Refresh the timeline with new data * @param {Number} zoom Zoom factor from 0.25 to 10, default 1 */ refresh(zoom, first) { if (!this.getMap()) return; if (!zoom) zoom = this.get('zoom'); zoom = Math.min(this.get('maxZoom'), Math.max(this.get('minZoom'), zoom || 1)); this.set('zoom', zoom); this._scrollDiv.innerHTML = ''; var features = this.getFeatures(); var d, d2; // Get features sorted by date var tline = this._tline = []; features.forEach(function (f) { if (d = this._getFeatureDate(f)) { if (!(d instanceof Date)) { d = new Date(d); } if (this._endFeatureDate) { d2 = this._endFeatureDate(f); if (!(d2 instanceof Date)) { d2 = new Date(d2); } } if (!isNaN(d)) { tline.push({ date: d, end: isNaN(d2) ? null : d2, feature: f }); } } }.bind(this)); tline.sort(function (a, b) { return (a.date < b.date ? -1 : (a.date === b.date ? 0 : 1)); }); // Draw var div = ol.ext.element.create('DIV', { parent: this._scrollDiv }); // Calculate width var min = this._minDate = Math.min(this.get('minDate'), tline.length ? tline[0].date : Infinity); var max = this._maxDate = Math.max(this.get('maxDate'), tline.length ? tline[tline.length - 1].date : -Infinity); if (!isFinite(min)) this._minDate = min = new Date(); if (!isFinite(max)) this._maxDate = max = new Date(); var delta = (max - min); var maxWidth = this.get('maxWidth'); var scale = this._scale = (delta > maxWidth ? maxWidth / delta : 1) * zoom; // Leave 10px on right min = this._minDate = this._minDate - 10 / scale; delta = (max - min) * scale; ol.ext.element.setStyle(div, { width: delta, maxWidth: 'unset' }); // Draw time's bar this._drawTime(div, min, max, scale); // Set interval if (this.get('interval')) { ol.ext.element.setStyle(this._intervalDiv, { width: this.get('interval') * scale }); } else { ol.ext.element.setStyle(this._intervalDiv, { width: '' }); } // Draw features var line = []; var lineHeight = ol.ext.element.getStyle(this._scrollDiv, 'lineHeight'); // Wrapper var fdiv = ol.ext.element.create('DIV', { className: 'ol-features', parent: div }); // Add features on the line tline.forEach(function (f) { var d = f.date; var t = f.elt = ol.ext.element.create('DIV', { className: 'ol-feature', style: { left: Math.round((d - min) * scale), }, html: this._getHTML(f.feature), parent: fdiv }); // Prevent image dragging var img = t.querySelectorAll('img'); for (var i = 0; i < img.length; i++) { img[i].ondragstart = function () { return false; }; } // Calculate image width if (f.end) { ol.ext.element.setStyle(t, { minWidth: (f.end - d) * scale, width: (f.end - d) * scale, maxWidth: 'unset' }); } var left = ol.ext.element.getStyle(t, 'left'); // Select on click t.addEventListener('click', function () { if (!this._moving) { this.dispatchEvent({ type: 'select', feature: f.feature }); } }.bind(this)); // Find first free Y position var pos, l; for (pos = 0; l = line[pos]; pos++) { if (left > l) { break; } } line[pos] = left + ol.ext.element.getStyle(t, 'width'); ol.ext.element.setStyle(t, { top: pos * lineHeight }); }.bind(this)); this._nbline = line.length; if (first) this.setDate(this._minDate, { anim: false, position: 'start' }); // Dispatch scroll event this.dispatchEvent({ type: 'scroll', date: this.getDate(), dateStart: this.getDate('start'), dateEnd: this.getDate('end') }); } /** Get offset given a date * @param {Date} date * @return {number} * @private */ _getOffsetFromDate(date) { return (date - this._minDate) * this._scale; } /** Get date given an offset * @param {Date} date * @return {number} * @private */ _getDateFromOffset(offset) { return offset / this._scale + this._minDate; } /** Set the current position * @param {number} scrollLeft current position (undefined when scrolling) * @returns {number} * @private */ _setScrollLeft(scrollLeft) { this._scrollLeft = scrollLeft; if (scrollLeft !== undefined) { this._scrollDiv.scrollLeft = scrollLeft; } } /** Get the current position * @returns {number} * @private */ _getScrollLeft() { // Unset when scrolling if (this._scrollLeft === undefined) { return this._scrollDiv.scrollLeft; } else { // St by user return this._scrollLeft; } } /** * Draw dates on line * @private */ _drawTime(div, min, max, scale) { // Times div var tdiv = ol.ext.element.create('DIV', { className: 'ol-times', parent: div }); var d, dt, month, dmonth; var dx = ol.ext.element.getStyle(tdiv, 'left'); var heigth = ol.ext.element.getStyle(tdiv, 'height'); // Year var year = (new Date(this._minDate)).getFullYear(); dt = ((new Date(0)).setFullYear(String(year)) - new Date(0).setFullYear(String(year - 1))) * scale; var dyear = Math.round(2 * heigth / dt) + 1; while (true) { d = new Date(0).setFullYear(year); if (d > this._maxDate) break; ol.ext.element.create('DIV', { className: 'ol-time ol-year', style: { left: this._getOffsetFromDate(d) - dx }, html: year, parent: tdiv }); year += dyear; } // Month if (/day|month/.test(this.get('graduation'))) { dt = ((new Date(0, 0, 1)).setFullYear(String(year)) - new Date(0, 0, 1).setFullYear(String(year - 1))) * scale; dmonth = Math.max(1, Math.round(12 / Math.round(dt / heigth / 2))); if (dmonth < 12) { year = (new Date(this._minDate)).getFullYear(); month = dmonth + 1; while (true) { d = new Date(0, 0, 1); d.setFullYear(year); d.setMonth(month - 1); if (d > this._maxDate) break; ol.ext.element.create('DIV', { className: 'ol-time ol-month', style: { left: this._getOffsetFromDate(d) - dx }, html: d.toLocaleDateString(undefined, { month: 'short' }), parent: tdiv }); month += dmonth; if (month > 12) { year++; month = dmonth + 1; } } } } // Day if (this.get('graduation') === 'day') { dt = (new Date(0, 1, 1) - new Date(0, 0, 1)) * scale; var dday = Math.max(1, Math.round(31 / Math.round(dt / heigth / 2))); if (dday < 31) { year = (new Date(this._minDate)).getFullYear(); month = 0; var day = dday; while (true) { d = new Date(0, 0, 1); d.setFullYear(year); d.setMonth(month); d.setDate(day); if (isNaN(d)) { month++; if (month > 12) { month = 1; year++; } day = dday; } else { if (d > this._maxDate) break; if (day > 1) { var offdate = this._getOffsetFromDate(d); if (this._getOffsetFromDate(new Date(year, month + 1, 1)) - offdate > heigth) { ol.ext.element.create('DIV', { className: 'ol-time ol-day', style: { left: offdate - dx }, html: day, parent: tdiv }); } } year = d.getFullYear(); month = d.getMonth(); day = d.getDate() + dday; if (day > new Date(year, month + 1, 0).getDate()) { month++; day = dday; } } } } } } /** Center timeline on a date * @param {Date|String|ol.feature} feature a date or a feature with a date * @param {Object} options * @param {boolean} options.anim animate scroll * @param {string} options.position start, end or middle, default middle */ setDate(feature, options) { var date; options = options || {}; // It's a date if (feature instanceof Date) { date = feature; } else { // Get date from Feature if (this.getFeatures().indexOf(feature) >= 0) { date = this._getFeatureDate(feature); } if (date && !(date instanceof Date)) { date = new Date(date); } if (!date || isNaN(date)) { date = new Date(String(feature)); } } if (!isNaN(date)) { if (options.anim === false) this._scrollDiv.classList.add('ol-move'); var scrollLeft = this._getOffsetFromDate(date); if (options.position === 'start') { scrollLeft += ol.ext.element.outerWidth(this._scrollDiv) / 2 - ol.ext.element.getStyle(this._scrollDiv, 'marginLeft') / 2; } else if (options.position === 'end') { scrollLeft -= ol.ext.element.outerWidth(this._scrollDiv) / 2 - ol.ext.element.getStyle(this._scrollDiv, 'marginLeft') / 2; } this._setScrollLeft(scrollLeft); if (options.anim === false) this._scrollDiv.classList.remove('ol-move'); if (feature) { for (var i = 0, f; f = this._tline[i]; i++) { if (f.feature === feature) { f.elt.classList.add('ol-select'); this._select = f; } else { f.elt.classList.remove('ol-select'); } } } } } /** Get round date (sticked to mn, hour day or month) * @param {Date} d * @param {string} stick sticking option to stick date to: 'mn', 'hour', 'day', 'month', default no stick * @return {Date} */ roundDate(d, stick) { switch (stick) { case 'mn': { return new Date(this._roundTo(d, 60 * 1000)); } case 'hour': { return new Date(this._roundTo(d, 60 * 60 * 1000)); } case 'day': { return new Date(this._roundTo(d, 24 * 60 * 60 * 1000)); } case 'month': { d = new Date(this._roundTo(d, 24 * 60 * 60 * 1000)); if (d.getDate() > 15) { d = new Date(d.setMonth(d.getMonth() + 1)); } d = d.setDate(1); return new Date(d); } default: return new Date(d); } } /** Get the date of the center * @param {string} position position to get 'start', 'end' or 'middle', default middle * @param {string} stick sticking option to stick date to: 'mn', 'hour', 'day', 'month', default no stick * @return {Date} */ getDate(position, stick) { var pos; if (!stick) stick = position; switch (position) { case 'start': { if (this.get('interval')) { pos = -ol.ext.element.getStyle(this._intervalDiv, 'width') / 2 + ol.ext.element.getStyle(this._scrollDiv, 'marginLeft') / 2; } else { pos = -ol.ext.element.outerWidth(this._scrollDiv) / 2 + ol.ext.element.getStyle(this._scrollDiv, 'marginLeft') / 2; } break; } case 'end': { if (this.get('interval')) { pos = ol.ext.element.getStyle(this._intervalDiv, 'width') / 2 - ol.ext.element.getStyle(this._scrollDiv, 'marginLeft') / 2; } else { pos = ol.ext.element.outerWidth(this._scrollDiv) / 2 - ol.ext.element.getStyle(this._scrollDiv, 'marginLeft') / 2; } break; } default: { pos = 0; break; } } var d = this._getDateFromOffset(this._getScrollLeft() + pos); d = this.roundDate(d, stick); return new Date(d); } /** Round number to * @param {number} d * @param {number} r * @return {number} * @private */ _roundTo(d, r) { return Math.round(d / r) * r; } /** Get the start date of the control * @return {Date} */ getStartDate() { return new Date(this.get('minDate')); } /** Get the end date of the control * @return {Date} */ getEndDate() { return new Date(this.get('maxDate')); } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Record map canvas as video * @constructor * @fire start * @fire error * @fire stop * @fire pause * @fire resume * @extends {ol.control.Control} * @param {Object=} options Control options. * @param {String} options.className class of the control * @param {number} [options.framerate=30] framerate for the video * @param {number} [options.videoBitsPerSecond=5000000] bitrate for the video * @param {DOMElement|string} [options.videoTarget] video element or the container to add the video when finished or 'DIALOG' to show it in a dialog, default none */ ol.control.VideoRecorder = class olcontrolVideoRecorder extends ol.control.Control { constructor(options) { options = options || {} var element = ol.ext.element.create('DIV', { className: (options.className || 'ol-videorec') + ' ol-unselectable ol-control' }) super({ element: element, target: options.target }) // buttons ol.ext.element.create('BUTTON', { type: 'button', className: 'ol-start', title: 'start', click: function () { this.start() }.bind(this), parent: element }) ol.ext.element.create('BUTTON', { type: 'button', className: 'ol-stop', title: 'stop', click: function () { this.stop() }.bind(this), parent: element }) ol.ext.element.create('BUTTON', { type: 'button', className: 'ol-pause', title: 'pause', click: function () { this.pause() }.bind(this), parent: element }) ol.ext.element.create('BUTTON', { type: 'button', className: 'ol-resume', title: 'resume', click: function () { this.resume() }.bind(this), parent: element }) // Start this.set('framerate', 30) this.set('videoBitsPerSecond', 5000000) if (options.videoTarget === 'DIALOG') { this._dialog = new ol.control.Dialog({ className: 'ol-fullscreen-dialog', target: document.body, closeBox: true }) this._videoTarget = this._dialog.getContentElement() } else { this._videoTarget = options.videoTarget } // Print control this._printCtrl = new ol.control.Print({ target: ol.ext.element.create('DIV') }) } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) { this.getMap().removeControl(this._printCtrl) if (this._dialog) this.getMap().removeControl(this._dialog) } super.setMap(map) if (this.getMap()) { this.getMap().addControl(this._printCtrl) if (this._dialog) this.getMap().addControl(this._dialog) } } /** Start recording */ start() { var print = this._printCtrl var stop = false function capture(canvas) { // Stop recording if (stop) return // New frame print.fastPrint({ canvas: canvas }, capture) } print.fastPrint({}, function (canvas) { var videoStream try { videoStream = canvas.captureStream(this.get('framerate') || 30) } catch (e) { this.dispatchEvent({ type: 'error', error: e }) // console.warn(e); return } this._mediaRecorder = new MediaRecorder(videoStream, { videoBitsPerSecond: this.get('videoBitsPerSecond') || 5000000 }) var chunks = [] this._mediaRecorder.ondataavailable = function (e) { chunks.push(e.data) } this._mediaRecorder.onstop = function () { stop = true var blob = new Blob(chunks, { 'type': 'video/mp4' }) // other types are available such as 'video/webm' for instance, see the doc for more info chunks = [] if (this._videoTarget instanceof Element) { var video if (this._videoTarget.tagName === 'VIDEO') { video = this._videoTarget } else { video = this._videoTarget.querySelector('video') if (!video) { video = ol.ext.element.create('VIDEO', { controls: '', parent: this._videoTarget }) } } if (this._dialog) this._dialog.show() video.src = URL.createObjectURL(blob) this.dispatchEvent({ type: 'stop', videoURL: video.src }) } else { this.dispatchEvent({ type: 'stop', videoURL: URL.createObjectURL(blob) }) } }.bind(this) this._mediaRecorder.onpause = function () { stop = true this.dispatchEvent({ type: 'pause' }) }.bind(this) this._mediaRecorder.onresume = function () { stop = false capture(canvas) this.dispatchEvent({ type: 'resume' }) }.bind(this) this._mediaRecorder.onerror = function (e) { this.dispatchEvent({ type: 'error', error: e }) }.bind(this) stop = false capture(canvas) this._mediaRecorder.start() this.dispatchEvent({ type: 'start', canvas: canvas }) this.element.setAttribute('data-state', 'rec') }.bind(this)) } /** Stop recording */ stop() { if (this._mediaRecorder) { this._mediaRecorder.stop() this._mediaRecorder = null this.element.setAttribute('data-state', 'inactive') } } /** Pause recording */ pause() { if (this._mediaRecorder) { this._mediaRecorder.pause() this.element.setAttribute('data-state', 'pause') } } /** Resume recording after pause */ resume() { if (this._mediaRecorder) { this._mediaRecorder.resume() this.element.setAttribute('data-state', 'rec') } } } /* Using WMS Layer with EPSG:4326 projection. The tiles will be reprojected to map pojection (EPSG:3857). NB: reduce tileSize to minimize deformations on small scales. */ /** WMSCapabilities * @constructor * @fires load * @fires capabilities * @extends {ol.control.Button} * @param {*} options * @param {string|Element} [options.target] the target to set the dialog, use document.body to have fullwindow dialog * @param {string} [options.proxy] proxy to use when requesting Getcapabilites, default none (suppose the service use CORS) * @param {string} [options.placeholder='service url...'] input placeholder, default 'service url...' * @param {string} [options.title=WMS] dialog title, default 'WMS' * @param {string} [options.searchLabel='search'] Label for search button, default 'search' * @param {string} [options.loadLabel='load'] Label for load button, default 'load' * @param {Array} [options.srs] an array of supported srs, default map projection code or 'EPSG:3857' * @param {number} [options.timeout=1000] Timeout for getCapabilities request, default 1000 * @param {boolean} [options.cors=false] Use CORS, default false * @param {string} [options.optional] a list of optional url properties (when set in the request url), separated with ',' * @param {boolean} [options.trace=false] Log layer info, default false * @param {*} [options.services] a key/url object of services for quick access in a menu */ ol.control.WMSCapabilities = class olcontrolWMSCapabilities extends ol.control.Button { constructor(options) { options = options || {} var buttonOptions = Object.assign({}, options || {}) if (buttonOptions.target === document.body) delete buttonOptions.target if (buttonOptions.target) { buttonOptions.className = ((buttonOptions.className || '') + ' ol-wmscapabilities ol-hidden').trim() delete buttonOptions.target } else { buttonOptions.className = ((buttonOptions.className || '') + ' ol-wmscapabilities').trim() buttonOptions.handleClick = function () { self.showDialog() } } super(buttonOptions); var self = this; this._proxy = options.proxy // WMS options this.set('srs', options.srs || []) this.set('cors', options.cors) this.set('trace', options.trace) this.set('title', options.title) this.set('loadLabel', options.loadLabel) this.set('optional', options.optional) // Dialog this.createDialog(options) // Default version this._elements.formVersion.value = '1.0.0' // Ajax request var parser = this._getParser() this._ajax = new ol.ext.Ajax({ dataType: 'text', auth: options.authentication }) this._ajax.on('success', function (evt) { var caps try { caps = parser.read(evt.response) } catch (e) { this.showError({ type: 'load', error: e }) } if (caps) { if (!caps.Capability.Layer.Layer) { this.showError({ type: 'noLayer' }) } else { this.showCapabilities(caps) } } this.dispatchEvent({ type: 'capabilities', capabilities: caps }) if (typeof (evt.options.callback) === 'function') evt.options.callback(caps) }.bind(this)) this._ajax.on('error', function (evt) { this.showError({ type: 'load', error: evt }) this.dispatchEvent({ type: 'capabilities' }) if (typeof (evt.options.callback) === 'function') false }.bind(this)) // Handle waiting this._ajax.on('loadstart', function () { this._elements.element.classList.add('ol-searching') }.bind(this)) this._ajax.on('loadend', function () { this._elements.element.classList.remove('ol-searching') }.bind(this)) // Load a layer if (options.onselect) { this.on('load', function (e) { options.onselect(e.layer, e.options) }) } } /** Get service parser */ _getParser() { return new ol.format.WMSCapabilities() } /** Create dialog * @private */ createDialog(options) { var target = options.target if (!target || target === document.body) { this._dialog = new ol.control.Dialog({ className: 'ol-wmscapabilities', closeBox: true, closeOnSubmit: false, target: options.target }) this._dialog.on('button', function (e) { if (e.button === 'submit') { this.getCapabilities(e.inputs.url.value) } }.bind(this)) target = null } var element = ol.ext.element.create('DIV', { className: ('ol-wmscapabilities ' + (options.className || '')).trim(), parent: target }) this._elements = { element: target || element } var inputdiv = ol.ext.element.create('DIV', { className: 'ol-url', parent: element }) var input = this._elements.input = ol.ext.element.create('INPUT', { className: 'url', type: 'text', tabIndex: 1, placeholder: options.placeholder || 'service url...', autocorrect: 'off', autocapitalize: 'off', parent: inputdiv }) input.addEventListener('keyup', function (e) { if (e.keyCode === 13) { this.getCapabilities(input.value, options) } }.bind(this)) if (options.services) { var qaccess = ol.ext.element.create('SELECT', { className: 'url', on: { change: function (e) { var url = e.target.options[e.target.selectedIndex].value this.getCapabilities(url, options) e.target.selectedIndex = 0 }.bind(this) }, parent: inputdiv }) ol.ext.element.create('OPTION', { html: ' ', parent: qaccess }) for (var k in options.services) { ol.ext.element.create('OPTION', { html: k, value: options.services[k], parent: qaccess }) } } ol.ext.element.create('BUTTON', { click: function () { this.getCapabilities(input.value, options) }.bind(this), html: options.searchLabel || 'search', parent: inputdiv }) // Errors this._elements.error = ol.ext.element.create('DIV', { className: 'ol-error', parent: inputdiv }) // Result div var rdiv = this._elements.result = ol.ext.element.create('DIV', { className: 'ol-result', parent: element }) // Preview var preview = ol.ext.element.create('DIV', { className: 'ol-preview', html: options.previewLabel || 'preview', parent: rdiv }) this._elements.preview = ol.ext.element.create('IMG', { parent: preview }) // Check tainted canvas this._img = new Image this._img.crossOrigin = 'Anonymous' this._img.addEventListener('error', function () { preview.className = 'ol-preview tainted' this._elements.formCrossOrigin.checked = false }.bind(this)) this._img.addEventListener('load', function () { preview.className = 'ol-preview ok' this._elements.formCrossOrigin.checked = true }.bind(this)) // Select list this._elements.select = ol.ext.element.create('DIV', { className: 'ol-select-list', tabIndex: 2, parent: rdiv }) // Info data this._elements.data = ol.ext.element.create('DIV', { className: 'ol-data', parent: rdiv }) this._elements.buttons = ol.ext.element.create('DIV', { className: 'ol-buttons', parent: rdiv }) this._elements.legend = ol.ext.element.create('IMG', { className: 'ol-legend', parent: rdiv }) // WMS form var form = this._elements.form = ol.ext.element.create('UL', { className: 'ol-wmsform', parent: element }) var addLine = function (label, val, pholder) { var li = ol.ext.element.create('LI', { parent: form }) ol.ext.element.create('LABEL', { html: this.labels[label], parent: li }) if (typeof (val) === 'boolean') { this._elements[label] = ol.ext.element.create('INPUT', { type: 'checkbox', checked: val, parent: li }) } else if (val instanceof Array) { var sel = this._elements[label] = ol.ext.element.create('SELECT', { parent: li }) val.forEach(function (v) { ol.ext.element.create('OPTION', { html: v, value: v, parent: sel }) }.bind(this)) } else { this._elements[label] = ol.ext.element.create('INPUT', { value: (val === undefined ? '' : val), placeholder: pholder || '', type: typeof (val) === 'number' ? 'number' : 'text', parent: li }) } return li }.bind(this) addLine('formTitle') addLine('formLayer', '', 'layer1,layer2,...') var li = addLine('formMap') li.setAttribute('data-param', 'map') li = addLine('formStyle') li.setAttribute('data-param', 'style') addLine('formFormat', ['image/png', 'image/jpeg']) addLine('formMinZoom', 0) addLine('formMaxZoom', 20) li = addLine('formExtent', '', 'xmin,ymin,xmax,ymax') li.setAttribute('data-param', 'extent') var extent = li.querySelector('input') ol.ext.element.create('BUTTON', { title: this.labels.mapExtent, click: function () { extent.value = this.getMap().getView().calculateExtent(this.getMap().getSize()).join(',') }.bind(this), parent: li }) li = addLine('formProjection', '') li.setAttribute('data-param', 'proj') addLine('formCrossOrigin', false) li = addLine('formVersion', '1.3.0') li.setAttribute('data-param', 'version') addLine('formAttribution', '') ol.ext.element.create('BUTTON', { html: this.get('loadLabel') || 'Load', click: function () { var opt = this._getFormOptions() var layer = this.getLayerFromOptions(opt) this.dispatchEvent({ type: 'load', layer: layer, options: opt }) this._dialog.hide() }.bind(this), parent: form }) return element } /** Create a new layer using options received by getOptionsFromCap method * @param {*} options */ getLayerFromOptions(options) { options.layer.source = new ol.source.TileWMS(options.source) var layer = new ol.layer.Tile(options.layer) delete options.layer.source return layer } /** * 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) if (this._dialog) this._dialog.setMap(map) } /** Get the dialog * @returns {ol.control.Dialog} */ getDialog() { return this._dialog } /** Show dialog for url * @param {string} [url] service url, default ask for an url * @param {*} options capabilities options * @param {string} options.map WMS map or get map in url?map=xxx * @param {string} options.version WMS version (yet only 1.3.0 is implemented), default 1.3.0 * @param {number} options.timeout timout to get the capabilities, default 10000 */ showDialog(url, options) { this.showError() if (!this._elements.formProjection.value) { this._elements.formProjection.value = this.getMap().getView().getProjection().getCode() } if (this._dialog) { this._dialog.show({ title: this.get('title') === undefined ? 'WMS' : this.get('title'), content: this._elements.element }) } this.getCapabilities(url, options) // Center on selection var sel = this._elements.select.querySelector('.selected') if (sel) { this._elements.select.scrollTop = sel.offsetTop - 20 } } /** Test url and return true if it is a valid url string * @param {string} url * @return {bolean} * @api */ testUrl(url) { // var pattern = /(https?:\/\/)([\da-z.-]+)\.([a-z]{2,6})([/\w.-]*)*\/?/ var pattern = new RegExp( // protocol '^(https?:\\/\\/)' + // domain name '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // OR ip (v4) address '((\\d{1,3}\\.){3}\\d{1,3}))' + // port and path '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // query string '(\\?[;&a-z\\d%_.~+=\\/-]*)?' + // fragment locator '(\\#[-a-z\\d_]*)?$', 'i') return !!pattern.test(url) } /** Get Capabilities request parameters * @param {*} options */ getRequestParam(options) { return { SERVICE: 'WMS', REQUEST: 'GetCapabilities', VERSION: options.version || '1.3.0' } } /** Get WMS capabilities for a server * @fire load * @param {string} url service url * @param {*} options * @param {string} options.map WMS map or get map in url?map=xxx * @param {string} [options.version=1.3.0] WMS version (yet only 1.3.0 is implemented), default 1.3.0 * @param {number} [options.timeout=10000] timout to get the capabilities, default 10000 * @param {function} [options.onload] callback function */ getCapabilities(url, options) { if (!url) return if (!this.testUrl(url)) { this.showError({ type: 'badUrl' }) return } options = options || {} // Extract map attributes url = url.split('?') var search = url[1] url = url[0] // reset this._elements.formMap.value = '' this._elements.formLayer.value = '' this._elements.formStyle.value = '' this._elements.formTitle.value = '' this._elements.formProjection.value = this.getMap().getView().getProjection().getCode() this._elements.formFormat.selectedIndex = 0 var map = options.map || '' var optional = {} if (search) { search = search.replace(/^\?/, '').split('&') search.forEach(function (s) { s = s.split('=') s[1] = decodeURIComponent(s[1] || '') if (/^map$/i.test(s[0])) { map = s[1] this._elements.formMap.value = map } if (/^layers$/i.test(s[0])) { this._elements.formLayer.value = s[1] this._elements.formTitle.value = s[1].split(',')[0] } if (/^style$/i.test(s[0])) { this._elements.formStyle.value = s[1] } if (/^crs$/i.test(s[0])) { this._elements.formProjection.value = s[1] } if (/^format$/i.test(s[0])) { for (var o, i = 0; o = this._elements.formFormat.options[i]; i++) { if (o.value === s[1]) { this._elements.formFormat.selectedIndex = i break } } } // Check optionals if (this.get('optional')) { this.get('optional').split(',').forEach(function (o) { if (o === s[0]) { optional[o] = s[1] } }.bind(this)) } }.bind(this)) } // Get request params var request = this.getRequestParam(options) var opt = [] if (map) { request.MAP = map opt.push('map=' + map) } for (var o in optional) { request[o] = optional[o] opt.push(o + '=' + optional[o]) } // Fill form this._elements.input.value = (url || '') + (opt ? '?' + opt.join('&') : '') this.clearForm() // Sen drequest if (this._proxy) { var q = '' for (var r in request) q += (q ? '&' : '') + r + '=' + request[r] this._ajax.send(this._proxy, { url: q }, { timeout: options.timeout || 10000, callback: options.onload, abort: false }) } else { this._ajax.send(url, request, { timeout: options.timeout || 10000, callback: options.onload, abort: false }) } } /** Display error * @param {*} error event */ showError(e) { if (!e) this._elements.error.innerHTML = '' else this._elements.error.innerHTML = this.error[e.type] || ('ERROR (' + e.type + ')') if (e && e.type === 'load') { this._elements.form.classList.add('visible') } else { this._elements.form.classList.remove('visible') } } /** Clear form */ clearForm() { this._elements.result.classList.remove('ol-visible') this.showError() this._elements.select.innerHTML = '' this._elements.data.innerHTML = '' this._elements.preview.src = '' this._elements.legend.src = '' this._elements.legend.classList.remove('visible') } /** Display capabilities in the dialog * @param {*} caps JSON capabilities */ showCapabilities(caps) { this._elements.result.classList.add('ol-visible') // console.log(caps) var list = [] var addLayers = function (parent, level) { level = level || 0 parent.Layer.forEach(function (l) { if (!l.Attribution) l.Attribution = parent.Attribution if (!l.EX_GeographicBoundingBox) l.EX_GeographicBoundingBox = parent.EX_GeographicBoundingBox var li = ol.ext.element.create('DIV', { className: (l.Layer ? 'ol-title ' : '') + 'level-' + level, html: l.Name || l.Title, click: function () { // Reset this._elements.buttons.innerHTML = '' this._elements.data.innerHTML = '' this._elements.legend.src = this._elements.preview.src = '' this._elements.element.classList.remove('ol-form') this.showError() // Load layer var options = this.getOptionsFromCap(l, caps) var layer = this.getLayerFromOptions(options) this._currentOptions = options // list.forEach(function (i) { i.classList.remove('selected') }) li.classList.add('selected') // Fill form if (layer) { ol.ext.element.create('BUTTON', { html: this.get('loadLabel') || 'Load', className: 'ol-load', click: function () { this.dispatchEvent({ type: 'load', layer: layer, options: options }) if (this._dialog) this._dialog.hide() }.bind(this), parent: this._elements.buttons }) ol.ext.element.create('BUTTON', { className: 'ol-wmsform', click: function () { this._elements.element.classList.toggle('ol-form') }.bind(this), parent: this._elements.buttons }) // Show preview var reso = this.getMap().getView().getResolution() var center = this.getMap().getView().getCenter() this._elements.preview.src = layer.getPreview(center, reso, this.getMap().getView().getProjection()) this._img.src = this._elements.preview.src // ShowInfo ol.ext.element.create('p', { className: 'ol-title', html: options.data.title, parent: this._elements.data }) ol.ext.element.create('p', { html: options.data.abstract, parent: this._elements.data }) if (options.data.legend && options.data.legend.length) { this._elements.legend.src = options.data.legend[0] this._elements.legend.classList.add('visible') } else { this._elements.legend.src = '' this._elements.legend.classList.remove('visible') } } }.bind(this), parent: this._elements.select }) list.push(li) if (l.Layer) { addLayers(l, level + 1) } }.bind(this)) }.bind(this) // Show layers this._elements.select.innerHTML = '' addLayers(caps.Capability.Layer) } /** Get resolution for a layer * @param {string} 'min' or 'max' * @param {*} layer * @param {number} val * @return {number} * @private */ getLayerResolution(m, layer, val) { var att = m === 'min' ? 'MinScaleDenominator' : 'MaxScaleDenominator' if (layer[att] !== undefined) return layer[att] / (72 / 2.54 * 100) if (!layer.Layer) return (m === 'min' ? 0 : 156543.03392804097) // Get min / max of contained layers val = (m === 'min' ? 156543.03392804097 : 0) for (var i = 0; i < layer.Layer.length; i++) { var res = this.getLayerResolution(m, layer.Layer[i], val) if (res !== undefined) val = Math[m](val, res) } return val } /** Return a WMS ol.layer.Tile for the given capabilities * @param {*} caps layer capabilities (read from the capabilities) * @param {*} parent capabilities * @return {*} options */ getOptionsFromCap(caps, parent) { var formats = parent.Capability.Request.GetMap.Format var format, i // Look for prefered format first var pref = [/png/, /jpeg/, /gif/] for (i = 0; i < 3; i++) { for (var f = 0; f < formats.length; f++) { if (pref[i].test(formats[f])) { format = formats[f] break } } if (format) break } if (!format) format = formats[0] // Check srs var srs = this.getMap().getView().getProjection().getCode() this.showError() var crs = false if (!caps.CRS) { crs = false } else if (caps.CRS.indexOf(srs) >= 0) { crs = true } else if (caps.CRS.indexOf('EPSG:4326') >= 0) { // try to set EPSG:4326 instead srs = 'EPSG:4326' crs = true } else { this.get('srs').forEach(function (s) { if (caps.CRS.indexOf(s) >= 0) { srs = s crs = true } }) } if (!crs) { this.showError({ type: 'srs' }) if (this.get('trace')) console.log('BAD srs: ', caps.CRS) } var bbox = caps.EX_GeographicBoundingBox //bbox = ol.proj.transformExtent(bbox, 'EPSG:4326', srs); if (bbox) bbox = ol.proj.transformExtent(bbox, 'EPSG:4326', this.getMap().getView().getProjection()) var attributions = [] if (caps.Attribution) { attributions.push('© ' + caps.Attribution.Title.replace(/') } var layer_opt = { title: caps.Title, extent: bbox, queryable: caps.queryable, abstract: caps.Abstract, minResolution: this.getLayerResolution('min', caps), maxResolution: this.getLayerResolution('max', caps) || 156543.03392804097 } var source_opt = { url: parent.Capability.Request.GetMap.DCPType[0].HTTP.Get.OnlineResource, projection: srs, attributions: attributions, crossOrigin: this.get('cors') ? 'anonymous' : null, params: { 'LAYERS': caps.Name, 'FORMAT': format, 'VERSION': parent.version || '1.3.0' } } // Resolution to zoom var view = new ol.View({ projection: this.getMap().getView().getProjection() }) view.setResolution(layer_opt.minResolution) var maxZoom = Math.round(view.getZoom()) view.setResolution(layer_opt.maxResolution) var minZoom = Math.round(view.getZoom()) // Fill form this._fillForm({ title: layer_opt.title, layers: source_opt.params.LAYERS, format: source_opt.params.FORMAT, minZoom: minZoom, maxZoom: maxZoom, extent: bbox ? bbox.join(',') : '', projection: source_opt.projection, attribution: source_opt.attributions[0] || '', version: source_opt.params.VERSION }) // Trace if (this.get('trace')) { var tso = JSON.stringify([source_opt], null, "\t").replace(/\\"/g, '"') layer_opt.source = "SOURCE" var t = "new ol.layer.Tile (" + JSON.stringify(layer_opt, null, "\t") + ")" t = t.replace(/\\"/g, '"') .replace('"SOURCE"', "new ol.source.TileWMS(" + tso + ")") .replace(/\\t/g, "\t").replace(/\\n/g, "\n") .replace("([\n\t", "(") .replace("}\n])", "})") console.log(t) delete layer_opt.source } // Legend ? var legend = [] if (caps.Style) { caps.Style.forEach(function (s) { if (s.LegendURL) { legend.push(s.LegendURL[0].OnlineResource) } }) } return ({ layer: layer_opt, source: source_opt, data: { title: caps.Title, abstract: caps.Abstract, logo: caps.Attribution && caps.Attribution.LogoURL ? caps.Attribution.LogoURL.OnlineResource : undefined, keyword: caps.KeywordList, legend: legend, opaque: caps.opaque, queryable: caps.queryable } }) } /** Get WMS options from control form * @return {*} options * @private */ _getFormOptions() { var minZoom = parseInt(this._elements.formMinZoom.value) var maxZoom = parseInt(this._elements.formMaxZoom.value) var view = new ol.View({ projection: this.getMap().getView().getProjection() }) view.setZoom(minZoom) var maxResolution = view.getResolution() view.setZoom(maxZoom) var minResolution = view.getResolution() var ext = [] if (this._elements.formExtent.value) { this._elements.formExtent.value.split(',').forEach(function (b) { ext.push(parseFloat(b)) }) } if (ext.length !== 4) ext = undefined var attributions = [] if (this._elements.formAttribution.value) attributions.push(this._elements.formAttribution.value) var options = { layer: { title: this._elements.formTitle.value, extent: ext, maxResolution: maxResolution, minResolution: minResolution }, source: { url: this._elements.input.value, crossOrigin: this._elements.formCrossOrigin.checked ? 'anonymous' : null, projection: this._elements.formProjection.value, attributions: attributions, params: { FORMAT: this._elements.formFormat.options[this._elements.formFormat.selectedIndex].value, LAYERS: this._elements.formLayer.value, VERSION: this._elements.formVersion.value } }, data: { title: this._elements.formTitle.value } } if (this._elements.formMap.value) options.source.params.MAP = this._elements.formMap.value return options } /** Fill dialog form * @private */ _fillForm(opt) { this._elements.formTitle.value = opt.title this._elements.formLayer.value = opt.layers this._elements.formStyle.value = opt.style var o, i for (i = 0; o = this._elements.formFormat.options[i]; i++) { if (o.value === opt.format) { this._elements.formFormat.selectedIndex = i break } } this._elements.formExtent.value = opt.extent || '' this._elements.formMaxZoom.value = opt.maxZoom this._elements.formMinZoom.value = opt.minZoom this._elements.formProjection.value = opt.projection this._elements.formAttribution.value = opt.attribution this._elements.formVersion.value = opt.version } /** Load a layer using service * @param {string} url service url * @param {string} layername * @param {function} [onload] callback function (or listen to 'load' event) */ loadLayer(url, layerName, onload) { this.getCapabilities(url, { onload: function (cap) { if (cap) { cap.Capability.Layer.Layer.forEach(function (l) { if (l.Name === layerName || l.Identifier === layerName) { var options = this.getOptionsFromCap(l, cap) var layer = this.getLayerFromOptions(options) this.dispatchEvent({ type: 'load', layer: layer, options: options }) if (typeof (onload) === 'function') onload({ layer: layer, options: options }) } }.bind(this)) } else { this.dispatchEvent({ type: 'load', error: true }) } }.bind(this) }) } } /** Error list: a key/value list of error to display in the dialog * Overwrite it to handle internationalization */ ol.control.WMSCapabilities.prototype.error = { load: 'Can\'t retrieve service capabilities, try to add it manually...', badUrl: 'The input value is not a valid url...', TileMatrix: 'No TileMatrixSet supported...', noLayer: 'No layer available for this service...', srs: 'The service projection looks different from that of your map, it may not display correctly...' }; /** Form labels: a key/value list of form labels to display in the dialog * Overwrite it to handle internationalization */ ol.control.WMSCapabilities.prototype.labels = { formTitle: 'Title:', formLayer: 'Layers:', formMap: 'Map:', formStyle: 'Style:', formFormat: 'Format:', formMinZoom: 'Min zoom level:', formMaxZoom: 'Max zoom level:', formExtent: 'Extent:', mapExtent: 'use map extent...', formProjection: 'Projection:', formCrossOrigin: 'CrossOrigin:', formVersion: 'Version:', formAttribution: 'Attribution:', }; /** WMTSCapabilities * @constructor * @fires load * @fires capabilities * @extends {ol.control.WMSCapabilities} * @param {*} options * @param {string|Element} [options.target] the target to set the dialog, use document.body to have fullwindow dialog * @param {string} [options.proxy] proxy to use when requesting Getcapabilites, default none (suppose the service use CORS) * @param {string} [options.placeholder='service url...'] input placeholder, default 'service url...' * @param {string} [options.title=WMTS] dialog title, default 'WMTS' * @param {string} [options.searchLabel='search'] Label for search button, default 'search' * @param {string} [options.loadLabel='load'] Label for load button, default 'load' * @param {Array} [options.srs] an array of supported srs, default map projection code or 'EPSG:3857' * @param {number} [options.timeout=1000] Timeout for getCapabilities request, default 1000 * @param {boolean} [options.cors=false] Use CORS, default false * @param {string} [options.optional] a list of optional url properties (when set in the request url), separated with ',' * @param {boolean} [options.trace=false] Log layer info, default false * @param {*} [options.services] a key/url object of services for quick access in a menu */ ol.control.WMTSCapabilities = class olcontrolWMTSCapabilities extends ol.control.WMSCapabilities { constructor(options) { options = options || {}; options.title = options.title || 'WMTS'; super(options); this.getDialog().element.classList.add('ol-wmtscapabilities'); } /** Get service parser * @private */ _getParser() { var pars = new ol.format.WMTSCapabilities(); return { read: function (data) { var resp = pars.read(data); resp.Capability = { Layer: resp.Contents, }; // Generic attribution for layers resp.Capability.Layer.Attribution = { Title: resp.ServiceProvider.ProviderName }; // Remove non image format var layers = []; resp.Contents.Layer.forEach(function (l) { if (l.Format && /jpeg|png/.test(l.Format[0])) { layers.push(l); } }); resp.Contents.Layer = layers; return resp; }.bind(this) }; } /** Get Capabilities request parameters * @param {*} options */ getRequestParam(options) { return { SERVICE: 'WMTS', REQUEST: 'GetCapabilities', VERSION: options.version || '1.0.0' }; } /** Get tile grid options only for EPSG:3857 projection * @returns {*} * @private */ _getTG(tileMatrixSet, minZoom, maxZoom, tilePrefix) { var matrixIds = new Array(); var resolutions = new Array(); var size = ol.extent.getWidth(ol.proj.get('EPSG:3857').getExtent()) / 256; for (var z = 0; z <= (maxZoom ? maxZoom : 20); z++) { var id = tilePrefix ? tileMatrixSet + ':' + z : z; matrixIds[z] = id; resolutions[z] = size / Math.pow(2, z); } return { origin: [-20037508, 20037508], resolutions: resolutions, matrixIds: matrixIds, minZoom: (minZoom ? minZoom : 0) }; } /** Get WMTS tile grid (only EPSG:3857) * @param {sting} tileMatrixSet * @param {number} minZoom * @param {number} maxZoom * @param {boolean} tilePrefix * @returns {ol.tilegrid.WMTS} * @private */ getTileGrid(tileMatrixSet, minZoom, maxZoom, tilePrefix) { return new ol.tilegrid.WMTS(this._getTG(tileMatrixSet, minZoom, maxZoom, tilePrefix)); } /** Check if the TileMatrixSet is supported * @param {Object} tm * @returns {boolean} */ isSupportedSet(tm) { return tm.TileMatrixSet === 'PM' || tm.TileMatrixSet === '3857' || tm.TileMatrixSet === 'EPSG:3857' || tm.TileMatrixSet === 'webmercator' || tm.TileMatrixSet === 'GoogleMapsCompatible' } /** Return a WMTS options for the given capabilities * @param {*} caps layer capabilities (read from the capabilities) * @param {*} parent capabilities * @return {*} options */ getOptionsFromCap(caps, parent) { var bbox = caps.WGS84BoundingBox; if (bbox) bbox = ol.proj.transformExtent(bbox, 'EPSG:4326', this.getMap().getView().getProjection()); // Tilematrix zoom var minZoom = Infinity, maxZoom = -Infinity; var tmatrix; caps.TileMatrixSetLink.forEach(function (tm) { if (this.isSupportedSet(tm)) { tmatrix = tm; caps.TileMatrixSet = tm.TileMatrixSet; } }.bind(this)); if (!tmatrix) { this.showError({ type: 'TileMatrix' }); return; } if (tmatrix.TileMatrixSetLimits) { var tilePrefix = tmatrix.TileMatrixSetLimits[0].TileMatrix.split(':').length > 1; tmatrix.TileMatrixSetLimits.forEach(function (tm) { var zoom = tm.TileMatrix.split(':').pop(); minZoom = Math.min(minZoom, parseInt(zoom)); maxZoom = Math.max(maxZoom, parseInt(zoom)); }); } else { minZoom = 0; maxZoom = 20; } var view = new ol.View(); view.setZoom(minZoom); var layer_opt = { title: caps.Title, extent: bbox, abstract: caps.Abstract, maxResolution: view.getResolution() }; var source_opt = { url: parent.OperationsMetadata.GetTile.DCP.HTTP.Get[0].href, layer: caps.Identifier, matrixSet: caps.TileMatrixSet, format: caps.Format[0] || 'image/jpeg', projection: 'EPSG:3857', //tileGrid: tg, tilePrefix: tilePrefix, minZoom: minZoom, maxZoom: maxZoom, style: caps.Style ? caps.Style[0].Identifier : 'normal', attributions: caps.Attribution.Title, crossOrigin: this.get('cors') ? 'anonymous' : null, wrapX: (this.get('wrapX') !== false), }; // Fill form this._fillForm({ title: layer_opt.title, layers: source_opt.layer, style: source_opt.style, format: source_opt.format, minZoom: minZoom, maxZoom: maxZoom, extent: bbox ? bbox.join(',') : '', projection: source_opt.projection, attribution: source_opt.attributions || '', version: '1.0.0' }); // Trace if (this.get('trace')) { // Source source_opt.tileGrid = 'TILEGRID'; var tso = JSON.stringify([source_opt], null, "\t").replace(/\\"/g, '"'); tso = tso.replace('"TILEGRID"', 'new ol.tilegrid.WMTS(' + JSON.stringify(this._getTG(source_opt.matrixSet, source_opt.minZoom, source_opt.maxZoom, source_opt.tilePrefix), null, '\t').replace(/\n/g, '\n\t\t') + ')' ); delete source_opt.tileGrid; // Layer layer_opt.source = "SOURCE"; var t = "new ol.layer.Tile (" + JSON.stringify(layer_opt, null, "\t") + ")"; t = t.replace(/\\"/g, '"') .replace('"SOURCE"', "new ol.source.WMTS(" + tso + ")") .replace(/\\t/g, "\t").replace(/\\n/g, "\n") .replace(/"tileGrid": {/g, '"tileGrid": new ol.tilegrid.WMTS({') .replace(/},\n(\t*)"style"/g, '}),\n$1"style"') .replace("([\n\t", "(") .replace("}\n])", "})"); console.log(t); delete layer_opt.source; } var returnedLegend = undefined; if (caps.Style && caps.Style[0] && caps.Style[0].LegendURL && caps.Style[0].LegendURL[0]) { returnedLegend = [ caps.Style[0].LegendURL[0].href ]; } return ({ layer: layer_opt, source: source_opt, data: { title: caps.Title, abstract: caps.Abstract, legend: returnedLegend, } }); } /** Get WMS options from control form * @return {*} original original options * @return {*} options * @private */ _getFormOptions() { var options = this._currentOptions || {}; if (!options.layer) options.layer = {}; if (!options.source) options.source = {}; if (!options.data) options.data = {}; var minZoom = parseInt(this._elements.formMinZoom.value) || 0; var maxZoom = parseInt(this._elements.formMaxZoom.value) || 20; var ext = []; if (this._elements.formExtent.value) { this._elements.formExtent.value.split(',').forEach(function (b) { ext.push(parseFloat(b)); }); } if (ext.length !== 4) ext = undefined; var attributions = []; if (this._elements.formAttribution.value) attributions.push(this._elements.formAttribution.value); var view = new ol.View({ projection: this.getMap().getView().getProjection() }); view.setZoom(minZoom); var layer_opt = { title: this._elements.formTitle.value, extent: ext, abstract: options.layer.abstract || '', maxResolution: view.getResolution() }; var source_opt = { url: this._elements.input.value, layer: this._elements.formLayer.value, matrixSet: options.source.matrixSet || 'PM', format: this._elements.formFormat.options[this._elements.formFormat.selectedIndex].value, projection: 'EPSG:3857', minZoom: minZoom, maxZoom: maxZoom, // tileGrid: this._getTG(options.source.matrixSet || 'PM', minZoom, maxZoom), style: this._elements.formStyle.value || 'normal', attributions: attributions, crossOrigin: this._elements.formCrossOrigin.checked ? 'anonymous' : null, wrapX: (this.get('wrapX') !== false), }; return ({ layer: layer_opt, source: source_opt, data: { title: this._elements.formTitle.value, abstract: options.data.abstract, legend: options.data.legend, } }); } /** Create a new layer using options received by getOptionsFromCap method * @param {*} options */ getLayerFromOptions(options) { if (!options) return; options.source.tileGrid = this.getTileGrid(options.source.matrixSet, options.source.minZoom, options.source.maxZoom, options.source.tilePrefix); options.layer.source = new ol.source.WMTS(options.source); var layer = new ol.layer.Tile(options.layer); // Restore options delete options.layer.source; delete options.source.tileGrid; return layer; } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Feature animation base class * Use the {@link ol.Map#animateFeature} or {@link ol.layer.Vector#animateFeature} to animate a feature * on postcompose in a map or a layer * @constructor * @fires animationstart * @fires animating * @fires animationend * @param {ol.featureAnimationOptions} options * @param {Number} options.duration duration of the animation in ms, default 1000 * @param {bool} options.revers revers the animation direction * @param {Number} options.repeat number of time to repeat the animation, default 0 * @param {ol.style.Style} options.hiddenStyle a style to display the feature when playing the animation * to be used to make the feature selectable when playing animation * (@see {@link ../examples/map.featureanimation.select.html}), default the feature * will be hidden when playing (and not selectable) * @param {ol.easing.Function} options.fade an easing function used to fade in the feature, default none * @param {ol.easing.Function} options.easing an easing function for the animation, default ol.easing.linear */ ol.featureAnimation = class olfeatureAnimation extends ol.Object { constructor(options) { options = options || {} super(); this.duration_ = typeof (options.duration) == 'number' ? (options.duration >= 0 ? options.duration : 0) : 1000 this.fade_ = typeof (options.fade) == 'function' ? options.fade : null this.repeat_ = Number(options.repeat) var easing = typeof (options.easing) == 'function' ? options.easing : ol.easing.linear if (options.revers) this.easing_ = function (t) { return (1 - easing(t)) } else this.easing_ = easing this.hiddenStyle = options.hiddenStyle } /** Draw a geometry * @param {olx.animateFeatureEvent} e * @param {ol.geom} geom geometry for shadow * @param {ol.geom} shadow geometry for shadow (ie. style with zIndex = -1) * @private */ drawGeom_(e, geom, shadow) { if (this.fade_) { e.context.globalAlpha = this.fade_(1 - e.elapsed) } var style = e.style for (var i = 0; i < style.length; i++) { // Prevent crach if the style is not ready (image not loaded) try { var vectorContext = e.vectorContext || ol.render.getVectorContext(e) var s = ol.ext.getVectorContextStyle(e, style[i]) vectorContext.setStyle(s) if (s.getZIndex() < 0) vectorContext.drawGeometry(shadow || geom) else vectorContext.drawGeometry(geom) } catch (e) { /* ok */ } } } /** Function to perform manipulations onpostcompose. * This function is called with an ol.featureAnimationEvent argument. * The function will be overridden by the child implementation. * Return true to keep this function for the next frame, false to remove it. * @param {ol.featureAnimationEvent} e * @return {bool} true to continue animation. * @api */ animate( /* e */) { return false } } /** Hidden style: a transparent style */ ol.featureAnimation.hiddenStyle = new ol.style.Style({ image: new ol.style.Circle({}), stroke: new ol.style.Stroke({ color: 'transparent' }) }); /** An animation controler object an object to control animation with start, stop and isPlaying function. * To be used with {@link olx.Map#animateFeature} or {@link ol.layer.Vector#animateFeature} * @typedef {Object} animationControler * @property {function} start - start animation. * @property {function} stop - stop animation option arguments can be passed in animationend event. * @property {function} isPlaying - return true if animation is playing. */ /** Animate feature on a map * @function * @param {ol.Feature} feature Feature to animate * @param {ol.featureAnimation|Array} fanim the animation to play * @return {animationControler} an object to control animation with start, stop and isPlaying function */ ol.Map.prototype.animateFeature = function(feature, fanim) { // Get or create an animation layer associated with the map var layer = this._featureAnimationLayer; if (!layer) { layer = this._featureAnimationLayer = new ol.layer.Vector({ source: new ol.source.Vector() }); layer.setMap(this); } // Animate feature on this layer layer.getSource().addFeature(feature); var listener = fanim.on('animationend', function(e) { if (e.feature===feature) { // Remove feature on end layer.getSource().removeFeature(feature); ol.Observable.unByKey(listener); } }); layer.animateFeature(feature, fanim); }; /** Animate feature on a vector layer * @fires animationstart, animationend * @param {ol.Feature} feature Feature to animate * @param {ol.featureAnimation|Array} fanim the animation to play * @param {boolean} useFilter use the filters of the layer * @return {animationControler} an object to control animation with start, stop and isPlaying function */ ol.layer.Base.prototype.animateFeature = function(feature, fanim, useFilter) { var self = this; var listenerKey; // Save style var style = feature.getStyle(); var flashStyle = style || (this.getStyleFunction ? this.getStyleFunction()(feature) : null); if (!flashStyle) flashStyle=[]; if (!(flashStyle instanceof Array)) flashStyle = [flashStyle]; // Structure pass for animating var event = { // Frame context vectorContext: null, frameState: null, start: 0, time: 0, elapsed: 0, extent: false, // Feature information feature: feature, geom: feature.getGeometry(), typeGeom: feature.getGeometry().getType(), bbox: feature.getGeometry().getExtent(), coord: ol.extent.getCenter(feature.getGeometry().getExtent()), style: flashStyle }; if (!(fanim instanceof Array)) fanim = [fanim]; // Remove null animations for (var i=fanim.length-1; i>=0; i--) { if (fanim[i].duration_===0) fanim.splice(i,1); } var nb=0, step = 0; // Filter availiable on the layer var filters = (useFilter && this.getFilters) ? this.getFilters() : []; function animate(e) { event.type = e.type; try { event.vectorContext = e.vectorContext || ol.render.getVectorContext(e); } catch(e) { /* nothing todo */ } event.frameState = e.frameState; event.inversePixelTransform = e.inversePixelTransform; if (!event.extent) { event.extent = e.frameState.extent; event.start = e.frameState.time; event.context = e.context; } event.time = e.frameState.time - event.start; event.elapsed = event.time / fanim[step].duration_; if (event.elapsed > 1) event.elapsed = 1; // Filter e.context.save(); filters.forEach(function(f) { if (f.get('active')) f.precompose(e); }); if (this.getOpacity) { e.context.globalAlpha = this.getOpacity(); } // Stop animation? if (!fanim[step].animate(event)) { nb++; // Repeat animation if (nb < fanim[step].repeat_) { event.extent = false; } else if (step < fanim.length-1) { // newt step fanim[step].dispatchEvent({ type:'animationend', feature: feature }); step++; nb=0; event.extent = false; } else { // the end stop(); } } else { var animEvent = { type: 'animating', step: step, start: event.start, time: event.time, elapsed: event.elapsed, rotation: event.rotation||0, geom: event.geom, coordinate: event.coord, feature: feature, extra: event.extra || {} }; fanim[step].dispatchEvent(animEvent); self.dispatchEvent(animEvent); } filters.forEach(function(f) { if (f.get('active')) f.postcompose(e); }); e.context.restore(); // tell OL3 to continue postcompose animation e.frameState.animate = true; } // Stop animation function stop(options) { ol.Observable.unByKey(listenerKey); listenerKey = null; feature.setStyle(style); event.stop = (new Date).getTime(); // Send event var eventEnd = { type:'animationend', feature: feature }; if (options) { for (var i in options) if (options.hasOwnProperty(i)) { eventEnd[i] = options[i]; } } fanim[step].dispatchEvent(eventEnd); self.dispatchEvent(eventEnd); } // Launch animation function start(options) { if (fanim.length && !listenerKey) { // Restart at stop time if (event.stop) { event.start = (new Date).getTime() - event.stop + event.start; event.stop = 0; } // Compose listenerKey = self.on(['postcompose','postrender'], animate.bind(self)); // map or layer? if (self.renderSync) { try { self.renderSync(); } catch(e) { /* ok */ } } else { self.changed(); } // Hide feature while animating feature.setStyle(fanim[step].hiddenStyle || ol.featureAnimation.hiddenStyle); // Send event var eventStart = { type:'animationstart', feature: feature }; if (options) { for (var i in options) if (options.hasOwnProperty(i)) { eventStart[i] = options[i]; } } fanim[step].dispatchEvent(eventStart); self.dispatchEvent(eventStart); } } start(); // Return animation controler return { start: start, stop: stop, isPlaying: function() { return (!!listenerKey); } }; }; /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Blink a feature * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationOptions} options * @param {Number} options.nb number of blink, default 10 */ ol.featureAnimation.Blink = class olfeatureAnimationBlink extends ol.featureAnimation { constructor(options) { super(options); this.set('nb', options.nb || 10); } /** Animate: Show or hide feature depending on the laptimes * @param {ol.featureAnimationEvent} e */ animate(e) { if (!(Math.round(this.easing_(e.elapsed) * this.get('nb')) % 2)) { this.drawGeom_(e, e.geom); } return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Bounce animation: * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationBounceOptions} options * @param {Integer} options.bounce number of bounce, default 3 * @param {Integer} options.amplitude bounce amplitude,default 40 * @param {ol.easing} options.easing easing used for decaying amplitude, use function(){return 0} for no decay, default ol.easing.linear * @param {Integer} options.duration duration in ms, default 1000 */ ol.featureAnimation.Bounce = class olfeatureAnimationBounce extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); this.amplitude_ = options.amplitude || 40; this.bounce_ = -Math.PI * (options.bounce || 3); } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { var flashGeom = e.geom.clone(); /* var t = this.easing_(e.elapsed) t = Math.abs(Math.sin(this.bounce_*t)) * this.amplitude_ * (1-t) * e.frameState.viewState.resolution; */ var t = Math.abs(Math.sin(this.bounce_ * e.elapsed)) * this.amplitude_ * (1 - this.easing_(e.elapsed)) * e.frameState.viewState.resolution; flashGeom.translate(0, t); this.drawGeom_(e, flashGeom, e.geom); return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Drop animation: drop a feature on the map * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationDropOptions} options * @param {Number} options.speed speed of the feature if 0 the duration parameter will be used instead, default 0 * @param {Number} options.side top or bottom, default top */ ol.featureAnimation.Drop = class olfeatureAnimationDrop extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); this.speed_ = options.speed || 0; this.side_ = options.side || 'top'; } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { if (!e.time) { var angle = e.frameState.viewState.rotation; var s = e.frameState.size[1] * e.frameState.viewState.resolution; if (this.side_ != 'top') s *= -1; this.dx = -Math.sin(angle) * s; this.dy = Math.cos(angle) * s; if (this.speed_) { this.duration_ = s / this.speed_ / e.frameState.viewState.resolution; } } // Animate var flashGeom = e.geom.clone(); flashGeom.translate( this.dx * (1 - this.easing_(e.elapsed)), this.dy * (1 - this.easing_(e.elapsed)) ); this.drawGeom_(e, flashGeom, e.geom); return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Fade animation: feature fade in * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationOptions} options */ ol.featureAnimation.Fade = class olfeatureAnimationFade extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); this.speed_ = options.speed || 0; } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { e.context.globalAlpha = this.easing_(e.elapsed); this.drawGeom_(e, e.geom); return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Do nothing for a given duration * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationShowOptions} options * */ ol.featureAnimation.None = class olfeatureAnimationNone extends ol.featureAnimation { constructor(options) { super(options); } /** Animate: do nothing during the laps time * @param {ol.featureAnimationEvent} e */ animate(e) { return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Do nothing * @constructor * @extends {ol.featureAnimation} */ ol.featureAnimation.Null = class olfeatureAnimationNull extends ol.featureAnimation { constructor() { super({ duration:0 }); } }; /* Copyright (c) 2016-2018 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Path animation: feature follow a path * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationPathOptions} options extend ol.featureAnimation options * @param {Number} options.speed speed of the feature, if 0 the duration parameter will be used instead, default 0 * @param {Number|boolean} options.rotate rotate the symbol when following the path, true or the initial rotation, default false * @param {ol.geom.LineString|ol.Feature} options.path the path to follow * @param {Number} options.duration duration of the animation in ms */ ol.featureAnimation.Path = class olfeatureAnimationPath extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); this.speed_ = options.speed || 0; this.path_ = options.path; switch (options.rotate) { case true: case 0: this.rotate_ = 0; break; default: this.rotate_ = options.rotate || false; break; } if (this.path_ && this.path_.getGeometry) this.path_ = this.path_.getGeometry(); if (this.path_ && this.path_.getLineString) this.path_ = this.path_.getLineString(); if (this.path_.getLength) { this.dist_ = this.path_.getLength(); if (this.path_ && this.path_.getCoordinates) this.path_ = this.path_.getCoordinates(); } else { this.dist_ = 0; } if (this.speed_ > 0) this.duration_ = this.dist_ / this.speed_; } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { // First time if (!e.time) { if (!this.dist_) return false; } var dmax = this.dist_ * this.easing_(e.elapsed); var p0, p, s, dx, dy, dl, d = 0; p = this.path_[0]; // Linear interpol for (var i = 1; i < this.path_.length; i++) { p0 = p; p = this.path_[i]; dx = p[0] - p0[0]; dy = p[1] - p0[1]; dl = Math.sqrt(dx * dx + dy * dy); if (dl && d + dl >= dmax) { e.extra = { index: i, coordinates: p}; s = (dmax - d) / dl; p = [p0[0] + (p[0] - p0[0]) * s, p0[1] + (p[1] - p0[1]) * s]; break; } d += dl; } // Rotate symbols var style = e.style; e.rotation = Math.PI / 2 + Math.atan2(p0[1] - p[1], p0[0] - p[0]); if (this.rotate_ !== false) { var st = []; var angle = this.rotate_ - e.rotation + e.frameState.viewState.rotation; e.rotation = Math.PI / 2 + Math.atan2(p0[1] - p[1], p0[0] - p[0]); for (var k = 0; s = e.style[k]; k++) { if (s.getImage()) { //s = s.clone(); s.getImage().setRotation(angle); } st.push(s); } // Rotated style e.style = st; } e.geom.setCoordinates(p); // Animate this.drawGeom_(e, e.geom); // restore style (if modify by rotation) e.style = style; return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Shakee animation: * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationShakeOptions} options * @param {Integer} options.bounce number o bounds, default 6 * @param {Integer} options.amplitude amplitude of the animation, default 40 * @param {bool} options.horizontal shake horizontally default false (vertical) */ ol.featureAnimation.Shake = class olfeatureAnimationShake extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); // this.easing_ = options.easing_ || function(t){return (0.5+t)*t -0.5*t ;}; this.amplitude_ = options.amplitude || 40; this.bounce_ = -Math.PI * (options.bounce || 6); this.horizontal_ = options.horizontal; } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { var flashGeom = e.geom.clone(); var shadow = e.geom.clone(); var t = this.easing_(e.elapsed); t = Math.sin(this.bounce_ * t) * this.amplitude_ * (1 - t) * e.frameState.viewState.resolution; if (this.horizontal_) { flashGeom.translate(t, 0); shadow.translate(t, 0); } else flashGeom.translate(0, t); this.drawGeom_(e, flashGeom, shadow); return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Show an object for a given duration * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationOptions} options */ ol.featureAnimation.Show = class olfeatureAnimationShow extends ol.featureAnimation { constructor(options) { super(options); } /** Animate: just show the object during the laps time * @param {ol.featureAnimationEvent} e */ animate(e) { this.drawGeom_(e, e.geom); return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Slice animation: feature enter from left * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationSlideOptions} options * @param {Number} options.speed speed of the animation, if 0 the duration parameter will be used instead, default 0 */ ol.featureAnimation.Slide = class olfeatureAnimationSlide extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); this.speed_ = options.speed || 0; this.side_ = options.side || 'left'; } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { if (!e.time) { if (this.side_ == 'left') this.dx = (e.extent[0] - e.bbox[2]); else this.dx = (e.extent[2] - e.bbox[0]); if (this.speed_) this.duration_ = Math.abs(this.dx) / this.speed_ / e.frameState.viewState.resolution; } // Animate var flashGeom = e.geom.clone(); flashGeom.translate(this.dx * (1 - this.easing_(e.elapsed)), 0); this.drawGeom_(e, flashGeom); return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Teleport a feature at a given place (feat. Star Trek) * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationOptions} options */ ol.featureAnimation.Teleport = class olfeatureAnimationTeleport extends ol.featureAnimation { constructor(options) { super(options); } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { var sc = this.easing_(e.elapsed); if (sc) { e.context.save(); var ratio = e.frameState.pixelRatio; e.context.globalAlpha = sc; e.context.scale(sc, 1 / sc); var m = e.frameState.coordinateToPixelTransform; var dx = (1 / sc - 1) * ratio * (m[0] * e.coord[0] + m[1] * e.coord[1] + m[4]); var dy = (sc - 1) * ratio * (m[2] * e.coord[0] + m[3] * e.coord[1] + m[5]); e.context.translate(dx, dy); this.drawGeom_(e, e.geom); e.context.restore(); } return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Slice animation: feature enter from left * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationThrowOptions} options * @param {left|right} options.side side of the animation, default left */ ol.featureAnimation.Throw = class olfeatureAnimationThrow extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); this.speed_ = options.speed || 0; this.side_ = options.side || 'left'; } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { if (!e.time && this.speed_) { var dx, dy; if (this.side_ == 'left') { dx = this.dx = e.extent[0] - e.bbox[2]; dy = this.dy = e.extent[3] - e.bbox[1]; } else { dx = this.dx = e.extent[2] - e.bbox[0]; dy = this.dy = e.extent[3] - e.bbox[1]; } this.duration_ = Math.sqrt(dx * dx + dy * dy) / this.speed_ / e.frameState.viewState.resolution; } // Animate var flashGeom = e.geom.clone(); var shadow = e.geom.clone(); flashGeom.translate(this.dx * (1 - this.easing_(e.elapsed)), this.dy * Math.cos(Math.PI / 2 * this.easing_(e.elapsed))); shadow.translate(this.dx * (1 - this.easing_(e.elapsed)), 0); this.drawGeom_(e, flashGeom, shadow); return (e.time <= this.duration_); } } /* Copyright (c) 2016 Jean-Marc VIGLINO, released under the CeCILL license (http://www.cecill.info/). */ /** Zoom animation: feature zoom in (for points) * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationZoomOptions} options * @param {bool} options.zoomOut to zoom out */ ol.featureAnimation.Zoom = class olfeatureAnimationZoom extends ol.featureAnimation { constructor(options) { options = options || {}; super(options); this.set('zoomout', options.zoomOut); } /** Animate * @param {ol.featureAnimationEvent} e */ animate(e) { var fac = this.easing_(e.elapsed); if (fac) { if (this.get('zoomout')) fac = 1 / fac; var style = e.style; var i, imgs, sc = []; for (i = 0; i < style.length; i++) { imgs = style[i].getImage(); if (imgs) { sc[i] = imgs.getScale(); // ol >= v6 if (e.type === 'postrender') imgs.setScale(sc[i] * fac / e.frameState.pixelRatio); else imgs.setScale(sc[i] * fac); } } this.drawGeom_(e, e.geom); for (i = 0; i < style.length; i++) { imgs = style[i].getImage(); if (imgs) imgs.setScale(sc[i]); } } /* var sc = this.easing_(e.elapsed); if (sc) { e.context.save() console.log(e) var ratio = e.frameState.pixelRatio; var m = e.frameState.coordinateToPixelTransform; var dx = (1/(sc)-1)* ratio * (m[0]*e.coord[0] + m[1]*e.coord[1] +m[4]); var dy = (1/(sc)-1)*ratio * (m[2]*e.coord[0] + m[3]*e.coord[1] +m[5]); e.context.scale(sc,sc); e.context.translate(dx,dy); this.drawGeom_(e, e.geom); e.context.restore() } */ return (e.time <= this.duration_); } } /** Zoom animation: feature zoom out (for points) * @constructor * @extends {ol.featureAnimation} * @param {ol.featureAnimationZoomOptions} options */ ol.featureAnimation.ZoomOut = class olfeatureAnimationZoomOut extends ol.featureAnimation.Zoom { constructor(options) { options = options || {}; options.zoomOut = true; super(options); } } /* 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). */ /* Namespace */ ol.filter = {}; /** * @classdesc * Abstract base class; normally only used for creating subclasses and not instantiated in apps. * Used to create filters * Use {@link ol.layer.Base#addFilter}, {@link ol.layer.Base#removeFilter} or {@link ol.layer.Base#getFilters} * to handle filters on layers. * * @constructor * @extends {ol.Object} * @param {Object} options * @param {boolean} [options.active] */ ol.filter.Base = class olfilterBase extends ol.Object { constructor(options) { super(options) // Array of postcompose listener this._listener = [] if (options && options.active === false) { this.set('active', false) } else { this.set('active', true) } } /** Activate / deactivate filter * @param {boolean} b */ setActive(b) { this.set('active', b === true) } /** Get filter active * @return {boolean} */ getActive() { return this.get('active') } } ;(function(){ /** Internal function * @this {ol.filter} this the filter * @private */ function precompose_(e) { if (this.get('active') && e.context) this.precompose(e); } /** Internal function * @this {ol.filter} this the filter * @private */ function postcompose_(e) { if (this.get('active') && e.context) this.postcompose(e); } /** Force filter redraw / Internal function * @this {ol.Map|ol.layer.Layer} this: the map or layer the filter is added to * @private */ function filterRedraw_(/* e */) { if (this.renderSync) { try { this.renderSync(); } catch(e) { /* ok */ } } else { this.changed(); } } /** Add a filter to an ol object * @this {ol.Map|ol.layer.Layer} this: the map or layer the filter is added to * @private */ function addFilter_(filter) { if (!this.filters_) this.filters_ = []; this.filters_.push(filter); if (filter.addToLayer) filter.addToLayer(this); if (filter.precompose) filter._listener.push ( { listener: this.on(['precompose','prerender'], precompose_.bind(filter)), target: this }); if (filter.postcompose) filter._listener.push ( { listener: this.on(['postcompose','postrender'], postcompose_.bind(filter)), target: this }); filter._listener.push ( { listener: filter.on('propertychange', filterRedraw_.bind(this)), target: this }); filterRedraw_.call (this); } /** Remove a filter to an ol object * @this {ol.Map|ol.layer.Layer} this: the map or layer the filter is added to * @private */ function removeFilter_(filter) { var i if (!this.filters_) this.filters_ = []; if (!filter) { this.filters_.forEach(function(f) { this.removeFilter(f) }.bind(this)) return; } for (i=this.filters_.length-1; i>=0; i--) { if (this.filters_[i]===filter) this.filters_.splice(i,1); } for (i=filter._listener.length-1; i>=0; i--) { // Remove listener on this object if (filter._listener[i].target === this) { if (filter.removeFromLayer) filter.removeFromLayer(this); ol.Observable.unByKey(filter._listener[i].listener); filter._listener.splice(i,1); } } filterRedraw_.call (this); } /** Add a filter to an ol.Map * @param {ol.filter} */ ol.Map.prototype.addFilter = function (filter) { console.warn('[OL-EXT] addFilter deprecated on map.') addFilter_.call (this, filter); }; /** Remove a filter to an ol.Map * @param {ol.filter} */ ol.Map.prototype.removeFilter = function (filter) { removeFilter_.call (this, filter); }; /** Get filters associated with an ol.Map * @return {Array} */ ol.Map.prototype.getFilters = function () { return this.filters_ || []; }; /** Add a filter to an ol.Layer * @param {ol.filter} */ ol.layer.Base.prototype.addFilter = function (filter) { addFilter_.call (this, filter); }; /** Remove a filter to an ol.Layer * @param {ol.filter} */ ol.layer.Base.prototype.removeFilter = function (filter) { removeFilter_.call (this, filter); }; /** Get filters associated with an ol.Map * @return {Array} */ ol.layer.Base.prototype.getFilters = function () { return this.filters_ || []; }; })(); /* 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). */ /** Mask drawing using an ol.Feature * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {Object} [options] * @param {ol.Feature} [options.feature] feature to mask with * @param {ol.style.Fill} [options.fill] style to fill with * @param {number} [options.shadowWidth=0] shadow width, default no shadow * @param {boolean} [options.shadowMapUnits=false] true if the shadow width is in mapUnits * @param {ol.colorLike} [options.shadowColor='rgba(0,0,0,.5)'] shadow color, default * @param {boolean} [options.inner=false] mask inner, default false * @param {boolean} [options.wrapX=false] wrap around the world, default false */ ol.filter.Mask = class olfilterMask extends ol.filter.Base { constructor(options) { options = options || {}; super(options); if (options.feature) { switch (options.feature.getGeometry().getType()) { case 'Polygon': case 'MultiPolygon': this.feature_ = options.feature; break; default: break; } } this.set('inner', options.inner); this._fillColor = options.fill ? ol.color.asString(options.fill.getColor()) || 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.2)'; this._shadowColor = options.shadowColor ? ol.color.asString(options.shadowColor) || 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.5)'; this.set('shadowWidth', options.shadowWidth || 0); this.set('shadowMapUnits', options.shadowMapUnits === true); } /** Set filter fill color * @param {ol/colorLike} color */ setFillColor(color) { this._fillColor = color ? ol.color.asString(color) || 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.2)'; } /** Set filter shadow color * @param {ol/colorLike} color */ setShadowColor(color) { this._shadowColor = color ? ol.color.asString(color) || 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.5)'; } /** Draw the feature into canvas * @private */ drawFeaturePath_(e, out) { var ctx = e.context; var canvas = ctx.canvas; var ratio = e.frameState.pixelRatio; // Transform var tr; if (e.frameState.coordinateToPixelTransform) { var m = e.frameState.coordinateToPixelTransform; // ol > 6 if (e.inversePixelTransform) { var ipt = e.inversePixelTransform; tr = function (pt) { pt = [ (pt[0] * m[0] + pt[1] * m[1] + m[4]), (pt[0] * m[2] + pt[1] * m[3] + m[5]) ]; return [ (pt[0] * ipt[0] - pt[1] * ipt[1] + ipt[4]), (-pt[0] * ipt[2] + pt[1] * ipt[3] + ipt[5]) ]; }; } else { // ol 5 tr = function (pt) { return [ (pt[0] * m[0] + pt[1] * m[1] + m[4]) * ratio, (pt[0] * m[2] + pt[1] * m[3] + m[5]) * ratio ]; }; } } else { // Older version m = e.frameState.coordinateToPixelMatrix; tr = function (pt) { return [ (pt[0] * m[0] + pt[1] * m[1] + m[12]) * ratio, (pt[0] * m[4] + pt[1] * m[5] + m[13]) * ratio ]; }; } // Geometry var ll = this.feature_.getGeometry().getCoordinates(); if (this.feature_.getGeometry().getType() === 'Polygon') ll = [ll]; // Draw feature at dx world function drawll(dx) { for (var l = 0; l < ll.length; l++) { var c = ll[l]; for (var i = 0; i < c.length; i++) { var pt = tr([c[i][0][0] + dx, c[i][0][1]]); ctx.moveTo(pt[0], pt[1]); for (var j = 1; j < c[i].length; j++) { pt = tr([c[i][j][0] + dx, c[i][j][1]]); ctx.lineTo(pt[0], pt[1]); } } } } ctx.beginPath(); if (out) { ctx.moveTo(-100, -100); ctx.lineTo(canvas.width + 100, -100); ctx.lineTo(canvas.width + 100, canvas.height + 100); ctx.lineTo(-100, canvas.height + 100); ctx.lineTo(-100, -100); } // Draw current world if (this.get('wrapX')) { var worldExtent = e.frameState.viewState.projection.getExtent(); var worldWidth = worldExtent[2] - worldExtent[0]; var extent = e.frameState.extent; var fExtent = this.feature_.getGeometry().getExtent(); var fWidth = fExtent[2] - fExtent[1]; var start = Math.floor((extent[0] + fWidth - worldExtent[0]) / worldWidth); var end = Math.floor((extent[2] - fWidth - worldExtent[2]) / worldWidth) + 1; if (start > end) { [start, end] = [end, start]; } for (var i = start; i <= end; i++) { drawll(i * worldWidth); } } else { drawll(0); } } /** * @param {ol/Event} e * @private */ postcompose(e) { if (!this.feature_) return; var ctx = e.context; ctx.save(); this.drawFeaturePath_(e, !this.get('inner')); ctx.fillStyle = this._fillColor; ctx.fill('evenodd'); // Draw shadow if (this.get('shadowWidth')) { var width = this.get('shadowWidth') * e.frameState.pixelRatio if (this.get('shadowMapUnits')) width /= e.frameState.viewState.resolution; ctx.clip('evenodd'); ctx.filter = 'blur(' + width + 'px)'; ctx.strokeStyle = this._shadowColor; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.lineWidth = width; ctx.stroke(); } ctx.restore(); } } /** Add a mix-blend-mode CSS filter (not working with IE or ol<6). * Add a className to the layer to apply the filter to a specific layer. * With ol<6 use {@link ol.filter.Composite} instead. * Use {@link ol.layer.Base#addFilter}, {@link ol.layer.Base#removeFilter} or {@link ol.layer.Base#getFilters} * @constructor * @extends {ol.Object} * @param {Object} options * @param {string} options.blend mix-blend-mode to apply (as {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode CSS property}) * @param {string} options.filter filter to apply (as {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter CSS property}) * @param {boolan} options.display show/hide layer from CSS (but keep it in layer list) */ ol.filter.CSS = class olfilterCSS extends ol.filter.Base { constructor(options) { super(options); this._layers = []; } /** Modify blend mode * @param {string} blend mix-blend-mode to apply (as {@link https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode CSS property}) */ setBlend(blend) { this.set('blend', blend); this._layers.forEach(function (layer) { layer.once('postrender', function (e) { e.context.canvas.parentNode.style['mix-blend-mode'] = blend || ''; }.bind(this)); layer.changed(); }); } /** Modify filter mode * @param {string} filter filter to apply (as {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter CSS property}) */ setFilter(filter) { this.set('filter', filter); this._layers.forEach(function (layer) { layer.once('postrender', function (e) { e.context.canvas.parentNode.style['filter'] = filter || ''; }.bind(this)); layer.changed(); }); } /** Modify layer visibility (but keep it in the layer list) * @param {bolean} display */ setDisplay(display) { this.set('display', display); this._layers.forEach(function (layer) { layer.once('postrender', function (e) { e.context.canvas.parentNode.style['display'] = display ? '' : 'none'; }.bind(this)); layer.changed(); }); } /** Add CSS filter to the layer * @param {ol.layer.Base} layer */ addToLayer(layer) { layer.once('postrender', function (e) { e.context.canvas.parentNode.style['mix-blend-mode'] = this.get('blend') || ''; e.context.canvas.parentNode.style['filter'] = this.get('filter') || ''; e.context.canvas.parentNode.style['display'] = this.get('display') !== false ? '' : 'none'; }.bind(this)); layer.changed(); this._layers.push(layer); // layer.getRenderer().getImage().parentNode.style['mix-blend-mode'] = 'multiply'; } /** Remove CSS filter from the layer * @param {ol.layer.Base} layer */ removeFromLayer(layer) { var pos = this._layers.indexOf(layer); if (pos >= 0) { layer.once('postrender', function (e) { e.context.canvas.parentNode.style['mix-blend-mode'] = ''; e.context.canvas.parentNode.style['filter'] = ''; e.context.canvas.parentNode.style['display'] = ''; }.bind(this)); layer.changed(); this._layers.splice(pos, 1); } } } /* 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). */ /** @typedef {Object} CanvasFilterOptions * @property {url} url Takes an IRI pointing to an SVG filter element * @property {number} blur Gaussian blur value in px * @property {number} brightness linear multiplier to the drawing, under 100: darkens the image, over 100 brightens it * @property {number} contrast Adjusts the contrast, under 0: black, 100 no change * @property {ol.pixel} shadow Applies a drop shadow effect, pixel offset * @property {number} shadowBlur Blur radius * @property {number} shadowColor * @property {number} grayscale 0: unchanged, 100: completely grayscale * @property {number} hueRotate Hue rotation angle in deg * @property {number} invert Inverts the drawing, 0: unchanged, 100: invert * @property {number} saturate Saturates the drawing, 0: unsaturated, 100: unchanged * @property {number} sepia Converts the drawing to sepia, 0: sepia, 100: unchanged */ /** Add a canvas Context2D filter to a layer * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {CanvasFilterOptions} options */ ol.filter.CanvasFilter = class olfilterCanvasFilter extends ol.filter.Base { constructor(options) { super(options); this._svg = {}; } /** Add a new svg filter * @param {string|ol.ext.SVGFilter} url IRI pointing to an SVG filter element */ addSVGFilter(url) { if (url.getId) url = '#' + url.getId(); this._svg[url] = 1; this.dispatchEvent({ type: 'propertychange', key: 'svg', oldValue: this._svg }); } /** Remove a svg filter * @param {string|ol.ext.SVGFilter} url IRI pointing to an SVG filter element */ removeSVGFilter(url) { if (url.getId) url = '#' + url.getId(); delete this._svg[url]; this.dispatchEvent({ type: 'propertychange', key: 'svg', oldValue: this._svg }); } /** * @private */ precompose() { } /** * @private */ postcompose(e) { var filter = []; // Set filters if (this.get('url') !== undefined) filter.push('url(' + this.get('url') + ')'); for (var f in this._svg) { filter.push('url(' + f + ')'); } if (this.get('blur') !== undefined) filter.push('blur(' + this.get('blur') + 'px)'); if (this.get('brightness') !== undefined) filter.push('brightness(' + this.get('brightness') + '%)'); if (this.get('contrast') !== undefined) filter.push('contrast(' + this.get('contrast') + '%)'); if (this.get('shadow') !== undefined) { filter.push('drop-shadow(' + this.get('shadow')[0] + 'px ' + this.get('shadow')[1] + 'px ' + (this.get('shadowBlur') || 0) + 'px ' + this.get('shadowColor') + ')'); } if (this.get('grayscale') !== undefined) filter.push('grayscale(' + this.get('grayscale') + '%)'); if (this.get('sepia') !== undefined) filter.push('sepia(' + this.get('sepia') + '%)'); if (this.get('hueRotate') !== undefined) filter.push('hue-rotate(' + this.get('hueRotate') + 'deg)'); if (this.get('invert') !== undefined) filter.push('invert(' + this.get('invert') + '%)'); if (this.get('saturate') !== undefined) filter.push('saturate(' + this.get('saturate') + '%)'); filter = filter.join(' '); // Apply filter if (filter) { e.context.save(); e.context.filter = filter; e.context.drawImage(e.context.canvas, 0, 0); e.context.restore(); } } } /* 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). */ /** Clip layer or map * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {Object} [options] * @param {Array} [options.coords] * @param {ol.Extent} [options.extent] * @param {string} [options.units] coords units percent (%) or pixel (px) * @param {boolean} [options.keepAspectRatio] keep aspect ratio * @param {string} [options.color] backgroundcolor */ ol.filter.Clip = class olfilterClip extends ol.filter.Base { constructor(options) { options = options || {}; super(options); this.set("coords", options.coords); this.set("units", options.units); this.set("keepAspectRatio", options.keepAspectRatio); this.set("extent", options.extent || [0, 0, 1, 1]); this.set("color", options.color); if (!options.extent && options.units != "%" && options.coords) { var xmin = Infinity; var ymin = Infinity; var xmax = -Infinity; var ymax = -Infinity; for (var i = 0, p; p = options.coords[i]; i++) { if (xmin > p[0]) xmin = p[0]; if (xmax < p[0]) xmax = p[0]; if (ymin > p[1]) ymin = p[1]; if (ymax < p[1]) ymax = p[1]; } options.extent = [xmin, ymin, xmax, ymax]; } } clipPath_(e) { var ctx = e.context; var size = e.frameState.size; var coords = this.get("coords"); if (!coords) return; var ex = this.get('extent'); var scx = 1, scy = 1; if (this.get("units") == "%") { scx = size[0] / (ex[2] - ex[0]); scy = size[1] / (ex[3] - ex[1]); } if (this.get("keepAspectRatio")) { scx = scy = Math.min(scx, scy); } var pos = this.get('position'); var dx = 0, dy = 0; if (/left/.test(pos)) { dx = -ex[0] * scx; } else if (/center/.test(pos)) { dx = size[0] / 2 - (ex[2] - ex[0]) * scx / 2; } else if (/right/.test(pos)) { dx = size[0] - (ex[2] - ex[0]) * scx; } var fx = function (x) { return x * scx + dx; }; if (/top/.test(pos)) { dy = -ex[1] * scy; } else if (/middle/.test(pos)) { dy = size[1] / 2 - (ex[3] - ex[1]) * scy / 2; } else if (/bottom/.test(pos)) { dy = size[1] - (ex[3] - ex[1]) * scy; } var fy = function (y) { return y * scy + dy; }; var pt = [fx(coords[0][0]), fy(coords[0][1])]; var tr = e.inversePixelTransform; if (tr) { pt = [ (pt[0] * tr[0] - pt[1] * tr[1] + tr[4]), (-pt[0] * tr[2] + pt[1] * tr[3] + tr[5]) ]; } ctx.moveTo(pt[0], pt[1]); for (var i = 1, p; p = coords[i]; i++) { pt = [fx(p[0]), fy(p[1])]; if (tr) { pt = [ (pt[0] * tr[0] - pt[1] * tr[1] + tr[4]), (-pt[0] * tr[2] + pt[1] * tr[3] + tr[5]) ]; } ctx.lineTo(pt[0], pt[1]); } pt = [fx(coords[0][0]), fy(coords[0][1])]; if (tr) { pt = [ (pt[0] * tr[0] - pt[1] * tr[1] + tr[4]), (-pt[0] * tr[2] + pt[1] * tr[3] + tr[5]) ]; } ctx.moveTo(pt[0], pt[1]); } /** * @private */ precompose(e) { if (!this.get("color")) { e.context.save(); e.context.beginPath(); this.clipPath_(e); e.context.clip(); } } /** * @private */ postcompose(e) { if (this.get("color")) { var ctx = e.context; var canvas = e.context.canvas; ctx.save(); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, canvas.height); ctx.lineTo(canvas.width, canvas.height); ctx.lineTo(canvas.width, canvas.height); ctx.lineTo(canvas.width, 0); ctx.lineTo(0, 0); this.clipPath_(e); ctx.fillStyle = this.get("color"); ctx.fill("evenodd"); } e.context.restore(); } } /* 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). */ /** @typedef {Object} FilterColorizeOptions * @property {ol.Color} color style to fill with * @property {string} operation 'enhance' or a CanvasRenderingContext2D.globalCompositeOperation * @property {number} value a value to modify the effect value [0-1] * @property {boolean} inner mask inner, default false * @property {boolean} preserveAlpha preserve alpha channel, default false */ /** Colorize map or layer * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @author Thomas Tilak https://github.com/thhomas * @author Jean-Marc Viglino https://github.com/viglino * @param {FilterColorizeOptions} options */ ol.filter.Colorize = class olfilterColorize extends ol.filter.Base { constructor(options) { super(options) this.setFilter(options) } /** Set options to the filter * @param {FilterColorizeOptions} [options] */ setFilter(options) { options = options || {} switch (options) { case "grayscale": options = { operation: 'hue', color: [0, 0, 0], value: 1 }; break case "invert": options = { operation: 'difference', color: [255, 255, 255], value: 1 }; break case "sepia": options = { operation: 'color', color: [153, 102, 51], value: 0.6 }; break default: break } var color = options.color ? ol.color.asArray(options.color) : [options.red, options.green, options.blue, options.value] this.set('color', ol.color.asString(color)) this.set('value', options.value || 1) this.set('preserveAlpha', options.preserveAlpha) var v switch (options.operation) { case 'hue': case 'difference': case 'color-dodge': case 'enhance': { this.set('operation', options.operation) break } case 'saturation': { v = 255 * (options.value || 0) this.set('color', ol.color.asString([0, 0, v, v || 1])) this.set('operation', options.operation) break } case 'luminosity': { v = 255 * (options.value || 0) this.set('color', ol.color.asString([v, v, v, 255])) //this.set ('operation', 'luminosity') this.set('operation', 'hard-light') break } case 'contrast': { v = 255 * (options.value || 0) this.set('color', ol.color.asString([v, v, v, 255])) this.set('operation', 'soft-light') break } default: { this.set('operation', 'color') this.setValue(options.value || 1) break } } } /** Set the filter value * @param {ol.Color} options.color style to fill with */ setValue(v) { this.set('value', v) var c = ol.color.asArray(this.get("color")) c[3] = v this.set("color", ol.color.asString(c)) } /** Set the color value * @param {number} options.value a [0-1] value to modify the effect value */ setColor(c) { c = ol.color.asArray(c) if (c) { c[3] = this.get("value") this.set("color", ol.color.asString(c)) } } /** @private */ precompose( /* e */) { } /** @private */ postcompose(e) { // Set back color hue var c2, ctx2 var ctx = e.context var canvas = ctx.canvas ctx.save() if (this.get('operation') == 'enhance') { var v = this.get('value') if (v) { var w = canvas.width var h = canvas.height if (this.get('preserveAlpha')) { c2 = document.createElement('CANVAS') c2.width = canvas.width c2.height = canvas.height ctx2 = c2.getContext('2d') ctx2.drawImage(canvas, 0, 0, w, h) ctx2.globalCompositeOperation = 'color-burn' // console.log(v) ctx2.globalAlpha = v ctx2.drawImage(c2, 0, 0, w, h) ctx2.drawImage(c2, 0, 0, w, h) ctx2.drawImage(c2, 0, 0, w, h) ctx.globalCompositeOperation = 'source-in' ctx.drawImage(c2, 0, 0) } else { ctx.globalCompositeOperation = 'color-burn' ctx.globalAlpha = v ctx.drawImage(canvas, 0, 0, w, h) ctx.drawImage(canvas, 0, 0, w, h) ctx.drawImage(canvas, 0, 0, w, h) } } } else { if (this.get('preserveAlpha')) { c2 = document.createElement('CANVAS') c2.width = canvas.width c2.height = canvas.height ctx2 = c2.getContext('2d') ctx2.drawImage(canvas, 0, 0) ctx2.globalCompositeOperation = this.get('operation') ctx2.fillStyle = this.get('color') ctx2.fillRect(0, 0, canvas.width, canvas.height) ctx.globalCompositeOperation = 'source-in' ctx.drawImage(c2, 0, 0) } else { ctx.globalCompositeOperation = this.get('operation') ctx.fillStyle = this.get('color') ctx.fillRect(0, 0, canvas.width, canvas.height) } } ctx.restore() } } /* 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). */ /** Add a composite filter on a layer. * With ol6+ you'd better use {@link ol.filter.CSS} instead. * Use {@link ol.layer.Base#addFilter}, {@link ol.layer.Base#removeFilter} or {@link ol.layer.Base#getFilters} * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {Object} options * @param {string} options.operation composite operation */ ol.filter.Composite = class olfilterComposite extends ol.filter.Base { constructor(options) { super(options); this.set("operation", options.operation || "source-over"); } /** Change the current operation * @param {string} operation composite function */ setOperation(operation) { this.set('operation', operation || "source-over"); } precompose(e) { var ctx = e.context; ctx.save(); ctx.globalCompositeOperation = this.get('operation'); } postcompose(e) { e.context.restore(); } } /* 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). */ /** Crop drawing using an ol.Feature * @constructor * @requires ol.filter * @requires ol.filter.Mask * @extends {ol.filter.Mask} * @param {Object} [options] * @param {ol.Feature} [options.feature] feature to crop with * @param {number} [options.shadowWidth=0] shadow width, default no shadow * @param {boolean} [options.shadowMapUnits=false] true if the shadow width is in mapUnits * @param {ol.colorLike} [options.shadowColor='rgba(0,0,0,.5)'] shadow color, default * @param {boolean} [options.inner=false] mask inner, default false * @param {boolean} [options.wrapX=false] wrap around the world, default false */ ol.filter.Crop = class olfilterCrop extends ol.filter.Mask { constructor(options) { options = options || {}; super(options); } /** * @param {ol/Event} e * @private */ precompose(e) { if (this.feature_) { var ctx = e.context; ctx.save(); this.drawFeaturePath_(e, this.get('inner')); ctx.clip('evenodd'); } } /** * @param {ol/Event} e * @private */ postcompose(e) { if (this.feature_) { var ctx = e.context; ctx.restore(); // Draw shadow if (this.get('shadowWidth')) { ctx.save() var width = this.get('shadowWidth') * e.frameState.pixelRatio if (this.get('shadowMapUnits')) width /= e.frameState.viewState.resolution; this.drawFeaturePath_(e, !this.get('inner')); ctx.clip('evenodd'); ctx.filter = 'blur(' + width + 'px)'; ctx.strokeStyle = this._shadowColor; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.lineWidth = width; ctx.stroke(); ctx.restore() } } } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Fold filer map * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {Object} [options] * @param {Array} [options.fold[8,4]] number of fold (horizontal and vertical) * @param {number} [options.margin=8] margin in px, default 8 * @param {number} [options.padding=8] padding in px, default 8 * @param {number|number[]} [options.fsize=[8,10]] fold size in px, default 8,10 * @param {boolean} [options.fill=false] true to fill the background, default false * @param {boolean} [options.shadow=true] true to display shadow * @param {boolean} [options.opacity=.2] effect opacity */ ol.filter.Fold = class olfilterFold extends ol.filter.Base { constructor(options) { options = options || {}; super(options); this.set('fold', options.fold || [8, 4]); this.set('margin', options.margin || 8); this.set('padding', options.padding || 8); if (typeof options.fsize == 'number') options.fsize = [options.fsize, options.fsize]; this.set('fsize', options.fsize || [8, 10]); this.set('fill', options.fill); this.set('shadow', options.shadow !== false); this.set('opacity', (options.hasOwnProperty('opacity') ? options.opacity : .2)); } drawLine_(ctx, d, m) { var canvas = ctx.canvas; var fold = this.get("fold"); var w = canvas.width; var h = canvas.height; var x, y, i; ctx.beginPath(); ctx.moveTo(m, m); for (i = 1; i <= fold[0]; i++) { x = i * w / fold[0] - (i == fold[0] ? m : 0); y = d[1] * (i % 2) + m; ctx.lineTo(x, y); } for (i = 1; i <= fold[1]; i++) { x = w - d[0] * (i % 2) - m; y = i * h / fold[1] - (i == fold[1] ? d[0] * (fold[0] % 2) + m : 0); ctx.lineTo(x, y); } for (i = fold[0]; i > 0; i--) { x = i * w / fold[0] - (i == fold[0] ? d[0] * (fold[1] % 2) + m : 0); y = h - d[1] * (i % 2) - m; ctx.lineTo(x, y); } for (i = fold[1]; i > 0; i--) { x = d[0] * (i % 2) + m; y = i * h / fold[1] - (i == fold[1] ? m : 0); ctx.lineTo(x, y); } ctx.closePath(); } precompose(e) { var ctx = e.context; ctx.save(); ctx.shadowColor = "rgba(0,0,0,0.3)"; ctx.shadowBlur = 8; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 3; this.drawLine_(ctx, this.get("fsize"), this.get("margin")); ctx.fillStyle = "#fff"; if (this.get('fill')) ctx.fill(); ctx.strokeStyle = "rgba(0,0,0,0.1)"; ctx.stroke(); ctx.restore(); ctx.save(); this.drawLine_(ctx, this.get("fsize"), this.get("margin") + this.get("padding")); ctx.clip(); } postcompose(e) { var ctx = e.context; var canvas = ctx.canvas; ctx.restore(); ctx.save(); this.drawLine_(ctx, this.get("fsize"), this.get("margin")); ctx.clip(); if (this.get('shadow')) { var fold = this.get("fold"); var w = canvas.width / fold[0]; var h = canvas.height / fold[1]; var grd = ctx.createRadialGradient(5 * w / 8, 5 * w / 8, w / 4, w / 2, w / 2, w); grd.addColorStop(0, "transparent"); grd.addColorStop(1, "rgba(0,0,0," + this.get('opacity') + ")"); ctx.fillStyle = grd; ctx.scale(1, h / w); for (var i = 0; i < fold[0]; i++) for (var j = 0; j < fold[1]; j++) { ctx.save(); ctx.translate(i * w, j * w); ctx.fillRect(0, 0, w, w); ctx.restore(); } } ctx.restore(); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Make a map or layer look like made of a set of Lego bricks. * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {Object} [options] * @param {string} [options.channel] RGB channels: 'r', 'g' or 'b', default use intensity * @param {ol.colorlike} [options.color] color, default black * @param {number} [options.size] point size, default 30 * @param {null | string | undefined} [options.crossOrigin] crossOrigin attribute for loaded images. */ ol.filter.Halftone = class olfilterHalftone extends ol.filter.Base { constructor(options) { options = options || {}; super(options); this.internal_ = document.createElement('canvas'); this.setSize(options.size); this.set('channel', options.channel); } /** Set the current size * @param {number} width the pattern width, default 30 */ setSize(size) { size = Number(size) || 30; this.set("size", size); } /** Postcompose operation */ postcompose(e) { var ctx = e.context; var canvas = ctx.canvas; var ratio = e.frameState.pixelRatio; // ol v6+ if (e.type === 'postrender') { ratio = 1; } ctx.save(); // resize var step = this.get('size') * ratio; var p = e.frameState.extent; var res = e.frameState.viewState.resolution / ratio; var offset = [-Math.round((p[0] / res) % step), Math.round((p[1] / res) % step)]; var ctx2 = this.internal_.getContext("2d"); var w = this.internal_.width = canvas.width; var h = this.internal_.height = canvas.height; // No smoothing please ctx2.webkitImageSmoothingEnabled = ctx2.mozImageSmoothingEnabled = ctx2.msImageSmoothingEnabled = ctx2.imageSmoothingEnabled = false; var w2 = Math.floor((w - offset[0]) / step); var h2 = Math.floor((h - offset[1]) / step); ctx2.drawImage(canvas, offset[0], offset[1], w2 * step, h2 * step, 0, 0, w2, h2); var data = ctx2.getImageData(0, 0, w2, h2).data; // Draw tone ctx.clearRect(0, 0, w, h); ctx.fillStyle = ol.color.asString(this.get('color') || '#000'); for (var x = 0; x < w2; x++) for (var y = 0; y < h2; y++) if (data[x * 4 + 3 + y * w2 * 4]) { var pix; switch (this.get('channel')) { case 'r': pix = data[x * 4 + y * w2 * 4] / 2.55; break; case 'g': pix = data[x * 4 + 1 + y * w2 * 4] / 2.55; break; case 'b': pix = data[x * 4 + 2 + y * w2 * 4] / 2.55; break; default: pix = ol.color.toHSL([data[x * 4 + y * w2 * 4], data[x * 4 + 1 + y * w2 * 4], data[x * 4 + 2 + y * w2 * 4]]); pix = pix[2]; break; } var l = (100 - pix) / 140; if (l) { ctx.beginPath(); ctx.arc(offset[0] + step / 2 + x * step, offset[1] + step / 2 + y * step, step * l, 0, 2 * Math.PI); ctx.closePath(); ctx.fill(); } } ctx.restore(); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Make a map or layer look like made of a set of Lego bricks. * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {Object} [options] * @param {string} [options.img] * @param {number} [options.brickSize] size of te brick, default 30 * @param {null | string | undefined} [options.crossOrigin] crossOrigin attribute for loaded images. */ ol.filter.Lego = class olfilterLego extends ol.filter.Base { constructor(options) { options = options || {}; super(options); var img = new Image(); // Default image img.src = this.img[options.img] || this.img.ol3; img.crossOrigin = options.crossOrigin || null; // and pattern this.pattern = { canvas: document.createElement('canvas') }; this.setBrick(options.brickSize, img); this.internal_ = document.createElement('canvas'); } /** Overwrite to handle brickSize * @param {string} key * @param {any} val */ set(key, val) { super.set(key, val); if (key == "brickSize" && this.pattern && this.pattern.canvas.width != val) { this.setBrick(val); } } /** Set the current brick * @param {number} width the pattern width, default 30 * @param {'brick'|'ol3'|'lego'|undefined} img the pattern, default ol3 * @param {string} crossOrigin */ setBrick(width, img, crossOrigin) { width = Number(width) || 30; if (typeof (img) === 'string') { var i = new Image; i.src = this.img[img] || this.img.ol3; i.crossOrigin = crossOrigin || null; img = i; } if (img) this.pattern.img = img; if (!this.pattern.img.width) { var self = this; this.pattern.img.onload = function () { self.setBrick(width, img); }; return; } this.pattern.canvas.width = this.pattern.canvas.height = width; this.pattern.ctx = this.pattern.canvas.getContext("2d"); this.pattern.ctx.fillStyle = this.pattern.ctx.createPattern(this.pattern.img, 'repeat'); this.set("brickSize", width); if (img) this.set("img", img.src); } /** Get translated pattern * @param {number} offsetX x offset * @param {number} offsetY y offset */ getPattern(offsetX, offsetY) { if (!this.pattern.ctx) return "transparent"; //return this.pattern.ctx.fillStyle var c = this.pattern.canvas; var ctx = this.pattern.ctx; var sc = c.width / this.pattern.img.width; ctx.save(); ctx.clearRect(0, 0, c.width, c.height); ctx.scale(sc, sc); offsetX /= sc; offsetY /= sc; ctx.translate(offsetX, offsetY); ctx.beginPath(); ctx.clearRect(-2 * c.width, -2 * c.height, 4 * c.width, 4 * c.height); ctx.rect(-offsetX, -offsetY, 2 * c.width / sc, 2 * c.height / sc); ctx.fill(); ctx.restore(); return ctx.createPattern(c, 'repeat'); } /** Postcompose operation */ postcompose(e) { // Set back color hue var ctx = e.context; var canvas = ctx.canvas; var ratio = e.frameState.pixelRatio; /* ol v6+ if (e.type === 'postrender') { ratio = 1; } */ ctx.save(); // resize var step = this.pattern.canvas.width * ratio; var p = e.frameState.extent; var res = e.frameState.viewState.resolution / ratio; var offset = [-Math.round((p[0] / res) % step), Math.round((p[1] / res) % step)]; var ctx2 = this.internal_.getContext("2d"); var w = this.internal_.width = canvas.width; var h = this.internal_.height = canvas.height; // No smoothing please ctx2.webkitImageSmoothingEnabled = ctx2.mozImageSmoothingEnabled = ctx2.msImageSmoothingEnabled = ctx2.imageSmoothingEnabled = false; var w2 = Math.floor((w - offset[0]) / step); var h2 = Math.floor((h - offset[1]) / step); ctx2.drawImage(canvas, offset[0], offset[1], w2 * step, h2 * step, 0, 0, w2, h2); // ctx.webkitImageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.msImageSmoothingEnabled = ctx.imageSmoothingEnabled = false; ctx.clearRect(0, 0, w, h); ctx.drawImage(this.internal_, 0, 0, w2, h2, offset[0], offset[1], w2 * step, h2 * step); /* for (var x=offset[0]; x=0 && y this.pixels.length) { while (this.pixels.length < nb) { this.pixels.push([Math.random(), Math.random(), Math.random() * 4 + 2]); } } return nb; } /** @private */ precompose( /* e */) { } /** @private */ postcompose(e) { // var ratio = e.frameState.pixelRatio; // Set back color hue var ctx = e.context; var canvas = ctx.canvas; var w = canvas.width; var h = canvas.height; // Grayscale image var img = document.createElement('canvas'); img.width = w; img.height = h; var ictx = img.getContext('2d'); ictx.filter = 'saturate(' + Math.round(2 * this.get('saturate') * 100) + '%)'; ictx.drawImage(canvas, 0, 0); ctx.save(); // Saturate and blur ctx.filter = 'blur(3px) saturate(' + (this.get('saturate') * 100) + '%)'; ctx.drawImage(canvas, 0, 0); // ctx.clearRect(0,0,w,h); // debug // Draw points ctx.filter = 'none'; ctx.opacity = .5; var max = this._getPixels(w * h / 50); for (var i = 0; i < max; i++) { var x = Math.floor(this.pixels[i][0] * w); var y = Math.floor(this.pixels[i][1] * h); ctx.fillStyle = ol.color.asString(ictx.getImageData(x, y, 1, 1).data); ctx.beginPath(); ctx.arc(x, y, this.pixels[i][2], 0, 2 * Math.PI); ctx.fill(); } ctx.restore(); } } /* 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). */ /** Add a canvas Context2D SVG filter to a layer * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {ol.ext.SVGFilter|Array} filters */ ol.filter.SVGFilter = class olfilterSVGFilter extends ol.filter.Base { constructor(filters) { super(); this._svg = {}; if (filters) { if (!(filters instanceof Array)) filters = [filters]; filters.forEach(function (f) { this.addSVGFilter(f); }.bind(this)); } } /** Add a new svg filter * @param {ol.ext.SVGFilter} filter */ addSVGFilter(filter) { var url = '#' + filter.getId(); this._svg[url] = 1; this.dispatchEvent({ type: 'propertychange', key: 'svg', oldValue: this._svg }); } /** Remove a svg filter * @param {ol.ext.SVGFilter} filter */ removeSVGFilter(filter) { var url = '#' + filter.getId(); delete this._svg[url]; this.dispatchEvent({ type: 'propertychange', key: 'svg', oldValue: this._svg }); } /** * @private */ precompose() { } /** * @private */ postcompose(e) { var filter = []; // Set filters for (var f in this._svg) { filter.push('url(' + f + ')'); } filter = filter.join(' '); var canvas = document.createElement('canvas'); canvas.width = e.context.canvas.width; canvas.height = e.context.canvas.height; canvas.getContext('2d').drawImage(e.context.canvas, 0, 0); // Apply filter if (filter) { e.context.save(); e.context.clearRect(0, 0, canvas.width, canvas.height); e.context.filter = filter; e.context.drawImage(canvas, 0, 0); e.context.restore(); } } } /* 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). */ /** @typedef {Object} FilterTextureOptions * @property {Image | undefined} img Image object for the texture * @property {string} src Image source URI * @property {number} scale scale to draw the image. Default 1. * @property {number} [opacity] * @property {boolean} rotate Whether to rotate the texture with the view (may cause animation lags on mobile or slow devices). Default is true. * @property {null | string | undefined} crossOrigin The crossOrigin attribute for loaded images. */ /** Add texture effects on maps or layers * @constructor * @requires ol.filter * @extends {ol.filter.Base} * @param {FilterTextureOptions} options */ ol.filter.Texture = class olfilterTexture extends ol.filter.Base { constructor(options) { super(options); this.setFilter(options); } /** Set texture * @param {FilterTextureOptions} [options] */ setFilter(options) { var img; options = options || {}; if (options.img) { img = options.img; } else { img = new Image(); if (options.src) { // Look for a texture stored in ol.filter.Texture.Image if (ol.filter.Texture.Image && ol.filter.Texture.Image[options.src]) { img.src = ol.filter.Texture.Image[options.src]; } else { // default source if (!img.src) img.src = options.src; } } img.crossOrigin = options.crossOrigin || null; } this.set('rotateWithView', options.rotateWithView !== false); this.set('opacity', typeof (options.opacity) == 'number' ? options.opacity : 1); this.set('ready', false); var self = this; function setPattern(img) { self.pattern = {}; self.pattern.scale = options.scale || 1; self.pattern.canvas = document.createElement('canvas'); self.pattern.canvas.width = img.width * self.pattern.scale; self.pattern.canvas.height = img.height * self.pattern.scale; self.pattern.canvas.width = img.width; // * self.pattern.scale; self.pattern.canvas.height = img.height; // * self.pattern.scale; self.pattern.ctx = self.pattern.canvas.getContext("2d"); self.pattern.ctx.fillStyle = self.pattern.ctx.createPattern(img, 'repeat'); // Force refresh self.set('ready', true); } if (img.width) { setPattern(img); } else { img.onload = function () { setPattern(img); }; } } /** Get translated pattern * @param {number} offsetX x offset * @param {number} offsetY y offset */ getPattern(offsetX, offsetY) { var c = this.pattern.canvas; var ctx = this.pattern.ctx; ctx.save(); /* offsetX /= this.pattern.scale; offsetY /= this.pattern.scale; ctx.scale(this.pattern.scale,this.pattern.scale); */ ctx.translate(-offsetX, offsetY); ctx.beginPath(); ctx.rect(offsetX, -offsetY, c.width, c.height); ctx.fill(); ctx.restore(); return ctx.createPattern(c, 'repeat'); } /** Draw pattern over the map on postcompose */ postcompose(e) { if (!this.pattern) return; // Set back color hue var ctx = e.context; var canvas = ctx.canvas; var m = 1.5 * Math.max(canvas.width, canvas.height); var mt = e.frameState.pixelToCoordinateTransform; // Old version (matrix) if (!mt) { mt = e.frameState.pixelToCoordinateMatrix, mt[2] = mt[4]; mt[3] = mt[5]; mt[4] = mt[12]; mt[5] = mt[13]; } var ratio = e.frameState.pixelRatio; var res = e.frameState.viewState.resolution; var w = canvas.width / 2, h = canvas.height / 2; ctx.save(); ctx.globalCompositeOperation = "multiply"; //ctx.globalCompositeOperation = "overlay"; //ctx.globalCompositeOperation = "color"; ctx.globalAlpha = this.get('opacity'); ctx.scale(ratio * this.pattern.scale, ratio * this.pattern.scale); if (this.get('rotateWithView')) { // Translate pattern res *= this.pattern.scale; ctx.fillStyle = this.getPattern((w * mt[0] + h * mt[1] + mt[4]) / res, (w * mt[2] + h * mt[3] + mt[5]) / res); // Rotate on canvas center and fill ctx.translate(w / this.pattern.scale, h / this.pattern.scale); ctx.rotate(e.frameState.viewState.rotation); ctx.beginPath(); ctx.rect(-w - m, -h - m, 2 * m, 2 * m); ctx.fill(); } else { /**/ var dx = -(w * mt[0] + h * mt[1] + mt[4]) / res; var dy = (w * mt[2] + h * mt[3] + mt[5]) / res; var cos = Math.cos(e.frameState.viewState.rotation); var sin = Math.sin(e.frameState.viewState.rotation); var offsetX = (dx * cos - dy * sin) / this.pattern.scale; var offsetY = (dx * sin + dy * cos) / this.pattern.scale; ctx.translate(offsetX, offsetY); ctx.beginPath(); ctx.fillStyle = this.pattern.ctx.fillStyle; ctx.rect(-offsetX - m, -offsetY - m, 2 * m, 2 * m); ctx.fill(); /* //old version without centered rotation var offsetX = -(e.frameState.extent[0]/res) % this.pattern.canvas.width; var offsetY = (e.frameState.extent[1]/res) % this.pattern.canvas.height; ctx.rotate(e.frameState.viewState.rotation); ctx.translate(offsetX, offsetY); ctx.beginPath(); ctx.fillStyle = this.pattern.ctx.fillStyle ctx.rect(-offsetX -m , -offsetY -m, 2*m, 2*m); ctx.fill(); */ } ctx.restore(); } } /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ /** Feature format for reading and writing data in the GeoJSONX format. * @constructor * @extends {ol.format.GeoJSON} * @param {*} options options. * @param {number} options.decimals number of decimals to save, default 7 for EPSG:4326, 2 for other projections * @param {boolean|Array<*>} options.deleteNullProperties An array of property values to remove, if false, keep all properties, default [null,undefined,""] * @param {boolean|Array<*>} options.extended Decode/encode extended GeoJSON with foreign members (id, bbox, title, etc.), default false * @param {Array|function} options.whiteList A list of properties to keep on features when encoding or a function that takes a property name and retrun true if the property is whitelisted * @param {Array|function} options.blackList A list of properties to remove from features when encoding or a function that takes a property name and retrun true if the property is blacklisted * @param {string} [options.layout='XY'] layout layout (XY or XYZ or XYZM) * @param {ol.ProjectionLike} options.dataProjection Projection of the data we are reading. If not provided `EPSG:4326` * @param {ol.ProjectionLike} options.featureProjection Projection of the feature geometries created by the format reader. If not provided, features will be returned in the dataProjection. */ ol.format.GeoJSONX = class olformatGeoJSONX extends ol.format.GeoJSON { constructor(options) { options = options || {}; super(options); this._hash = {}; this._count = 0; this._extended = options.extended; if (typeof (options.whiteList) === 'function') { this._whiteList = options.whiteList; } else if (options.whiteList && options.whiteList.indexOf) { this._whiteList = function (k) { return options.whiteList.indexOf(k) > -1; }; } else { this._whiteList = function () { return true; }; } if (typeof (options.blackList) === 'function') { this._blackList = options.blackList; } else if (options.blackList && options.blackList.indexOf) { this._blackList = function (k) { return options.blackList.indexOf(k) > -1; }; } else { this._blackList = function () { return false; }; } this._deleteNull = options.deleteNullProperties === false ? false : [null, undefined, ""]; var decimals = 2; if (!options.dataProjection || options.dataProjection === 'EPSG:4326') decimals = 7; if (!isNaN(parseInt(options.decimals))) decimals = parseInt(options.decimals); this._decimals = decimals; this.setLayout(options.layout || 'XY'); } /** Set geometry layout * @param {string} layout the geometry layout (XY or XYZ or XYZM) */ setLayout(layout) { switch (layout) { case 'XYZ': case 'XYZM': { this._layout = layout; break; } default: { this._layout = 'XY'; break; } } } /** Get geometry layout * @return {string} layout */ getLayout() { return this._layout; } /** Encode a number * @param {number} number Number to encode * @param {number} decimals Number of decimals * @param {string} */ encodeNumber(number, decimals) { if (isNaN(Number(number)) || number === null || !isFinite(number)) { number = 0; } if (!decimals && decimals !== 0) decimals = this._decimals; // Round number number = Math.round(number * Math.pow(10, decimals)); // Zigzag encoding (get positive number) if (number < 0) number = -2 * number - 1; else number = 2 * number; // Encode var result = ''; var modulo, residual = number; while (true) { modulo = residual % this._size; result = this._radix.charAt(modulo) + result; residual = Math.floor(residual / this._size); if (residual == 0) break; } return result; } /** Decode a number * @param {string} s * @param {number} decimals Number of decimals * @return {number} */ decodeNumber(s, decimals) { if (!decimals && decimals !== 0) decimals = this._decimals; var decode = 0; s.split('').forEach(function (c) { decode = (decode * this._size) + this._radix.indexOf(c); }.bind(this)); // Zigzag encoding var result = Math.floor(decode / 2); if (result !== decode / 2) result = -1 - result; return result / Math.pow(10, decimals); } /** Encode coordinates * @param {ol.coordinate|Array} v * @param {number} decimal * @return {string|Array} * @api */ encodeCoordinates(v, decimal) { var i, p, tp; if (typeof (v[0]) === 'number') { p = this.encodeNumber(v[0], decimal) + ',' + this.encodeNumber(v[1], decimal); if (this._layout[2] == 'Z' && v.length > 2) p += ',' + this.encodeNumber(v[2], 2); if (this._layout[3] == 'M' && v.length > 3) p += ',' + this.encodeNumber(v[3], 0); return p; } else if (v.length && v[0]) { if (typeof (v[0][0]) === 'number') { var dxy = [0, 0, 0, 0]; var xy = []; var hasZ = (this._layout[2] == 'Z' && v[0].length > 2); var hasM = (this._layout[3] == 'M' && v[0].length > 3); for (i = 0; i < v.length; i++) { tp = [ Math.round(v[i][0] * Math.pow(10, decimal)), Math.round(v[i][1] * Math.pow(10, decimal)) ]; if (hasZ) tp[2] = v[i][2]; if (hasM) tp[3] = v[i][3]; v[i] = tp; var dx = v[i][0] - dxy[0]; var dy = v[i][1] - dxy[1]; if (i == 0 || (dx !== 0 || dy !== 0)) { p = this.encodeNumber(dx, 0) + ',' + this.encodeNumber(dy, 0) + (hasZ ? ',' + this.encodeNumber(v[i][2] - dxy[2], 2) : '') + (hasM ? ',' + this.encodeNumber(v[i][3] - dxy[3], 0) : ''); xy.push(p); dxy = v[i]; } } // Almost 2 points... // if (xy.length<2) xy.push('A,A'); return xy.join(';'); } else { for (i = 0; i < v.length; i++) { v[i] = this.encodeCoordinates(v[i], decimal); } return v; } } else { return this.encodeCoordinates([0, 0], decimal); } } /** Decode coordinates * @param {string|Array} * @param {number} decimal Number of decimals * @return {ol.coordinate|Array} v * @api */ decodeCoordinates(v, decimals) { var i, p; if (typeof (v) === 'string') { v = v.split(';'); if (v.length > 1) { var pow = Math.pow(10, decimals); var dxy = [0, 0, 0, 0]; v.forEach(function (vi, i) { v[i] = vi.split(','); v[i][0] = Math.round((this.decodeNumber(v[i][0], decimals) + dxy[0]) * pow) / pow; v[i][1] = Math.round((this.decodeNumber(v[i][1], decimals) + dxy[1]) * pow) / pow; if (v[i].length > 2) v[i][2] = Math.round((this.decodeNumber(v[i][2], 2) + dxy[2]) * pow) / pow; if (v[i].length > 3) v[i][3] = Math.round((this.decodeNumber(v[i][3], 0) + dxy[3]) * pow) / pow; dxy = v[i]; }.bind(this)); return v; } else { v = v[0].split(','); p = [this.decodeNumber(v[0], decimals), this.decodeNumber(v[1], decimals)]; if (v.length > 2) p[2] = this.decodeNumber(v[2], 2); if (v.length > 3) p[3] = this.decodeNumber(v[3], 0); return p; } } else if (v.length) { var r = []; for (i = 0; i < v.length; i++) { r[i] = this.decodeCoordinates(v[i], decimals); } return r; } else { return [0, 0]; } } /** Encode an array of features as a GeoJSONX object. * @param {Array} features Features. * @param {*} options Write options. * @return {*} GeoJSONX Object. * @override * @api */ writeFeaturesObject(features, options) { options = options || {}; this._count = 0; this._hash = {}; var geojson = ol.format.GeoJSON.prototype.writeFeaturesObject.call(this, features, options); geojson.decimals = this._decimals; geojson.hashProperties = []; Object.keys(this._hash).forEach(function (k) { geojson.hashProperties.push(k); }.bind(this)); this._count = 0; this._hash = {}; // Push features at the end of the object var temp = geojson.features; delete geojson.features; geojson.features = temp; return geojson; } /** Encode a set of features as a GeoJSONX object. * @param {ol.Feature} feature Feature * @param {*} options Write options. * @return {*} GeoJSONX Object. * @override * @api */ writeFeatureObject(source, options) { var f0 = ol.format.GeoJSON.prototype.writeFeatureObject.call(this, source, options); // Only features supported yet if (f0.type !== 'Feature') throw 'GeoJSONX doesn\'t support ' + f0.type + '.'; var f = []; // Encode geometry if (f0.geometry.type === 'Point') { f.push(this.encodeCoordinates(f0.geometry.coordinates, this._decimals)); } else if (f0.geometry.type === 'MultiPoint') { var pts = []; f0.geometry.coordinates.forEach(function (p) { pts.push(this.encodeCoordinates(p, this._decimals)); }.bind(this)); f.push([ this._type[f0.geometry.type], pts.join(';') ]); } else { if (!this._type[f0.geometry.type]) { throw 'GeoJSONX doesn\'t support ' + f0.geometry.type + '.'; } f.push([ this._type[f0.geometry.type], this.encodeCoordinates(f0.geometry.coordinates, this._decimals) ]); } // Encode properties var k; var prop = []; for (k in f0.properties) { if (!this._whiteList(k) || this._blackList(k)) continue; if (!this._hash.hasOwnProperty(k)) { this._hash[k] = this._count; this._count++; } if (!this._deleteNull || this._deleteNull.indexOf(f0.properties[k]) < 0) { prop.push(this._hash[k], f0.properties[k]); } } // Create prop table if (prop.length || this._extended) { f.push(prop); } // Other properties (id, title, bbox, centerline... if (this._extended) { var found = false; prop = {}; for (k in f0) { if (!/^type$|^geometry$|^properties$/.test(k)) { prop[k] = f0[k]; found = true; } } if (found) f.push(prop); } return f; } /** Encode a geometry as a GeoJSONX object. * @param {ol.geom.Geometry} geometry Geometry. * @param {*} options Write options. * @return {*} Object. * @override * @api */ writeGeometryObject(source, options) { var g = ol.format.GeoJSON.prototype.writeGeometryObject.call(this, source, options); // Encode geometry if (g.type === 'Point') { return this.encodeCoordinates(g.coordinates, this._decimals); } else { return [ this._type[g.type], this.encodeCoordinates(g.coordinates, this._decimals) ]; } } /** Decode a GeoJSONX object. * @param {*} object GeoJSONX * @param {*} options Read options. * @return {Array} * @override * @api */ readFeaturesFromObject(object, options) { this._hashProperties = object.hashProperties || []; options = options || {}; options.decimals = parseInt(object.decimals); if (!options.decimals && options.decimals !== 0) throw 'Bad file format...'; var features = ol.format.GeoJSON.prototype.readFeaturesFromObject.call(this, object, options); return features; } /** Decode GeoJSONX Feature object. * @param {*} object GeoJSONX * @param {*} options Read options. * @return {ol.Feature} */ readFeatureFromObject(f0, options) { var f = { type: 'Feature' }; if (typeof (f0[0]) === 'string') { f.geometry = { type: 'Point', coordinates: this.decodeCoordinates(f0[0], typeof (options.decimals) === 'number' ? options.decimals : this.decimals) }; } else { f.geometry = { type: this._toType[f0[0][0]] }; if (f.geometry.type === 'MultiPoint') { var g = f.geometry.coordinates = []; var coords = f0[0][1].split(';'); coords.forEach(function (c) { c = c.split(','); g.push([this.decodeNumber(c[0], options.decimals), this.decodeNumber(c[1], options.decimals)]); }.bind(this)); } else { f.geometry.coordinates = this.decodeCoordinates(f0[0][1], typeof (options.decimals) === 'number' ? options.decimals : this.decimals); } } if (this._hashProperties && f0[1]) { f.properties = {}; var t = f0[1]; for (var i = 0; i < t.length; i += 2) { f.properties[this._hashProperties[t[i]]] = t[i + 1]; } } else { f.properties = f0[1]; } // Extended properties if (f0[2]) { for (var k in f0[2]) { f[k] = f0[2][k]; } } var feature = ol.format.GeoJSON.prototype.readFeatureFromObject.call(this, f, options); return feature; } } /** Radix */ ol.format.GeoJSONX.prototype._radix = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ !#$%&\'()*-.:<=>?@[]^_`{|}~'; /** Radix size */ ol.format.GeoJSONX.prototype._size = ol.format.GeoJSONX.prototype._radix.length; /** GeoSJON types */ ol.format.GeoJSONX.prototype._type = { "Point": 0, "LineString": 1, "Polygon": 2, "MultiPoint": 3, "MultiLineString": 4, "MultiPolygon": 5, "GeometryCollection": null // Not supported }; /** GeoSJONX types */ ol.format.GeoJSONX.prototype._toType = [ "Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon" ]; /** Feature format for reading and writing data in the GeoJSONP format, * using Polyline Algorithm to encode geometry. * @constructor * @extends {ol.format.GeoJSON} * @param {*} options options. * @param {number} options.decimals number of decimals to save, default 6 * @param {ol.ProjectionLike} options.dataProjection Projection of the data we are reading. If not provided `EPSG:4326` * @param {ol.ProjectionLike} options.featureProjection Projection of the feature geometries created by the format reader. If not provided, features will be returned in the dataProjection. */ ol.format.GeoJSONP = class olformatGeoJSONP extends ol.format.GeoJSONX { constructor(options) { options = options || {} super(options) this._lineFormat = new ol.format.Polyline({ factor: Math.pow(10, options.decimals || 6) }) } /** Encode coordinates * @param {ol.coordinate|Array} v * @return {string|Array} * @api */ encodeCoordinates(v) { var g if (typeof (v[0]) === 'number') { g = new ol.geom.Point(v) return this._lineFormat.writeGeometry(g) } else if (v.length && v[0]) { var tab = (typeof (v[0][0]) === 'number') if (tab) { g = new ol.geom.LineString(v) return this._lineFormat.writeGeometry(g) } else { var r = [] for (var i = 0; i < v.length; i++) { r[i] = this.encodeCoordinates(v[i]) } return r } } else { return this.encodeCoordinates([0, 0]) } } /** Decode coordinates * @param {string|Array} * @return {ol.coordinate|Array} v * @api */ decodeCoordinates(v) { var i, g if (typeof (v) === 'string') { g = this._lineFormat.readGeometry(v) return g.getCoordinates()[0] } else if (v.length) { var tab = (typeof (v[0]) === 'string') var r = [] if (tab) { for (i = 0; i < v.length; i++) { g = this._lineFormat.readGeometry(v[i]) r[i] = g.getCoordinates() } } else { for (i = 0; i < v.length; i++) { r[i] = this.decodeCoordinates(v[i]) } } return r } else { return null } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Feature format for reading data in the GeoRSS format. * @constructor ol.fromat.GeoRSS * @extends {ol.Object} * @param {*} options options. * @param {ol.ProjectionLike} options.dataProjection Projection of the data we are reading. If not provided `EPSG:4326` * @param {ol.ProjectionLike} options.featureProjection Projection of the feature geometries created by the format reader. If not provided, features will be returned in the dataProjection. */ ol.format.GeoRSS = class olformatGeoRSS extends ol.Object { constructor(options) { options = options || {} super(options) } /** * Read a feature. Only works for a single feature. Use `readFeatures` to * read a feature collection. * * @param {Node|string} source Source. * @param {*} options Read options. * @param {ol.ProjectionLike} options.dataProjection Projection of the data we are reading. If not provided `EPSG:4326` * @param {ol.ProjectionLike} options.featureProjection Projection of the feature geometries created by the format reader. If not provided, features will be returned in the dataProjection. * @return {ol.Feature} Feature or null if no feature read * @api */ readFeature(source, options) { options = options || {} var att, atts = source.children var f = new ol.Feature() // Get attributes for (var j = 0; att = atts[j]; j++) { f.set(att.tagName, att.innerHTML) } var temp, g, coord = [] // Get geometry if (f.get('geo:long')) { // LonLat g = new ol.geom.Point([parseFloat(f.get('geo:long')), parseFloat(f.get('geo:lat'))]) f.unset('geo:long') f.unset('geo:lat') } else if (f.get('georss:point')) { // Point coord = f.get('georss:point').trim().split(' ') g = new ol.geom.Point([parseFloat(coord[1]), parseFloat(coord[0])]) f.unset('georss:point') } else if (f.get('georss:polygon')) { // Polygon temp = f.get('georss:polygon').trim().split(' ') for (var i = 0; i < temp.length; i += 2) { coord.push([parseFloat(temp[i + 1]), parseFloat(temp[i])]) } g = new ol.geom.Polygon([coord]) f.unset('georss:polygon') } else if (f.get('georss:where')) { // GML console.warn('[GeoRSS] GML format not implemented') f.unset('georss:where') return null } else { console.warn('[GeoRSS] unknown geometry') return null } if (options.featureProjection || this.get('featureProjection')) { g.transform(options.dataProjection || this.get('dataProjection') || 'EPSG:4326', options.featureProjection || this.get('featureProjection')) } f.setGeometry(g) return f } /** * Read all features. Works with both a single feature and a feature * collection. * * @param {Document|Node|string} source Source. * @param {*} options Read options. * @param {ol.ProjectionLike} options.dataProjection Projection of the data we are reading. If not provided `EPSG:4326` * @param {ol.ProjectionLike} options.featureProjection Projection of the feature geometries created by the format reader. If not provided, features will be returned in the dataProjection. * @return {Array} Features. * @api */ readFeatures(source, options) { var items if (typeof (source) === 'string') { var parser = new DOMParser() var xmlDoc = parser.parseFromString(source, "text/xml") items = xmlDoc.getElementsByTagName(this.getDocumentItemsTagName(xmlDoc)) } else if (source instanceof Document) { items = source.getElementsByTagName(this.getDocumentItemsTagName(source)) } else if (source instanceof Node) { items = source } else { return [] } var features = [] for (var i = 0, item; item = items[i]; i++) { var f = this.readFeature(item, options) if (f) features.push(f) } return features } /** * Get the tag name for the items in the XML Document depending if we are * dealing with an atom base document or not. * @param {Document} xmlDoc document to extract the tag name for the items * @return {string} tag name * @private */ getDocumentItemsTagName(xmlDoc) { switch (xmlDoc.documentElement.tagName) { case 'feed': return 'entry' default: return 'item' } } } /** Clip interaction to clip layers in a circle * @constructor * @extends {ol.interaction.Pointer} * @param {ol.interaction.Clip.options} options flashlight param * @param {number} options.radius radius of the clip, default 100 * @param {ol.layer|Array} options.layers layers to clip */ ol.interaction.Clip = class olinteractionClip extends ol.interaction.Pointer { constructor(options) { super({ handleDownEvent: function(e) { return this._setPosition(e) }, handleMoveEvent: function(e) { return this._setPosition(e) }, }); this.layers_ = []; this.precomposeBind_ = this.precompose_.bind(this); this.postcomposeBind_ = this.postcompose_.bind(this); // Default options options = options || {}; this.pos = false; this.radius = (options.radius || 100); if (options.layers) this.addLayer(options.layers); this.setActive(true); } /** Set the map > start postcompose */ setMap(map) { var i; if (this.getMap()) { for (i = 0; i < this.layers_.length; i++) { this.layers_[i].un(['precompose', 'prerender'], this.precomposeBind_); this.layers_[i].un(['postcompose', 'postrender'], this.postcomposeBind_); } try { this.getMap().renderSync(); } catch (e) { /* ok */ } } super.setMap(map); if (map) { for (i = 0; i < this.layers_.length; i++) { this.layers_[i].on(['precompose', 'prerender'], this.precomposeBind_); this.layers_[i].on(['postcompose', 'postrender'], this.postcomposeBind_); } try { map.renderSync(); } catch (e) { /* ok */ } } } /** Set clip radius * @param {integer} radius */ setRadius(radius) { this.radius = radius; if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Get clip radius * @returns {integer} radius */ getRadius() { return this.radius; } /** Add a layer to clip * @param {ol.layer|Array} layer to clip */ addLayer(layers) { if (!(layers instanceof Array)) layers = [layers]; for (var i = 0; i < layers.length; i++) { if (this.getMap()) { layers[i].on(['precompose', 'prerender'], this.precomposeBind_); layers[i].on(['postcompose', 'postrender'], this.postcomposeBind_); try { this.getMap().renderSync(); } catch (e) { /* ok */ } } this.layers_.push(layers[i]); } } /** Remove all layers */ removeLayers() { this.removeLayer(this.layers_); } /** Remove a layer to clip * @param {ol.layer|Array} layer to clip */ removeLayer(layers) { if (!(layers instanceof Array)) layers = [layers]; for (var i = 0; i < layers.length; i++) { var k; for (k = 0; k < this.layers_.length; k++) { if (this.layers_[k] === layers[i]) { break; } } if (k != this.layers_.length && this.getMap()) { this.layers_[k].un(['precompose', 'prerender'], this.precomposeBind_); this.layers_[k].un(['postcompose', 'postrender'], this.postcomposeBind_); this.layers_.splice(k, 1); } } if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Set position of the clip * @param {ol.coordinate} coord */ setPosition(coord) { if (this.getMap()) { this.pos = this.getMap().getPixelFromCoordinate(coord); try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Get position of the clip * @returns {ol.coordinate} */ getPosition() { if (this.pos) return this.getMap().getCoordinateFromPixel(this.pos); return null; } /** Set position of the clip * @param {ol.Pixel} pixel */ setPixelPosition(pixel) { this.pos = pixel; if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Get position of the clip * @returns {ol.Pixel} pixel */ getPixelPosition() { return this.pos; } /** Set position of the clip * @param {ol.MapBrowserEvent} e * @privata */ _setPosition(e) { if (e.type === 'pointermove' && this.get('action') === 'onclick'){ return; } if (e.pixel) { this.pos = e.pixel; } if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /* @private */ precompose_(e) { if (!this.getActive()) return; var ctx = e.context; var ratio = e.frameState.pixelRatio; ctx.save(); ctx.beginPath(); var pt = [this.pos[0], this.pos[1]]; var radius = this.radius; var tr = e.inversePixelTransform; if (tr) { // Transform pt pt = [ (pt[0] * tr[0] - pt[1] * tr[1] + tr[4]), (-pt[0] * tr[2] + pt[1] * tr[3] + tr[5]) ]; // Get radius / transform radius = pt[0] - ((this.pos[0] - radius) * tr[0] - this.pos[1] * tr[1] + tr[4]); } else { pt[0] *= ratio; pt[1] *= ratio; radius *= ratio; } ctx.arc(pt[0], pt[1], radius, 0, 2 * Math.PI); ctx.clip(); } /* @private */ postcompose_(e) { if (!this.getActive()) return; e.context.restore(); } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @observable * @api */ setActive(b) { if (b === this.getActive()) { return; } super.setActive(b); if (!this.layers_) return; var i; if (b) { for (i = 0; i < this.layers_.length; i++) { this.layers_[i].on(['precompose', 'prerender'], this.precomposeBind_); this.layers_[i].on(['postcompose', 'postrender'], this.postcomposeBind_); } } else { for (i = 0; i < this.layers_.length; i++) { this.layers_[i].un(['precompose', 'prerender'], this.precomposeBind_); this.layers_[i].un(['postcompose', 'postrender'], this.postcomposeBind_); } } if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } } /** An interaction to check the current map and add key events listeners. * It will fire a 'focus' event on the map when map is focused (use mapCondition option to handle the condition when the map is focused). * @constructor * @fires focus * @param {*} options * @param {function} condition a function that takes a mapBrowserEvent and returns true if the map must be activated, default always true * @param {function} onKeyDown a function that takes a keydown event is fired on the active map * @param {function} onKeyPress a function that takes a keypress event is fired on the active map * @param {function} onKeyUp a function that takes a keyup event is fired on the active map * @extends {ol.interaction.Interaction} */ ol.interaction.CurrentMap = class olinteractionCurrentMap extends ol.interaction.Interaction { constructor(options) { options = options || {}; var condition = options.condition || function () { return true; }; // Check events on the map super({ handleEvent: function (e) { if (condition(e)) { if (!this.isCurrentMap()) { this.setCurrentMap(this.getMap()); this.dispatchEvent({ type: 'focus', map: this.getMap() }); this.getMap().dispatchEvent({ type: 'focus', map: this.getMap() }); } } return true; } }); // Add a key listener if (options.onKeyDown) { document.addEventListener('keydown', function (e) { if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) { options.onKeyDown({ type: e.type, map: this.getMap(), originalEvent: e }); } }.bind(this)); } if (options.onKeyPress) { document.addEventListener('keydown', function (e) { if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) { options.onKeyPress({ type: e.type, map: this.getMap(), originalEvent: e }); } }.bind(this)); } if (options.onKeyUp) { document.addEventListener('keydown', function (e) { if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) { options.onKeyUp({ type: e.type, map: this.getMap(), originalEvent: e }); } }.bind(this)); } } /** Check if is the current map * @return {boolean} */ isCurrentMap() { return this.getMap() === ol.interaction.CurrentMap.prototype._currentMap; } /** Get the current map * @return {ol.Map} */ getCurrentMap() { return ol.interaction.CurrentMap.prototype._currentMap; } /** Set the current map * @param {ol.Map} map */ setCurrentMap(map) { ol.interaction.CurrentMap.prototype._currentMap = map; } } /** The current map */ ol.interaction.CurrentMap.prototype._currentMap = undefined; /** Blob interaction to clip layers in a blob * @constructor * @extends {ol.interaction.Clip} * @param {*} options blob options * @param {number} options.radius radius of the clip, default 100 * @param {ol.layer|Array} options.layers layers to clip * @param {number} [options.stiffness=20] spring stiffness coef, default 20 * @param {number} [options.damping=7] spring damping coef * @param {number} [options.mass=1] blob mass * @param {number} [options.points=10] number of points for the blob polygon * @param {number} [options.tension=.5] blob polygon spline tension * @param {number} [options.fuss] bob fussing factor * @param {number} [options.amplitude=1] blob deformation amplitude factor */ ol.interaction.Blob = class olinteractionBlob extends ol.interaction.Clip { constructor(options) { super(options); } /** Animate the blob * @private */ precompose_(e) { if (!this.getActive()) return; var ctx = e.context; var ratio = e.frameState.pixelRatio; ctx.save(); if (!this.pos) { ctx.beginPath(); ctx.moveTo(0, 0); ctx.clip(); return; } var pt = [this.pos[0], this.pos[1]]; var tr = e.inversePixelTransform; if (tr) { pt = [ (pt[0] * tr[0] - pt[1] * tr[1] + tr[4]), (-pt[0] * tr[2] + pt[1] * tr[3] + tr[5]) ]; } else { pt[0] *= ratio; pt[1] *= ratio; } // Time laps if (!this.frame) this.frame = e.frameState.time; var dt = e.frameState.time - this.frame; this.frame = e.frameState.time; // Blob position pt = this._getCenter(pt, dt); // Blob geom var blob = this._calculate(dt); // Draw var p = blob[0]; ctx.beginPath(); ctx.moveTo(pt[0] + p[0], pt[1] + p[1]); for (var i = 1; p = blob[i]; i++) { ctx.lineTo(pt[0] + p[0], pt[1] + p[1]); } ctx.clip(); e.frameState.animate = true; } /** Get blob center with kinetic * @param {number} dt0 time laps * @private */ _getCenter(pt, dt0) { if (!this._center) { this._center = pt; this._velocity = [0, 0]; } else { var k = this.get('stiffness') || 20; // stiffness var d = -1 * (this.get('damping') || 7); // damping var mass = Math.max(this.get('mass') || 1, .1); var dt = Math.min(dt0 / 1000, 1 / 30); var fSpring = [ k * (pt[0] - this._center[0]), k * (pt[1] - this._center[1]) ]; var fDamping = [ d * this._velocity[0], d * this._velocity[1] ]; var accel = [ (fSpring[0] + fDamping[0]) / mass, (fSpring[1] + fDamping[1]) / mass ]; this._velocity[0] += accel[0] * dt; this._velocity[1] += accel[1] * dt; this._center[0] += this._velocity[0] * dt; this._center[1] += this._velocity[1] * dt; } return this._center; } /** Calculate the blob geom * @param {number} dt time laps * @returns {Array} * @private */ _calculate(dt) { var i, nb = this.get('points') || 10; if (!this._waves || this._waves.length !== nb) { this._waves = []; for (i = 0; i < nb; i++) { this._waves.push({ angle: Math.random() * Math.PI, noise: Math.random() }); } } var blob = []; var speed = (this._velocity[0] * this._velocity[0] + this._velocity[1] * this._velocity[1]) / 500; this._rotation = (this._rotation || 0) + (this._velocity[0] > 0 ? 1 : -1) * Math.min(.015, speed / 70000 * dt); for (i = 0; i < nb; i++) { var angle = i * 2 * Math.PI / nb + this._rotation; var radius = this.radius + Math.min(this.radius, speed); var delta = Math.cos(this._waves[i].angle) * radius / 4 * this._waves[i].noise * (this.get('amplitude') || 1); blob.push([ (this.radius + delta) * Math.cos(angle), (this.radius + delta) * Math.sin(angle) ]); // Add noise this._waves[i].angle += (Math.PI + Math.random() + speed / 200) / 350 * dt * (this.get('fuss') || 1); this._waves[i].noise = Math.min(1, Math.max(0, this._waves[i].noise + (Math.random() - .5) * .1 * (this.get('fuss') || 1))); } blob.push(blob[0]); return ol.coordinate.cspline(blob, { tension: this.get('tension') }); } } /* 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). */ /** Handles coordinates on the center of the viewport. * It can be used as abstract base class used for creating subclasses. * The CenterTouch interaction modifies map browser event coordinate and pixel properties to force pointer on the viewport center to any interaction that them. * Only pointermove pointerup are concerned with it. * @constructor * @extends {ol.interaction.Interaction} * @param {olx.interaction.InteractionOptions} options Options * @param {ol.style.Style|Array} options.targetStyle a style to draw the target point, default cross style * @param {string} options.composite composite operation for the target : difference|multiply|xor|screen|overlay|darken|lighter|lighten|... */ ol.interaction.CenterTouch = class olinteractionCenterTouch extends ol.interaction.Interaction { constructor(options) { options = options || {}; super({ handleEvent: function (e) { if (rex.test(e.type)) this.pos_ = e.coordinate; if (options.handleEvent) return options.handleEvent.call(this, e); return true; } }); // List of listerner on the object this._listener = {}; // Filter event var rex = /^pointermove$|^pointerup$/; // Interaction to defer center on top of the interaction // this is done to enable other coordinates manipulation inserted after the interaction (snapping) this.ctouch = new ol.interaction.Interaction({ handleEvent: function (e) { if (rex.test(e.type) && this.getMap()) { e.coordinate = this.getMap().getView().getCenter(); e.pixel = this.getMap().getSize(); e.pixel = [e.pixel[0] / 2, e.pixel[1] / 2]; } return true; } }); // Target on map center this._target = new ol.control.Target({ style: options.targetStyle, composite: options.composite }); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {_ol_Map_} map Map. * @api stable */ setMap(map) { if (this.getMap()) { this.getMap().removeInteraction(this.ctouch); this.getMap().removeInteraction(this._target); } super.setMap(map); if (this.getMap()) { if (this.getActive()) { this.getMap().addInteraction(this.ctouch); this.getMap().addControl(this._target); } } } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @observable * @api */ setActive(b) { super.setActive(b); this.pos_ = null; if (this.getMap()) { if (this.getActive()) { this.getMap().addInteraction(this.ctouch); this.getMap().addControl(this._target); } else { this.getMap().removeInteraction(this.ctouch); this.getMap().removeControl(this._target); } } } /** Get the position of the target * @return {ol.coordinate} */ getPosition() { if (!this.pos_) { var px = this.getMap().getSize(); px = [px[0] / 2, px[1] / 2]; this.pos_ = this.getMap().getCoordinateFromPixel(px); } return this.pos_; } } /** Clip interaction to clip layers in a circle * @constructor * @extends {ol.interaction.Pointer} * @param {ol.interaction.ClipMap.options} options flashlight param * @param {number} options.radius radius of the clip, default 100 (px) */ ol.interaction.ClipMap = class olinteractionClipMap extends ol.interaction.Pointer { constructor(options) { super({ handleDownEvent: function(e) { return this._clip(e) }, handleMoveEvent: function(e) { return this._clip(e) }, }); // Default options options = options || {}; this.layers_ = []; this.pos = false; this.radius = (options.radius || 100); this.pos = [-1000, -1000]; } /** Set the map > start postcompose */ setMap(map) { if (this.getMap()) { if (this._listener) ol.Observable.unByKey(this._listener); var layerDiv = this.getMap().getViewport().querySelector('.ol-layers'); layerDiv.style.clipPath = ''; } super.setMap(map); if (map) { this._listener = map.on('change:size', this._clip.bind(this)); } } /** Set clip radius * @param {integer} radius */ setRadius(radius) { this.radius = radius; this._clip(); } /** Get clip radius * @returns {integer} radius */ getRadius() { return this.radius; } /** Set position of the clip * @param {ol.coordinate} coord */ setPosition(coord) { if (this.getMap()) { this.pos = this.getMap().getPixelFromCoordinate(coord); this._clip(); } } /** Get position of the clip * @returns {ol.coordinate} */ getPosition() { if (this.pos) return this.getMap().getCoordinateFromPixel(this.pos); return null; } /** Set position of the clip * @param {ol.Pixel} pixel */ setPixelPosition(pixel) { this.pos = pixel; this._clip(); } /** Get position of the clip * @returns {ol.Pixel} pixel */ getPixelPosition() { return this.pos; } /** Set position of the clip * @param {ol.MapBrowserEvent} e * @privata */ _setPosition(e) { if (e.type === 'pointermove' && this.get('action') === 'onclick') return; if (e.pixel) { this.pos = e.pixel; } if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** Clip * @private */ _clip(e) { if (e && e.pixel) { this.pos = e.pixel; } if (this.pos && this.getMap()) { var layerDiv = this.getMap().getViewport().querySelector('.ol-layers'); layerDiv.style.clipPath = 'circle(' + this.getRadius() + 'px' // radius + ' at ' + this.pos[0] + 'px ' + this.pos[1] + 'px)'; } } } /** An interaction to copy/paste features on a map. * It will fire a 'focus' event on the map when map is focused (use mapCondition option to handle the condition when the map is focused). * @constructor * @fires focus * @fires copy * @fires paste * @extends {ol.interaction.Interaction} * @param {Object} options Options * @param {function} options.condition a function that takes a mapBrowserEvent and return the action to perform: 'copy', 'cut' or 'paste', default Ctrl+C / Ctrl+V * @param {function} options.mapCondition a function that takes a mapBrowserEvent and return true if the map is the active map, default always returns true * @param {ol.Collection} options.features list of features to copy * @param {ol.source.Vector | Array} options.sources the source to copy from (used for cut), if not defined, it will use the destination * @param {ol.source.Vector} options.destination the source to copy to */ ol.interaction.CopyPaste = class olinteractionCopyPaste extends ol.interaction.CurrentMap { constructor(options) { options = options || {}; var condition = options.condition; if (typeof (condition) !== 'function') { condition = function (e) { if (e.originalEvent.ctrlKey) { if (/^c$/i.test(e.originalEvent.key)) return 'copy'; if (/^x$/i.test(e.originalEvent.key)) return 'cut'; if (/^v$/i.test(e.originalEvent.key)) return 'paste'; } return false; }; } // Create interaction super({ condition: options.mapCondition, onKeyDown: function (e) { switch (condition(e)) { case 'copy': { self.copy({ silent: false }); break; } case 'cut': { self.copy({ cut: true, silent: false }); break; } case 'paste': { self.paste({ silent: false }); break; } default: break; } } }); var self = this; // Features to copy this.features = []; this._cloneFeature = true; this._featuresSource = options.features || new ol.Collection(); this.setSources(options.sources); this.setDestination(options.destination); } /** Sources to cut feature from * @param { ol.source.Vector | Array } sources */ setSources(sources) { if (sources) { this._source = []; this._source = sources instanceof Array ? sources : [sources]; } else { this._source = null; } } /** Get sources to cut feature from * @return { Array } */ getSources() { return this._source; } /** Source to paste features * @param { ol.source.Vector } source */ setDestination(destination) { this._destination = destination; } /** Get source to paste features * @param { ol.source.Vector } */ getDestination() { return this._destination; } /** Get current feature to copy * @return {Array} */ getFeatures() { return this.features; } /** Set current feature to copy * @param {Object} options * @param {Array | ol.Collection} options.features feature to copy, default get in the provided collection * @param {boolean} options.cut try to cut feature from the sources, default false * @param {boolean} options.silent true to send an event, default true */ copy(options) { options = options || {}; var features = options.features || this._featuresSource.getArray(); // Try to remove feature from sources if (options.cut) { var sources = this._source || [this._destination]; // Remove feature from sources features.forEach(function (f) { sources.forEach(function (source) { try { source.removeFeature(f); } catch (e) { /*ok*/ } }); }); } if (this._cloneFeature) { this.features = []; features.forEach(function (f) { this.features.push(f.clone()); }.bind(this)); } else { this.features = features; } // Send an event if (options.silent === false) this.dispatchEvent({ type: options.cut ? 'cut' : 'copy', time: (new Date).getTime() }); } /** Paste features * @param {Object} options * @param {Array | ol.Collection} features feature to copy, default get current features * @param {ol.source.Vector} options.destination Source to paste to, default the current source * @param {boolean} options.silent true to send an event, default true */ paste(options) { options = options || {}; var features = options.features || this.features; if (features) { var destination = options.destination || this._destination; if (destination) { destination.addFeatures(this.features); if (this._cloneFeature) this.copy({ features: this.features }); } } // Send an event if (options.silent === false) this.dispatchEvent({ type: 'paste', features: features, time: (new Date).getTime() }); } } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A Select interaction to delete features on click. * @constructor * @extends {ol.interaction.Interaction} * @fires deletestart * @fires deleteend * @param {Object} options ol.interaction.Select options */ ol.interaction.Delete = class olinteractionDelete extends ol.interaction.Select { constructor(options) { super(options); this.on('select', function (e) { this.getFeatures().clear(); this.delete(e.selected); }.bind(this)); } /** Get vector source of the map * @return {Array} */ _getSources(layers) { if (!this.getMap()) return []; if (!layers) layers = this.getMap().getLayers(); var sources = []; layers.forEach(function (l) { // LayerGroup if (l.getLayers) { sources = sources.concat(this._getSources(l.getLayers())); } else { if (l.getSource && l.getSource() instanceof ol.source.Vector) { sources.push(l.getSource()); } } }.bind(this)); return sources; } /** Delete features: remove the features from the map (from all layers in the map) * @param {ol.Collection|Array} features The features to delete * @api */ delete(features) { if (features && (features.length || features.getLength())) { this.dispatchEvent({ type: 'deletestart', features: features }); var delFeatures = []; // Get the sources concerned this._getSources().forEach(function (source) { try { // Try to delete features in the source features.forEach(function (f) { source.removeFeature(f); delFeatures.push(f); }); } catch (e) { /* ok */ } }); this.dispatchEvent({ type: 'deleteend', features: delFeatures }); } } } /** Drag an overlay on the map * @constructor * @extends {ol.interaction.Pointer} * @fires dragstart * @fires dragging * @fires dragend * @param {any} options * @param {ol.Overlay|Array} options.overlays the overlays to drag * @param {ol.Size} options.offset overlay offset, default [0,0] */ ol.interaction.DragOverlay = class olinteractionDragOverlay extends ol.interaction.Pointer { constructor(options) { options = options || {}; var offset = options.offset || [0, 0]; // Extend pointer super({ // start draging on an overlay handleDownEvent: function (evt) { var res = evt.frameState.viewState.resolution; var coordinate = [evt.coordinate[0] + offset[0] * res, evt.coordinate[1] - offset[1] * res]; // Click on a button (closeBox) or on a link: don't drag! if (/^(BUTTON|A)$/.test(evt.originalEvent.target.tagName)) { this._dragging = false; return true; } // Start dragging if (this._dragging) { if (options.centerOnClick !== false) { this._dragging.setPosition(coordinate, true); } else { coordinate = this._dragging.getPosition(); } this.dispatchEvent({ type: 'dragstart', overlay: this._dragging, originalEvent: evt.originalEvent, frameState: evt.frameState, coordinate: coordinate }); return true; } return false; }, // Drag handleDragEvent: function (evt) { var res = evt.frameState.viewState.resolution; var coordinate = [evt.coordinate[0] + offset[0] * res, evt.coordinate[1] - offset[1] * res]; if (this._dragging) { this._dragging.setPosition(coordinate, true); this.dispatchEvent({ type: 'dragging', overlay: this._dragging, originalEvent: evt.originalEvent, frameState: evt.frameState, coordinate: coordinate }); } }, // Stop dragging handleUpEvent: function (evt) { var res = evt.frameState.viewState.resolution; var coordinate = [evt.coordinate[0] + offset[0] * res, evt.coordinate[1] - offset[1] * res]; if (this._dragging) { this.dispatchEvent({ type: 'dragend', overlay: this._dragging, originalEvent: evt.originalEvent, frameState: evt.frameState, coordinate: coordinate }); this._dragging = false; return true; } return false; } }); // List of overlays / listeners this._overlays = []; if (!(options.overlays instanceof Array)) options.overlays = [options.overlays]; options.overlays.forEach(this.addOverlay.bind(this)); } /** Add an overlay to the interacton * @param {ol.Overlay} ov */ addOverlay(ov) { for (var i = 0, o; o = this._overlays[i]; i++) { if (o === ov) return; } // Stop event overlay if (ov.element.parentElement && ov.element.parentElement.classList.contains('ol-overlaycontainer-stopevent')) { console.warn('[DragOverlay.addOverlay] overlay must be created with stopEvent set to false!'); return; } // Add listener on overlay of the same map var handler = function () { if (this.getMap() === ov.getMap()) this._dragging = ov; }.bind(this); this._overlays.push({ overlay: ov, listener: handler }); ov.element.addEventListener('pointerdown', handler); } /** Remove an overlay from the interacton * @param {ol.Overlay} ov */ removeOverlay(ov) { for (var i = 0, o; o = this._overlays[i]; i++) { if (o.overlay === ov) { var l = this._overlays.splice(i, 1)[0]; ov.element.removeEventListener('pointerdown', l.listener); break; } } } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Interaction to draw holes in a polygon. * It fires a drawstart, drawend event when drawing the hole * and a modifystart, modifyend event before and after inserting the hole in the feature geometry. * @constructor * @extends {ol.interaction.Interaction} * @fires drawstart * @fires drawend * @fires modifystart * @fires modifyend * @param {olx.interaction.DrawHoleOptions} options extend olx.interaction.DrawOptions * @param {Array | function | undefined} options.layers A list of layers from which polygons should be selected. Alternatively, a filter function can be provided. default: all visible layers * @param {Array | ol.Collection | function | undefined} options.featureFilter An array or a collection of features the interaction applies on or a function that takes a feature and a layer and returns true if the feature is a candidate * @param { ol.style.Style | Array | StyleFunction | undefined } Style for the selected features, default: default edit style */ ol.interaction.DrawHole = class olinteractionDrawHole extends ol.interaction.Draw { constructor(options) { options = options || {} // Geometry function that test points inside the current selection var _geometryFn = function(coordinates, geometry) { var coord = coordinates[0].pop() if (!this.getPolygon() || this.getPolygon().intersectsCoordinate(coord)) { this.lastOKCoord = [coord[0], coord[1]] } coordinates[0].push([this.lastOKCoord[0], this.lastOKCoord[1]]) if (geometry) { geometry.setCoordinates([coordinates[0].concat([coordinates[0][0]])]) } else { geometry = new ol.geom.Polygon(coordinates) } return geometry } var geomFn = options.geometryFunction if (geomFn) { options.geometryFunction = function (c, g) { g = _geometryFn(c, g) return geomFn(c, g) } } else { options.geometryFunction = _geometryFn } // Create draw interaction options.type = 'Polygon'; super(options) // Select interaction for the current feature this._select = new ol.interaction.Select({ style: options.style }) this._select.setActive(false) // Layer filter function if (options.layers) { if (typeof (options.layers) === 'function') { this.layers_ = options.layers } else if (options.layers.indexOf) { this.layers_ = function (l) { return (options.layers.indexOf(l) >= 0) } } } // Features to apply on if (typeof (options.featureFilter) === 'function') { this._features = options.featureFilter } else if (options.featureFilter) { var features = options.featureFilter this._features = function (f) { if (features.indexOf) { return !!features[features.indexOf(f)] } else { return !!features.item(features.getArray().indexOf(f)) } } } else { this._features = function () { return true } } // Start drawing if inside a feature this.on('drawstart', this._startDrawing.bind(this)) // End drawing add the hole to the current Polygon this.on('drawend', this._finishDrawing.bind(this)) } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { // Remove previous selection if (this.getMap()) this.getMap().removeInteraction(this._select) // Add new one if (map) map.addInteraction(this._select) super.setMap.call(this, map) } /** * Activate/deactivate the interaction * @param {boolean} * @api stable */ setActive(b) { if (this._select) this._select.getFeatures().clear() super.setActive.call(this, b) } /** * Remove last point of the feature currently being drawn * (test if points to remove before). */ removeLastPoint() { if (this._feature && this._feature.getGeometry().getCoordinates()[0].length > 2) { super.removeLastPoint.call(this) } } /** * Get the current polygon to hole * @return {ol.Feature} */ getPolygon() { return this._polygon // return this._select.getFeatures().item(0).getGeometry(); } /** * Get current feature to add a hole and start drawing * @param {ol.interaction.Draw.Event} e * @private */ _startDrawing(e) { var map = this.getMap() this._feature = e.feature var coord = e.feature.getGeometry().getCoordinates()[0][0] this._current = null // Check object under the pointer map.forEachFeatureAtPixel( map.getPixelFromCoordinate(coord), function (feature, layer) { // Not yet found? if (!this._current && this._features(feature, layer)) { var poly = feature.getGeometry() if (poly.getType() === "Polygon" && poly.intersectsCoordinate(coord)) { this._polygonIndex = false this._polygon = poly this._current = feature } else if (poly.getType() === "MultiPolygon" && poly.intersectsCoordinate(coord)) { for (var i = 0, p; p = poly.getPolygon(i); i++) { if (p.intersectsCoordinate(coord)) { this._polygonIndex = i this._polygon = p this._current = feature break } } } } }.bind(this), { layerFilter: this.layers_ } ) this._select.getFeatures().clear() if (!this._current) { this.setActive(false) this.setActive(true) } else { this._select.getFeatures().push(this._current) } } /** * Stop drawing and add the sketch feature to the target feature. * @param {ol.interaction.Draw.Event} e * @private */ _finishDrawing(e) { // The feature is the hole e.hole = e.feature // Get the current feature e.feature = this._select.getFeatures().item(0) this.dispatchEvent({ type: 'modifystart', features: [this._current] }) // Create the hole var c = e.hole.getGeometry().getCoordinates()[0] if (c.length > 3) { if (this._polygonIndex !== false) { var geom = e.feature.getGeometry() var newGeom = new ol.geom.MultiPolygon([]) for (var i = 0, pi; pi = geom.getPolygon(i); i++) { if (i === this._polygonIndex) { pi.appendLinearRing(new ol.geom.LinearRing(c)) newGeom.appendPolygon(pi) } else { newGeom.appendPolygon(pi) } } e.feature.setGeometry(newGeom) } else { this.getPolygon().appendLinearRing(new ol.geom.LinearRing(c)) } } this.dispatchEvent({ type: 'modifyend', features: [this._current] }) // reset this._feature = null this._select.getFeatures().clear() } /** * Function that is called when a geometry's coordinates are updated. * @param {Array} coordinates * @param {ol.geom.Polygon} geometry * @return {ol.geom.Polygon} * @private */ _geometryFn(coordinates, geometry) { var coord = coordinates[0].pop() if (!this.getPolygon() || this.getPolygon().intersectsCoordinate(coord)) { this.lastOKCoord = [coord[0], coord[1]] } coordinates[0].push([this.lastOKCoord[0], this.lastOKCoord[1]]) if (geometry) { geometry.setCoordinates([coordinates[0].concat([coordinates[0][0]])]) } else { geometry = new ol.geom.Polygon(coordinates) } return geometry } } /* 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). */ /** Interaction rotate * @constructor * @extends {ol.interaction.Interaction} * @fires drawstart, drawing, drawend, drawcancel * @param {olx.interaction.TransformOptions} options * @param {Array} options.source Destination source for the drawn features * @param {ol.Collection} options.features Destination collection for the drawn features * @param {ol.style.Style | Array. | ol.style.StyleFunction | undefined} options.style style for the sketch * @param {integer} options.sides number of sides, default 0 = circle * @param { ol.events.ConditionType | undefined } options.condition A function that takes an ol.MapBrowserEvent and returns a boolean that event should be handled. By default module:ol/events/condition.always. * @param { ol.events.ConditionType | undefined } options.squareCondition A function that takes an ol.MapBrowserEvent and returns a boolean to draw square features. Default test shift key * @param { ol.events.ConditionType | undefined } options.centerCondition A function that takes an ol.MapBrowserEvent and returns a boolean to draw centered features. Default check Ctrl key * @param { bool } options.canRotate Allow rotation when centered + square, default: true * @param { string } [options.geometryName=geometry] * @param { number } options.clickTolerance click tolerance on touch devices, default: 6 * @param { number } options.maxCircleCoordinates Maximum number of point on a circle, default: 100 */ ol.interaction.DrawRegular = class olinteractionDrawRegular extends ol.interaction.Interaction { constructor(options) { options = options || {} super({ handleEvent: function(e) { return self.handleEvent_(e) } }) var self = this; this.squaredClickTolerance_ = options.clickTolerance ? options.clickTolerance * options.clickTolerance : 36 this.maxCircleCoordinates_ = options.maxCircleCoordinates || 100 // Collection of feature to transform this.features_ = options.features // List of layers to transform this.source_ = options.source // Square condition this.conditionFn_ = options.condition // Square condition this.squareFn_ = options.squareCondition // Centered condition this.centeredFn_ = options.centerCondition // Allow rotation when centered + square this.canRotate_ = (options.canRotate !== false) // Specify custom geometry name this.geometryName_ = options.geometryName || 'geometry' // Number of sides (default=0: circle) this.setSides(options.sides) // Style var defaultStyle = ol.style.Style.defaultStyle(true) // Create a new overlay layer for the sketch this.sketch_ = new ol.Collection() this.overlayLayer_ = new ol.layer.Vector({ source: new ol.source.Vector({ features: this.sketch_, useSpatialIndex: false }), name: 'DrawRegular overlay', displayInLayerSwitcher: false, style: options.style || defaultStyle }) } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_) super.setMap(map) this.overlayLayer_.setMap(map) } /** * Activate/deactivate the interaction * @param {boolean} * @api stable */ setActive(b) { this.reset() super.setActive(b) } /** * Reset the interaction * @api stable */ reset() { if (this.overlayLayer_) this.overlayLayer_.getSource().clear() this.started_ = false } /** * Set the number of sides. * @param {int} number of sides. * @api stable */ setSides(nb) { nb = parseInt(nb) this.sides_ = nb > 2 ? nb : 0 } /** * Allow rotation when centered + square * @param {bool} * @api stable */ canRotate(b) { if (b === true || b === false) this.canRotate_ = b return this.canRotate_ } /** * Get the number of sides. * @return {int} number of sides. * @api stable */ getSides() { return this.sides_ } /** Get geom of the current drawing * @return {ol.geom.Polygon | ol.geom.Point} */ getGeom_() { this.overlayLayer_.getSource().clear() if (!this.center_) return false var g if (this.coord_) { var center = this.center_ var coord = this.coord_ // Specific case: circle var d, dmax, r, circle, centerPx if (!this.sides_ && this.square_ && !this.centered_) { center = [(coord[0] + center[0]) / 2, (coord[1] + center[1]) / 2] d = [coord[0] - center[0], coord[1] - center[1]] r = Math.sqrt(d[0] * d[0] + d[1] * d[1]) circle = new ol.geom.Circle(center, r, 'XY') // Optimize points on the circle centerPx = this.getMap().getPixelFromCoordinate(center) dmax = Math.max(100, Math.abs(centerPx[0] - this.coordPx_[0]), Math.abs(centerPx[1] - this.coordPx_[1])) dmax = Math.min(this.maxCircleCoordinates_, Math.round(dmax / 3)) return ol.geom.Polygon.fromCircle(circle, dmax, 0) } else { var hasrotation = this.canRotate_ && this.centered_ && this.square_ d = [coord[0] - center[0], coord[1] - center[1]] if (this.square_ && !hasrotation) { //var d = [coord[0] - center[0], coord[1] - center[1]]; var dm = Math.max(Math.abs(d[0]), Math.abs(d[1])) coord = [ center[0] + (d[0] > 0 ? dm : -dm), center[1] + (d[1] > 0 ? dm : -dm) ] } r = Math.sqrt(d[0] * d[0] + d[1] * d[1]) if (r > 0) { circle = new ol.geom.Circle(center, r, 'XY') var a if (hasrotation) a = Math.atan2(d[1], d[0]) else a = this.startAngle[this.sides_] || this.startAngle['default'] if (this.sides_) { g = ol.geom.Polygon.fromCircle(circle, this.sides_, a) } else { // Optimize points on the circle centerPx = this.getMap().getPixelFromCoordinate(this.center_) dmax = Math.max(100, Math.abs(centerPx[0] - this.coordPx_[0]), Math.abs(centerPx[1] - this.coordPx_[1])) dmax = Math.min(this.maxCircleCoordinates_, Math.round(dmax / (this.centered_ ? 3 : 5))) g = ol.geom.Polygon.fromCircle(circle, dmax, 0) } if (hasrotation) return g // Scale polygon to fit extent var ext = g.getExtent() if (!this.centered_) center = this.center_ else center = [2 * this.center_[0] - this.coord_[0], 2 * this.center_[1] - this.coord_[1]] var scx = (center[0] - coord[0]) / (ext[0] - ext[2]) var scy = (center[1] - coord[1]) / (ext[1] - ext[3]) if (this.square_) { var sc = Math.min(Math.abs(scx), Math.abs(scy)) scx = Math.sign(scx) * sc scy = Math.sign(scy) * sc } var t = [center[0] - ext[0] * scx, center[1] - ext[1] * scy] g.applyTransform(function (g1, g2, dim) { for (var i = 0; i < g1.length; i += dim) { g2[i] = g1[i] * scx + t[0] g2[i + 1] = g1[i + 1] * scy + t[1] } return g2 }) return g } } } // No geom => return a point return new ol.geom.Point(this.center_) } /** Draw sketch * @return {ol.Feature} The feature being drawn. */ drawSketch_(evt) { this.overlayLayer_.getSource().clear() if (evt) { this.square_ = this.squareFn_ ? this.squareFn_(evt) : evt.originalEvent.shiftKey this.centered_ = this.centeredFn_ ? this.centeredFn_(evt) : evt.originalEvent.metaKey || evt.originalEvent.ctrlKey var g = this.getGeom_() if (g) { var f = this.feature_ //f.setGeometry (g); if (g.getType() === 'Polygon') f.getGeometry().setCoordinates(g.getCoordinates()) this.overlayLayer_.getSource().addFeature(f) if (this.coord_ && this.square_ && ((this.canRotate_ && this.centered_ && this.coord_) || (!this.sides_ && !this.centered_))) { this.overlayLayer_.getSource().addFeature(new ol.Feature(new ol.geom.LineString([this.center_, this.coord_]))) } return f } } } /** Draw sketch (Point) */ drawPoint_(pt, noclear) { if (!noclear) this.overlayLayer_.getSource().clear() this.overlayLayer_.getSource().addFeature(new ol.Feature(new ol.geom.Point(pt))) } /** * @param {ol.MapBrowserEvent} evt Map browser event. */ handleEvent_(evt) { var dx, dy // Event date time this._eventTime = new Date() switch (evt.type) { case "pointerdown": { if (this.conditionFn_ && !this.conditionFn_(evt)) break this.downPx_ = evt.pixel this.start_(evt) // Test long touch var dt = 500 this._longTouch = false setTimeout(function () { this._longTouch = (new Date() - this._eventTime > .9 * dt) if (this._longTouch) this.handleMoveEvent_(evt) }.bind(this), dt) break } case "pointerup": { // Started and fisrt move if (this.started_ && this.coord_) { dx = this.downPx_[0] - evt.pixel[0] dy = this.downPx_[1] - evt.pixel[1] if (dx * dx + dy * dy <= this.squaredClickTolerance_) { // The pointer has moved if (this.lastEvent == "pointermove" || this.lastEvent == "keydown") { this.end_(evt) } // On touch device there is no move event : terminate = click on the same point else { dx = this.upPx_[0] - evt.pixel[0] dy = this.upPx_[1] - evt.pixel[1] if (dx * dx + dy * dy <= this.squaredClickTolerance_) { this.end_(evt) } else { this.handleMoveEvent_(evt) this.drawPoint_(evt.coordinate, true) } } } } this.upPx_ = evt.pixel break } case "pointerdrag": { if (this.started_) { var centerPx = this.getMap().getPixelFromCoordinate(this.center_) dx = centerPx[0] - evt.pixel[0] dy = centerPx[1] - evt.pixel[1] if (dx * dx + dy * dy <= this.squaredClickTolerance_) { this.reset() } } return !this._longTouch // break; } case "pointermove": { if (this.started_) { dx = this.downPx_[0] - evt.pixel[0] dy = this.downPx_[1] - evt.pixel[1] if (dx * dx + dy * dy > this.squaredClickTolerance_) { this.handleMoveEvent_(evt) this.lastEvent = evt.type } } break } default: { this.lastEvent = evt.type // Prevent zoom in on dblclick if (this.started_ && evt.type === 'dblclick') { //evt.stopPropagation(); return false } break } } return true } /** Stop drawing. */ finishDrawing() { if (this.started_ && this.coord_) { this.end_({ pixel: this.upPx_, coordinate: this.coord_ }) } } /** * @param {ol.MapBrowserEvent} evt Event. */ handleMoveEvent_(evt) { if (this.started_) { this.coord_ = evt.coordinate this.coordPx_ = evt.pixel var f = this.drawSketch_(evt) this.dispatchEvent({ type: 'drawing', feature: f, pixel: evt.pixel, startCoordinate: this.center_, coordinate: evt.coordinate, square: this.square_, centered: this.centered_ }) } else { this.drawPoint_(evt.coordinate) } } /** Start an new draw * @param {ol.MapBrowserEvent} evt Map browser event. * @return {boolean} `false` to stop the drag sequence. */ start_(evt) { if (!this.started_) { this.started_ = true this.center_ = evt.coordinate this.coord_ = null var f = this.feature_ = new ol.Feature({}) f.setGeometryName(this.geometryName_ || 'geometry') f.setGeometry(new ol.geom.Polygon([[evt.coordinate, evt.coordinate, evt.coordinate]])) this.drawSketch_(evt) this.dispatchEvent({ type: 'drawstart', feature: f, pixel: evt.pixel, coordinate: evt.coordinate }) } else { this.coord_ = evt.coordinate } } /** End drawing * @param {ol.MapBrowserEvent} evt Map browser event. * @return {boolean} `false` to stop the drag sequence. */ end_(evt) { this.coord_ = evt.coordinate this.started_ = false if (this.coord_ && (this.center_[0] !== this.coord_[0] || this.center_[1] !== this.coord_[1])) { var f = this.feature_ f.setGeometry(this.getGeom_()) if (this.source_) this.source_.addFeature(f) else if (this.features_) this.features_.push(f) this.dispatchEvent({ type: 'drawend', feature: f, pixel: evt.pixel, coordinate: evt.coordinate, square: this.square_, centered: this.centered_ }) } else { this.dispatchEvent({ type: 'drawcancel', feature: null, pixel: evt.pixel, coordinate: evt.coordinate, square: this.square_, centered: this.centered_ }) } this.center_ = this.coord_ = null this.drawSketch_() } } /** Default start angle array for each sides */ ol.interaction.DrawRegular.prototype.startAngle = { 'default':Math.PI/2, 3: -Math.PI/2, 4: Math.PI/4 }; /* 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). */ /** Interaction DrawTouch : pointer is deferred to the center of the viewport and a target is drawn to materialize this point * The interaction modifies map browser event coordinate and pixel properties to force pointer on the viewport center to any interaction that them. * @constructor * @fires drawstart * @fires drawend * @fires drawabort * @extends {ol.interaction.CenterTouch} * @param {olx.interaction.DrawOptions} options * @param {ol.source.Vector | undefined} options.source Destination source for the drawn features. * @param {ol.geom.GeometryType} options.type Drawing type ('Point', 'LineString', 'Polygon') not ('MultiPoint', 'MultiLineString', 'MultiPolygon' or 'Circle'). Required. * @param {boolean} [options.tap=true] enable point insertion on tap, default true * @param {ol.style.Style|Array} [options.style] Drawing style * @param {ol.style.Style|Array} [options.sketchStyle] Sketch style * @param {ol.style.Style|Array} [options.targetStyle] a style to draw the target point, default cross style * @param {string} [options.composite] composite operation : difference|multiply|xor|screen|overlay|darken|lighter|lighten|... */ ol.interaction.DrawTouch = class olinteractionDrawTouch extends ol.interaction.CenterTouch { constructor(options) { options = options || {}; options.handleEvent = function (e) { if (this.get('tap')) { this.sketch.setPosition(this.getPosition()); switch (e.type) { case 'singleclick': { this.addPoint(); break; } case 'dblclick': { this.addPoint(); this.finishDrawing(); return false; //break; } default: break; } } return true; }; super(options); if (!options.sketchStyle) { options.sketchStyle = ol.style.Style.defaultStyle(); } var sketch = this.sketch = new ol.layer.SketchOverlay(options); sketch.on(['drawstart', 'drawabort'], function (e) { this.dispatchEvent(e); }.bind(this)); sketch.on(['drawend'], function (e) { if (e.feature && e.valid && options.source) options.source.addFeature(e.feature); this.dispatchEvent(e); }.bind(this)); this._source = options.source; this.set('tap', options.tap !== false); this.setActive(options.active !== false); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._listener) { for (var l in this._listener) ol.Observable.unByKey(l); } this._listener = {}; super.setMap(map); this.sketch.setMap(map); if (map) { this._listener.center = map.on('postcompose', function () { if (!ol.coordinate.equal(this.getPosition(), this.sketch.getPosition() || [])) { this.sketch.setPosition(this.getPosition()); } }.bind(this)); } } /** Set geometry type * @param {ol.geom.GeometryType} type */ setGeometryType(type) { return this.sketch.setGeometryType(type); } /** Get geometry type * @return {ol.geom.GeometryType} */ getGeometryType() { return this.sketch.getGeometryType(); } /** Start drawing and add the sketch feature to the target layer. * The ol.interaction.Draw.EventType.DRAWEND event is dispatched before inserting the feature. */ finishDrawing() { this.sketch.finishDrawing(true); } /** Add a new Point to the drawing */ addPoint() { this.sketch.addPoint(this.getPosition()); } /** Remove last point of the feature currently being drawn. */ removeLastPoint() { this.sketch.removeLastPoint(); } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @observable * @api */ setActive(b) { super.setActive(b); if (this.sketch) { this.sketch.abortDrawing(); this.sketch.setVisible(b); } } } /** Extend DragAndDrop choose drop zone + fires loadstart, loadend * @constructor * @extends {ol.interaction.Interaction} * @fires loadstart, loadend, addfeatures * @param {*} options * @param {string} options.zone selector for the drop zone, default document * @param{ol.projection} options.projection default projection of the map * @param {Array|undefined} options.formatConstructors Format constructors, default [ ol.format.GPX, ol.format.GeoJSONX, ol.format.GeoJSONP, ol.format.GeoJSON, ol.format.IGC, ol.format.KML, ol.format.TopoJSON ] * @param {Array|undefined} options.accept list of accepted format, default ["gpx","json","geojsonx","geojsonp","geojson","igc","kml","topojson"] */ ol.interaction.DropFile = class olinteractionDropFile extends ol.interaction.Interaction { constructor(options) { options = options || {} super({}) var zone = options.zone || document zone.addEventListener('dragenter', this.onstop) zone.addEventListener('dragover', this.onstop) zone.addEventListener('dragleave', this.onstop) // Options this.formatConstructors_ = options.formatConstructors || [ol.format.GPX, ol.format.GeoJSONX, ol.format.GeoJSONP, ol.format.GeoJSON, ol.format.IGC, ol.format.KML, ol.format.TopoJSON] this.projection_ = options.projection this.accept_ = options.accept || ["gpx", "json", "geojsonx", "geojsonp", "geojson", "igc", "kml", "topojson"] zone.addEventListener('drop', function (e) { return this.ondrop(e) }.bind(this)) } /** Set the map */ setMap(map) { super.setMap(map) } /** Do something when over */ onstop(e) { e.preventDefault() e.stopPropagation() return false } /** Do something when over */ ondrop(e) { e.preventDefault() if (e.dataTransfer && e.dataTransfer.files.length) { var projection = this.projection_ || (this.getMap() ? this.getMap().getView().getProjection() : null) // fetch FileList object var files = e.dataTransfer.files // e.originalEvent.target.files ? // process all File objects var pat = /\.([0-9a-z]+)(?=[?#])|(\.)(?:[\w]+)$/ Array.prototype.forEach.call(files, function(file) { var ex = file.name.match(pat)[0] var isok = (this.accept_.indexOf(ex.toLocaleLowerCase()) >= 0) this.dispatchEvent({ type: 'loadstart', file: file, filesize: file.size, filetype: file.type, fileextension: ex, projection: projection, isok: isok }) // Don't load file if (this.formatConstructors_.length) { // Load file var reader = new FileReader() var formatConstructors = this.formatConstructors_ var theFile = file reader.onload = function (e) { var result = e.target.result var features = [] var i, ii for (i = 0, ii = formatConstructors.length; i < ii; ++i) { var formatConstructor = formatConstructors[i] try { var format = new formatConstructor() features = format.readFeatures(result, { featureProjection: projection }) if (features && features.length > 0) { this.dispatchEvent({ type: 'addfeatures', features: features, file: theFile, projection: projection }) this.dispatchEvent({ type: 'loadend', features: features, file: theFile, projection: projection }) return } } catch (e) { /* ok */ } } // Nothing match, try to load by yourself this.dispatchEvent({ type: 'loadend', file: theFile, result: result }) }.bind(this) // Start loading reader.readAsText(file) } }.bind(this)) } return false } } /** A Select interaction to fill feature's properties on click. * @constructor * @extends {ol.interaction.Interaction} * @fires setattributestart * @fires setattributeend * @param {*} options extentol.interaction.Select options * @param {boolean} options.active activate the interaction on start, default true * @param {string=} options.name * @param {boolean|string} options.cursor interaction cursor if false use default, default use a paint bucket cursor * @param {*} properties The properties as key/value */ ol.interaction.FillAttribute = class olinteractionFillAttribute extends ol.interaction.Select { constructor(options, properties) { options = options || {}; if (!options.condition) options.condition = ol.events.condition.click; super(options); this.setActive(options.active !== false); this.set('name', options.name); this._attributes = properties; this.on('select', function (e) { this.getFeatures().clear(); this.fill(e.selected, this._attributes); }.bind(this)); if (options.cursor === undefined) { var canvas = document.createElement('CANVAS'); canvas.width = canvas.height = 32; var ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.moveTo(9, 3); ctx.lineTo(2, 9); ctx.lineTo(10, 17); ctx.lineTo(17, 11); ctx.closePath(); ctx.fillStyle = "#fff"; ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(6, 4); ctx.lineTo(0, 8); ctx.lineTo(0, 13); ctx.lineTo(3, 17); ctx.lineTo(3, 8); ctx.closePath(); ctx.fillStyle = "#000"; ctx.fill(); ctx.stroke(); ctx.moveTo(8, 8); ctx.lineTo(10, 0); ctx.lineTo(11, 0); ctx.lineTo(13, 3); ctx.lineTo(13, 7); ctx.stroke(); this._cursor = 'url(' + canvas.toDataURL() + ') 0 13, auto'; } if (options.cursor) { this._cursor = options.cursor; } } /** Define the interaction cursor * @param {string} cursor CSS cursor */ setCursor(cursor) { this._cursor = cursor; } /** Get the interaction cursor * @return {string} cursor */ getCursor() { return this._cursor; } /** Activate the interaction * @param {boolean} active */ setActive(active) { if (active === this.getActive()) return; super.setActive(active); if (this.getMap() && this._cursor) { if (active) { this._previousCursor = this.getMap().getTargetElement().style.cursor; this.getMap().getTargetElement().style.cursor = this._cursor; // console.log('setCursor',this._cursor) } else { this.getMap().getTargetElement().style.cursor = this._previousCursor; this._previousCursor = undefined; } } } /** Set attributes * @param {*} properties The properties as key/value */ setAttributes(properties) { this._attributes = properties; } /** Set an attribute * @param {string} key * @param {*} val */ setAttribute(key, val) { this._attributes[key] = val; } /** get attributes * @return {*} The properties as key/value */ getAttributes() { return this._attributes; } /** Get an attribute * @param {string} key * @return {*} val */ getAttribute(key) { return this._attributes[key]; } /** Fill feature attributes * @param {Array} features The features to modify * @param {*} properties The properties as key/value */ fill(features, properties) { if (features.length && properties) { // Test changes var changes = false; for (var i = 0, f; f = features[i]; i++) { for (var p in properties) { if (f.get(p) !== properties[p]) changes = true; } if (changes) break; } // Set Attributes if (changes) { this.dispatchEvent({ type: 'setattributestart', features: features, properties: properties }); features.forEach(function (f) { for (var p in properties) { f.set(p, properties[p]); } }); this.dispatchEvent({ type: 'setattributeend', features: features, properties: properties }); } } } } /** * @constructor * @extends {ol.interaction.Pointer} * @param {ol.flashlight.options} flashlight options param * @param {ol.Color} options.color light color, default transparent * @param {ol.Color} options.fill fill color, default rgba(0,0,0,0.8) * @param {number} options.radius radius of the flash */ ol.interaction.Flashlight = class olinteractionFlashlight extends ol.interaction.Pointer { constructor(options) { super({ handleDownEvent: function(e) { return this.setPosition(e) }, handleMoveEvent: function(e) { return this.setPosition(e) }, }) // Default options options = options || {} this.pos = false this.radius = (options.radius || 100) this.setColor(options) } /** Set the map > start postcompose */ setMap(map) { if (this.getMap()) { this.getMap().render() } if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null super.setMap(map) if (map) { this._listener = map.on('postcompose', this.postcompose_.bind(this)) } } /** Set flashlight radius * @param {integer} radius */ setRadius(radius) { this.radius = radius if (this.getMap()) { try { this.getMap().renderSync()} catch (e) { /* ok */ } } } /** Set flashlight color * @param {ol.flashlight.options} flashlight options param * @param {ol.Color} [options.color] light color, default transparent * @param {ol.Color} [options.fill] fill color, default rgba(0,0,0,0.8) */ setColor(options) { // Backcolor var color = (options.fill ? options.fill : [0, 0, 0, 0.8]) var c = ol.color.asArray(color) // var op = c[3] this.startColor = ol.color.asString(c) // Halo color if (options.color) { this.endColor = ol.color.asString(ol.color.asArray(options.color) || options.color) c = ol.color.asArray(this.endColor); } else { c[3] = 0 this.endColor = ol.color.asString(c) } //c[3] = (op + c[3] * 9) / 10; this.midColor = ol.color.asString(c) if (this.getMap()) { try { this.getMap().renderSync()} catch (e) { /* ok */ } } } /** Set position of the flashlight * @param {ol.Pixel|ol.MapBrowserEvent} */ setPosition(e) { if (e.pixel) this.pos = e.pixel else this.pos = e if (this.getMap()) { try { this.getMap().renderSync()} catch (e) { /* ok */ } } } /** Postcompose function */ postcompose_(e) { var ctx = ol.ext.getMapCanvas(this.getMap()).getContext('2d') var ratio = e.frameState.pixelRatio var w = ctx.canvas.width var h = ctx.canvas.height ctx.save() ctx.scale(ratio, ratio) if (!this.pos) { ctx.fillStyle = this.startColor ctx.fillRect(0, 0, w, h) } else { var d = Math.max(w, h) // reveal wherever we drag var radGrd = ctx.createRadialGradient(this.pos[0], this.pos[1], w * this.radius / d, this.pos[0], this.pos[1], h * this.radius / d) radGrd.addColorStop(0, this.startColor) radGrd.addColorStop(0.8, this.midColor) radGrd.addColorStop(1, this.endColor) ctx.fillStyle = radGrd ctx.fillRect(this.pos[0] - d, this.pos[1] - d, 2 * d, 2 * d) } ctx.restore() } } /** An interaction to focus on the map on click. Usefull when using keyboard event on the map. * @deprecated use ol/interaction/CurrentMap instead * @constructor * @fires focus * @extends {ol.interaction.Interaction} */ ol.interaction.FocusMap = class olinteractionFocusMap extends ol.interaction.Interaction { constructor() { // super({}); // Focus (hidden) button to focus on the map when click on it this.focusBt = ol.ext.element.create('BUTTON', { on: { focus: function () { this.dispatchEvent({ type: 'focus' }); }.bind(this) }, style: { position: 'absolute', zIndex: -1, top: 0, opacity: 0 } }); } /** Set the map > add the focus button and focus on the map when pointerdown to enable keyboard events. */ setMap(map) { if (this._listener) ol.Observable.unByKey(this._listener); this._listener = null; if (this.getMap()) { this.getMap().getViewport().removeChild(this.focusBt); } super.setMap(map); if (this.getMap()) { // Force focus on the clicked map this._listener = this.getMap().on('pointerdown', function () { if (this.getActive()) this.focusBt.focus(); }.bind(this)); this.getMap().getViewport().appendChild(this.focusBt); } } } // /** Interaction to draw on the current geolocation * It combines a draw with a ol.Geolocation * @constructor * @extends {ol.interaction.Interaction} * @fires drawstart, drawend, drawing, tracking, follow * @param {any} options * @param { ol.Collection. | undefined } option.features Destination collection for the drawn features. * @param { ol.source.Vector | undefined } options.source Destination source for the drawn features. * @param {ol.geom.GeometryType} options.type Drawing type ('Point', 'LineString', 'Polygon'), default LineString. * @param {Number | undefined} options.minAccuracy minimum accuracy underneath a new point will be register (if no condition), default 20 * @param {function | undefined} options.condition a function that take a ol.Geolocation object and return a boolean to indicate whether location should be handled or not, default return true if accuracy < minAccuracy * @param {Object} options.attributes a list of attributes to register as Point properties: {accuracy:true,accuracyGeometry:true,heading:true,speed:true}, default none. * @param {Number} options.tolerance tolerance to add a new point (in meter), default 5 * @param {Number} options.zoom zoom for tracking, default 16 * @param {Number} options.minZoom min zoom for tracking, if zoom is less it will zoom to it, default use zoom option * @param {boolean|auto|position|visible} options.followTrack true if you want the interaction to follow the track on the map, default true * @param { ol.style.Style | Array. | ol.StyleFunction | undefined } options.style Style for sketch features. */ ol.interaction.GeolocationDraw = class olinteractionGeolocationDraw extends ol.interaction.Interaction { constructor(options) { options = options || {} super({ handleEvent: function () { return (!this.get('followTrack') || this.get('followTrack') == 'auto') // || !geoloc.getTracking()); } }) // Geolocation this.geolocation = new ol.Geolocation(({ projection: "EPSG:4326", trackingOptions: { maximumAge: 10000, enableHighAccuracy: true, timeout: 600000 } })) this.geolocation.on('change', this.draw_.bind(this)) // Current path this.path_ = [] this.lastPosition_ = false // Default style var white = [255, 255, 255, 1] var blue = [0, 153, 255, 1] var width = 3 var circle = new ol.style.Circle({ radius: width * 2, fill: new ol.style.Fill({ color: blue }), stroke: new ol.style.Stroke({ color: white, width: width / 2 }) }) var style = [ new ol.style.Style({ stroke: new ol.style.Stroke({ color: white, width: width + 2 }) }), new ol.style.Style({ stroke: new ol.style.Stroke({ color: blue, width: width }), fill: new ol.style.Fill({ color: [255, 255, 255, 0.5] }) }) ] var triangle = new ol.style.RegularShape({ radius: width * 3.5, points: 3, rotation: 0, fill: new ol.style.Fill({ color: blue }), stroke: new ol.style.Stroke({ color: white, width: width / 2 }) }) // stretch the symbol var c = triangle.getImage() var ctx = c.getContext("2d") var c2 = document.createElement('canvas') c2.width = c2.height = c.width c2.getContext("2d").drawImage(c, 0, 0) ctx.clearRect(0, 0, c.width, c.height) ctx.drawImage(c2, 0, 0, c.width, c.height, width, 0, c.width - 2 * width, c.height) var defaultStyle = function (f) { if (f.get('heading') === undefined) { style[1].setImage(circle) } else { style[1].setImage(triangle) triangle.setRotation(f.get('heading') || 0) } return style } // Style for the accuracy geometry this.locStyle = { error: new ol.style.Style({ fill: new ol.style.Fill({ color: [255, 0, 0, 0.2] }) }), warn: new ol.style.Style({ fill: new ol.style.Fill({ color: [255, 192, 0, 0.2] }) }), ok: new ol.style.Style({ fill: new ol.style.Fill({ color: [0, 255, 0, 0.2] }) }), } // Create a new overlay layer for the sketch this.overlayLayer_ = new ol.layer.Vector({ source: new ol.source.Vector(), name: 'GeolocationDraw overlay', style: options.style || defaultStyle }) this.sketch_ = [new ol.Feature(), new ol.Feature(), new ol.Feature()] this.overlayLayer_.getSource().addFeatures(this.sketch_) this.features_ = options.features this.source_ = options.source this.condition_ = options.condition || function (loc) { return loc.getAccuracy() < this.get("minAccuracy") } this.set('type', options.type || "LineString") this.set('attributes', options.attributes || {}) this.set('minAccuracy', options.minAccuracy || 20) this.set('tolerance', options.tolerance || 5) this.set('zoom', options.zoom) this.set('minZoom', options.minZoom) this.setFollowTrack(options.followTrack === undefined ? true : options.followTrack) this.setActive(false) } /** Simplify 3D geometry * @param {ol.geom.Geometry} geo * @param {number} tolerance */ simplify3D(geo, tolerance) { var geom = geo.getCoordinates() var proj = this.getMap().getView().getProjection() if (this.get("type") === 'Polygon') { geom = geom[0] } var simply = [geom[0]] var pi, p = ol.proj.transform(geom[0], proj, 'EPSG:4326') for (var i = 1; i < geom.length; i++) { pi = ol.proj.transform(geom[i], proj, 'EPSG:4326') var d = ol.sphere.getDistance(p, pi) if (d > tolerance) { simply.push(geom[i]) p = pi } } if (simply[simply.length - 1] !== geom[geom.length - 1]) { simply.push(geom[geom.length - 1]) } /* var simply = geo.simplify(tolerance).getCoordinates(); if (this.get("type")==='Polygon') { simply = simply[0]; } var step=0; simply.forEach(function(p) { for (; step|boolean} track a list of point or false to stop * @param {*} options * @param {number} delay delay in ms, default 1000 (1s) * @param {number} accuracy gps accuracy, default 10 * @param {boolean} repeat repeat track, default true */ simulate(track, options) { if (this._track) { clearTimeout(this._track.timeout) } if (!track) { this._track = false return } options = options || {} var delay = options.delay || 1000 function handleTrack() { if (this._track.pos >= this._track.track.length) { this._track = false return } var coord = this._track.track[this._track.pos] coord[2] = coord[3] || 0 coord[3] = (new Date()).getTime() this._track.pos++ if (options.repeat !== false) { this._track.pos = this._track.pos % this._track.track.length } if (this.getActive()) this.draw_(true, coord, options.accuracy) this._track.timeout = setTimeout(handleTrack.bind(this), delay) } this._track = { track: track, pos: 0, timeout: setTimeout(handleTrack.bind(this), 0) } } /** Is simulation on ? * @returns {boolean} */ simulating() { return !!this._track } /** Reset drawing */ reset() { this.sketch_[1].setGeometry() this.path_ = [] this.lastPosition_ = false } /** Start tracking = setActive(true) */ start() { this.setActive(true) } /** Stop tracking = setActive(false) */ stop() { this.setActive(false) } /** Pause drawing * @param {boolean} b */ pause(b) { this.pause_ = (b !== false) } /** Is paused * @return {boolean} b */ isPaused() { return this.pause_ } /** Enable following the track on the map * @param {boolean|auto|position|visible} follow, * false: don't follow, * true: follow (position+zoom), * 'position': follow only position, * 'auto': start following until user move the map, * 'visible': center when position gets out of the visible extent */ setFollowTrack(follow) { this.set('followTrack', follow) var map = this.getMap() // Center if wanted if (this.getActive() && map) { var zoom if (follow !== 'position') { if (this.get('minZoom')) { zoom = Math.max(this.get('minZoom'), map.getView().getZoom()) } else { zoom = this.get('zoom') } } if (follow !== false && !this.lastPosition_) { var pos = this.path_[this.path_.length - 1] if (pos) { map.getView().animate({ center: pos, zoom: zoom }) } } else if (follow === 'auto' && this.lastPosition_) { map.getView().animate({ center: this.lastPosition_, zoom: zoom }) } } this.lastPosition_ = false this.dispatchEvent({ type: 'follow', following: follow !== false }) } /** Add a new point to the current path * @private */ draw_(simulate, coord, accuracy) { var map = this.getMap() if (!map) return var accu, pos, p, loc, heading // Simulation mode if (this._track) { if (simulate !== true) return pos = coord accu = accuracy || 10 if (this.path_ && this.path_.length) { var pt = this.path_[this.path_.length - 1] heading = Math.atan2(coord[0] - pt[0], coord[1] - pt[1]) } var circle = new ol.geom.Circle(pos, map.getView().getResolution() * accu) p = ol.geom.Polygon.fromCircle(circle) } else { // Current location loc = this.geolocation accu = loc.getAccuracy() pos = this.getPosition(loc) p = loc.getAccuracyGeometry() heading = loc.getHeading() } // Center on point // console.log(this.get('followTrack')) switch (this.get('followTrack')) { // Follow center + zoom case true: { // modify zoom if (this.get('followTrack') == true) { if (this.get('minZoom')) { if (this.get('minZoom') > map.getView().getZoom()) { map.getView().setZoom(this.get('minZoom')) } } else { map.getView().setZoom(this.get('zoom') || 16) } if (!ol.extent.containsExtent(map.getView().calculateExtent(map.getSize()), p.getExtent())) { map.getView().fit(p.getExtent()) } } map.getView().setCenter(pos) break } // Follow position case 'position': { // modify center map.getView().setCenter(pos) break } // Keep on following case 'auto': { if (this.lastPosition_) { var center = map.getView().getCenter() // console.log(center,this.lastPosition_) if (center[0] != this.lastPosition_[0] || center[1] != this.lastPosition_[1]) { //this.dispatchEvent({ type:'follow', following: false }); this.setFollowTrack(false) } else { map.getView().setCenter(pos) this.lastPosition_ = pos } } else { map.getView().setCenter(pos) if (this.get('minZoom')) { if (this.get('minZoom') > map.getView().getZoom()) { map.getView().setZoom(this.get('minZoom')) } } else if (this.get('zoom')) { map.getView().setZoom(this.get('zoom')) } this.lastPosition_ = pos } break } // Force to stay on the map case 'visible': { if (!ol.extent.containsCoordinate(map.getView().calculateExtent(map.getSize()), pos)) { map.getView().setCenter(pos) } break } // Don't follow default: break } // Draw occuracy var f = this.sketch_[0] f.setGeometry(p) if (accu < this.get("minAccuracy") / 2) f.setStyle(this.locStyle.ok) else if (accu < this.get("minAccuracy")) f.setStyle(this.locStyle.warn) else f.setStyle(this.locStyle.error) var geo if (this.pause_) { this.lastPosition_ = pos } if (!this.pause_ && (!loc || this.condition_.call(this, loc))) { f = this.sketch_[1] this.path_.push(pos) switch (this.get("type")) { case "Point": this.path_ = [pos] f.setGeometry(new ol.geom.Point(pos, 'XYZM')) var attr = this.get('attributes') if (attr.heading) f.set("heading", loc.getHeading()) if (attr.accuracy) f.set("accuracy", loc.getAccuracy()) if (attr.altitudeAccuracy) f.set("altitudeAccuracy", loc.getAltitudeAccuracy()) if (attr.speed) f.set("speed", loc.getSpeed()) break case "LineString": if (this.path_.length > 1) { geo = new ol.geom.LineString(this.path_, 'XYZM') if (this.get("tolerance")) geo = this.simplify3D(geo, this.get("tolerance")) f.setGeometry(geo) } else { f.setGeometry() } break case "Polygon": if (this.path_.length > 2) { geo = new ol.geom.Polygon([this.path_], 'XYZM') if (this.get("tolerance")) geo = this.simplify3D(geo, this.get("tolerance")) f.setGeometry(geo) } else f.setGeometry() break } this.dispatchEvent({ type: 'drawing', feature: this.sketch_[1], geolocation: loc }) } this.sketch_[2].setGeometry(new ol.geom.Point(pos)) this.sketch_[2].set("heading", heading) // Drawing this.dispatchEvent({ type: 'tracking', feature: this.sketch_[1], geolocation: loc }) } /** Get a position according to the geolocation * @param {Geolocation} loc * @returns {Array} an array of measure X,Y,Z,T * @api */ getPosition(loc) { var pos = loc.getPosition() pos.push(Math.round((loc.getAltitude() || 0) * 100) / 100) pos.push(Math.round((new Date()).getTime() / 1000)) return pos } } /** Interaction hover do to something when hovering a feature * @constructor * @extends {ol.interaction.Interaction} * @fires hover, enter, leave * @param {olx.interaction.HoverOptions} * @param { string | undefined } options.cursor css cursor propertie or a function that gets a feature, default: none * @param {function | undefined} options.featureFilter filter a function with two arguments, the feature and the layer of the feature. Return true to select the feature * @param {function | undefined} options.layerFilter filter a function with one argument, the layer to test. Return true to test the layer * @param {Array | undefined} options.layers a set of layers to test * @param {number | undefined} options.hitTolerance Hit-detection tolerance in pixels. * @param { function | undefined } options.handleEvent Method called by the map to notify the interaction that a browser event was dispatched to the map. The function may return false to prevent the propagation of the event to other interactions in the map's interactions chain. */ ol.interaction.Hover = class olinteractionHover extends ol.interaction.Interaction { constructor(options) { if (!options) options = options || {}; var dragging = false; super({ handleEvent: function (e) { if (!self.getActive()) return true; switch (e.type) { case 'pointerdrag': { dragging = true; break; } case 'pointerup': { dragging = false; break; } case 'pointermove': { if (!dragging) { self.handleMove_(e); } break; } } if (options.handleEvent) return options.handleEvent(e); return true; } }); var self = this; this.setLayerFilter(options.layerFilter); if (options.layers && options.layers.length) { this.setLayerFilter(function (l) { return (options.layers.indexOf(l) >= 0); }); } this.setFeatureFilter(options.featureFilter); this.set('hitTolerance', options.hitTolerance); this.setCursor(options.cursor); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.previousCursor_ !== undefined && this.getMap()) { this.getMap().getTargetElement().style.cursor = this.previousCursor_; this.previousCursor_ = undefined; } super.setMap(map); } /** Activate / deactivate interaction * @param {boolean} b */ setActive(b) { super.setActive(b); if (this.cursor_ && this.getMap() && this.getMap().getTargetElement()) { var style = this.getMap().getTargetElement().style; if (this.previousCursor_ !== undefined) { style.cursor = this.previousCursor_; this.previousCursor_ = undefined; } } } /** * Set cursor on hover * @param { string } cursor css cursor propertie or a function that gets a feature, default: none * @api stable */ setCursor(cursor) { if (!cursor && this.previousCursor_ !== undefined && this.getMap()) { this.getMap().getTargetElement().style.cursor = this.previousCursor_; this.previousCursor_ = undefined; } this.cursor_ = cursor; } /** Feature filter to get only one feature * @param {function} filter a function with two arguments, the feature and the layer of the feature. Return true to select the feature */ setFeatureFilter(filter) { if (typeof (filter) == 'function') this.featureFilter_ = filter; else this.featureFilter_ = function () { return true; }; } /** Feature filter to get only one feature * @param {function} filter a function with one argument, the layer to test. Return true to test the layer */ setLayerFilter(filter) { if (typeof (filter) == 'function') this.layerFilter_ = filter; else this.layerFilter_ = function () { return true; }; } /** Get features whenmove * @param {ol.event} e "move" event */ handleMove_(e) { var map = this.getMap(); if (map) { //var b = map.hasFeatureAtPixel(e.pixel); var feature, layer; var self = this; var b = map.forEachFeatureAtPixel( e.pixel, function (f, l) { if (self.featureFilter_.call(null, f, l)) { feature = f; layer = l; return true; } else { feature = layer = null; return false; } }, { hitTolerance: this.get('hitTolerance'), layerFilter: self.layerFilter_ } ); if (b) this.dispatchEvent({ type: 'hover', feature: feature, layer: layer, coordinate: e.coordinate, pixel: e.pixel, map: e.map, originalEvent: e.originalEvent, dragging: e.dragging }); if (this.feature_ === feature && this.layer_ === layer) { /* ok */ } else { this.feature_ = feature; this.layer_ = layer; if (feature) { this.dispatchEvent({ type: 'enter', feature: feature, layer: layer, coordinate: e.coordinate, pixel: e.pixel, map: e.map, originalEvent: e.originalEvent, dragging: e.dragging }); } else { this.dispatchEvent({ type: 'leave', coordinate: e.coordinate, pixel: e.pixel, map: e.map, originalEvent: e.originalEvent, dragging: e.dragging }); } } if (this.cursor_) { var style = map.getTargetElement().style; if (b) { if (style.cursor != this.cursor_) { this.previousCursor_ = style.cursor; style.cursor = this.cursor_; } } else if (this.previousCursor_ !== undefined) { style.cursor = this.previousCursor_; this.previousCursor_ = undefined; } } } } } /* 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). */ /** Interaction to handle longtouch events * @constructor * @extends {ol.interaction.Interaction} * @param {olx.interaction.LongTouchOptions} * @param {function | undefined} options.handleLongTouchEvent Function handling 'longtouch' events, it will receive a mapBrowserEvent. Or listen to the map 'longtouch' event. * @param {integer | undefined} [options.pixelTolerance=0] pixel tolerance before drag, default 0 * @param {integer | undefined} [options.delay=1000] The delay for a long touch in ms, default is 1000 */ ol.interaction.LongTouch = class olinteractionLongTouch extends ol.interaction.Interaction { constructor(options) { options = options || {}; var ltouch = options.handleLongTouchEvent || function () { }; var _timeout = null; var position, event; var tol = options.pixelTolerance || 0; super({ handleEvent: function (e) { if (this.getActive()) { switch (e.type) { case 'pointerdown': { if (_timeout) clearTimeout(_timeout); position = e.pixel; event = { type: 'longtouch', originalEvent: e.originalEvent, frameState: e.frameState, pixel: e.pixel, coordinate: e.coordinate, map: this.getMap() }; _timeout = setTimeout(function () { ltouch(event); event.map.dispatchEvent(event); }, this.delay_); break; } case 'pointerdrag': { // Check if dragging over tolerance if (_timeout && (Math.abs(e.pixel[0] - position[0]) > tol || Math.abs(e.pixel[1] - position[1]) > tol)) { clearTimeout(_timeout); _timeout = null; } break; } case 'pointerup': { if (_timeout) { clearTimeout(_timeout); _timeout = null; } break; } default: break; } } else { if (_timeout) { clearTimeout(_timeout); _timeout = null; } } return true; } }); this.delay_ = options.delay || 1000; } } // Use ol.getUid for Openlayers < v6 /** Extent the ol/interaction/Modify with a getModifyFeatures * Get the features modified by the interaction * @return {Array} the modified features * @deprecated */ ol.interaction.Modify.prototype.getModifiedFeatures = function() { var featuresById = {}; this.dragSegments_.forEach( function(s) { var feature = s[0].feature; // Openlayers > v.6 if (window && window.ol && window.ol.util) featuresById[ol.util.getUid(feature)] = feature; // old version of Openlayers (< v.6) or ol all versions else featuresById[ol.getUid(feature)] = feature; }); var features = []; for (var i in featuresById) features.push(featuresById[i]); return features; }; /* 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). */ /** Interaction for modifying feature geometries. Similar to the core ol/interaction/Modify. * The interaction is more suitable to use to handle feature modification: only features concerned * by the modification are passed to the events (instead of all feature with ol/interaction/Modify) * - the modifystart event is fired before the feature is modified (no points still inserted) * - the modifyend event is fired after the modification * - it fires a modifying event * @constructor * @extends {ol.interaction.Interaction} * @fires modifystart * @fires modifying * @fires modifyend * @fires select * @param {*} options * @param {ol.source.Vector} options.source a source to modify (configured with useSpatialIndex set to true) * @param {ol.source.Vector|Array} options.sources a list of source to modify (configured with useSpatialIndex set to true) * @param {ol.Collection.} options.features collection of feature to modify * @param {integer} options.pixelTolerance Pixel tolerance for considering the pointer close enough to a segment or vertex for editing. Default is 10. * @param {function|undefined} options.filter a filter that takes a feature and return true if it can be modified, default always true. * @param {ol.style.Style | Array | undefined} options.style Style for the sketch features. * @param {ol.EventsConditionType | undefined} options.condition A function that takes an ol.MapBrowserEvent and returns a boolean to indicate whether that event will be considered to add or move a vertex to the sketch. Default is ol.events.condition.primaryAction. * @param {ol.EventsConditionType | undefined} options.deleteCondition A function that takes an ol.MapBrowserEvent and returns a boolean to indicate whether that event should be handled. By default, ol.events.condition.singleClick with ol.events.condition.altKeyOnly results in a vertex deletion. * @param {ol.EventsConditionType | undefined} options.insertVertexCondition A function that takes an ol.MapBrowserEvent and returns a boolean to indicate whether a new vertex can be added to the sketch features. Default is ol.events.condition.always * @param {boolean} options.wrapX Wrap the world horizontally on the sketch overlay, default false */ ol.interaction.ModifyFeature = class olinteractionModifyFeature extends ol.interaction.Interaction { constructor(options) { options = options || {} var dragging, modifying super({ handleEvent: function (e) { switch (e.type) { case 'pointerdown': { dragging = this.handleDownEvent(e) modifying = dragging || this._deleteCondition(e) return !dragging } case 'pointerup': { dragging = false return this.handleUpEvent(e) } case 'pointerdrag': { if (dragging) return this.handleDragEvent(e) else return true } case 'pointermove': { if (!dragging){ return this.handleMoveEvent(e) } else { return false } } case 'singleclick': case 'click': { // Prevent click when modifying return !modifying } default: return true } } }) // Snap distance (in px) this.snapDistance_ = options.pixelTolerance || 10 // Split tolerance between the calculated intersection and the geometry this.tolerance_ = 1e-10 // Cursor this.cursor_ = options.cursor // List of source to split this.sources_ = options.sources ? (options.sources instanceof Array) ? options.sources : [options.sources] : [] if (options.source) { this.sources_.push(options.source) } if (options.features) { this.sources_.push(new ol.source.Vector({ features: options.features })) } // Get all features candidate this.filterSplit_ = options.filter || function () { return true } this._condition = options.condition || ol.events.condition.primaryAction this._deleteCondition = options.deleteCondition || ol.events.condition.altKeyOnly this._insertVertexCondition = options.insertVertexCondition || ol.events.condition.always // Default style var sketchStyle = function () { return [new ol.style.Style({ image: new ol.style.Circle({ radius: 6, fill: new ol.style.Fill({ color: [0, 153, 255, 1] }), stroke: new ol.style.Stroke({ color: '#FFF', width: 1.25 }) }) }) ] } // Custom style if (options.style) { if (typeof (options.style) === 'function') { sketchStyle = options.style } else { sketchStyle = function () { return options.style } } } // Create a new overlay for the sketch this.overlayLayer_ = new ol.layer.Vector({ source: new ol.source.Vector({ useSpatialIndex: false }), name: 'Modify overlay', displayInLayerSwitcher: false, style: sketchStyle, wrapX: options.wrapX }) } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_) super.setMap(map) this.overlayLayer_.setMap(map) } /** * Activate or deactivate the interaction + remove the sketch. * @param {boolean} active. * @api stable */ setActive(active) { super.setActive(active) if (this.overlayLayer_) this.overlayLayer_.getSource().clear() } /** Change the filter function * @param {function|undefined} options.filter a filter that takes a feature and return true if it can be modified, default always true. */ setFilter(filter) { if (typeof (filter) === 'function') this.filterSplit_ = filter else if (filter === undefined) this.filterSplit_ = function () { return true } } /** Get closest feature at pixel * @param {ol.Pixel} * @return {*} * @private */ getClosestFeature(e) { var f, c, d = this.snapDistance_ + 1 for (var i = 0; i < this.sources_.length; i++) { var source = this.sources_[i] f = source.getClosestFeatureToCoordinate(e.coordinate) if (f && this.filterSplit_(f)) { var ci = f.getGeometry().getClosestPoint(e.coordinate) var di = ol.coordinate.dist2d(e.coordinate, ci) / e.frameState.viewState.resolution if (di < d) { d = di c = ci } break } } if (d > this.snapDistance_) { if (this.currentFeature) this.dispatchEvent({ type: 'select', selected: [], deselected: [this.currentFeature] }) this.currentFeature = null return false } else { // Snap to node var coord = this.getNearestCoord(c, f.getGeometry()) if (coord) { coord = coord.coord var p = this.getMap().getPixelFromCoordinate(coord) if (ol.coordinate.dist2d(e.pixel, p) < this.snapDistance_) { c = coord } // if (this.currentFeature !== f) this.dispatchEvent({ type: 'select', selected: [f], deselected: [this.currentFeature] }) this.currentFeature = f return { source: source, feature: f, coord: c } } } } /** Get nearest coordinate in a list * @param {ol.coordinate} pt the point to find nearest * @param {ol.geom} coords list of coordinates * @return {*} the nearest point with a coord (projected point), dist (distance to the geom), ring (if Polygon) */ getNearestCoord(pt, geom) { var i, l, p, p0, dm switch (geom.getType()) { case 'Point': { return { coord: geom.getCoordinates(), dist: ol.coordinate.dist2d(geom.getCoordinates(), pt) } } case 'MultiPoint': { return this.getNearestCoord(pt, new ol.geom.LineString(geom.getCoordinates())) } case 'LineString': case 'LinearRing': { var d dm = Number.MAX_VALUE var coords = geom.getCoordinates() for (i = 0; i < coords.length; i++) { d = ol.coordinate.dist2d(pt, coords[i]) if (d < dm) { dm = d p0 = coords[i] } } return { coord: p0, dist: dm } } case 'MultiLineString': { var lstring = geom.getLineStrings() p0 = false, dm = Number.MAX_VALUE for (i = 0; l = lstring[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.ring = i } } return p0 } case 'Polygon': { var lring = geom.getLinearRings() p0 = false dm = Number.MAX_VALUE for (i = 0; l = lring[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.ring = i } } return p0 } case 'MultiPolygon': { var poly = geom.getPolygons() p0 = false dm = Number.MAX_VALUE for (i = 0; l = poly[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.poly = i } } return p0 } case 'GeometryCollection': { var g = geom.getGeometries() p0 = false dm = Number.MAX_VALUE for (i = 0; l = g[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.geom = i } } return p0 } default: return false } } /** Get arcs concerned by a modification * @param {ol.geom} geom the geometry concerned * @param {ol.coordinate} coord pointed coordinates */ getArcs(geom, coord) { var arcs = false var coords, i, s, l, g switch (geom.getType()) { case 'Point': { if (ol.coordinate.equal(coord, geom.getCoordinates())) { arcs = { geom: geom, type: geom.getType(), coord1: [], coord2: [], node: true } } break } case 'MultiPoint': { coords = geom.getCoordinates() for (i = 0; i < coords.length; i++) { if (ol.coordinate.equal(coord, coords[i])) { arcs = { geom: geom, type: geom.getType(), index: i, coord1: [], coord2: [], node: true } break } } break } case 'LinearRing': case 'LineString': { var p = geom.getClosestPoint(coord) if (ol.coordinate.dist2d(p, coord) < 1.5 * this.tolerance_) { var split // Split the line in two if (geom.getType() === 'LinearRing') { g = new ol.geom.LineString(geom.getCoordinates()) split = g.splitAt(coord, this.tolerance_) } else { split = geom.splitAt(coord, this.tolerance_) } // If more than 2 if (split.length > 2) { coords = split[1].getCoordinates() for (i = 2; s = split[i]; i++) { var c = s.getCoordinates() c.shift() coords = coords.concat(c) } split = [split[0], new ol.geom.LineString(coords)] } // Split in two if (split.length === 2) { var c0 = split[0].getCoordinates() var c1 = split[1].getCoordinates() var nbpt = c0.length + c1.length - 1 c0.pop() c1.shift() arcs = { geom: geom, type: geom.getType(), coord1: c0, coord2: c1, node: (geom.getCoordinates().length === nbpt), closed: false } } else if (split.length === 1) { s = split[0].getCoordinates() var start = ol.coordinate.equal(s[0], coord) var end = ol.coordinate.equal(s[s.length - 1], coord) // Move first point if (start) { s.shift() if (end) s.pop() arcs = { geom: geom, type: geom.getType(), coord1: [], coord2: s, node: true, closed: end } } else if (end) { // Move last point s.pop() arcs = { geom: geom, type: geom.getType(), coord1: s, coord2: [], node: true, closed: false } } } } break } case 'MultiLineString': { var lstring = geom.getLineStrings() for (i = 0; l = lstring[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.type = geom.getType() arcs.lstring = i break } } break } case 'Polygon': { var lring = geom.getLinearRings() for (i = 0; l = lring[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.type = geom.getType() arcs.index = i break } } break } case 'MultiPolygon': { var poly = geom.getPolygons() for (i = 0; l = poly[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.type = geom.getType() arcs.poly = i break } } break } case 'GeometryCollection': { g = geom.getGeometries() for (i = 0; l = g[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.g = i arcs.typeg = arcs.type arcs.type = geom.getType() break } } break } default: { console.error('ol/interaction/ModifyFeature ' + geom.getType() + ' not supported!') break } } return arcs } /** * @param {ol.MapBrowserEvent} evt Map browser event. * @return {boolean} `true` to start the drag sequence. */ handleDownEvent(evt) { if (!this.getActive()) return false // Something to move ? var current = this.getClosestFeature(evt) if (current && (this._condition(evt) || this._deleteCondition(evt))) { var features = [] this.arcs = [] // Get features concerned this.sources_.forEach(function (s) { var extent = ol.extent.buffer(ol.extent.boundingExtent([current.coord]), this.tolerance_) features = features.concat(features, s.getFeaturesInExtent(extent)) }.bind(this)) // Get arcs concerned this._modifiedFeatures = [] features.forEach(function (f) { var a = this.getArcs(f.getGeometry(), current.coord) if (a) { if (this._insertVertexCondition(evt) || a.node) { a.feature = f this._modifiedFeatures.push(f) this.arcs.push(a) } } }.bind(this)) if (this._modifiedFeatures.length) { if (this._deleteCondition(evt)) { return !this._removePoint(current, evt) } else { this.dispatchEvent({ type: 'modifystart', coordinate: current.coord, originalEvent: evt.originalEvent, features: this._modifiedFeatures }) this.handleDragEvent({ coordinate: current.coord, originalEvent: evt.originalEvent }) return true } } else { return true } } else { return false } } /** Get modified features * @return {Array} list of modified features */ getModifiedFeatures() { return this._modifiedFeatures || [] } /** Removes the vertex currently being pointed. */ removePoint() { this._removePoint({}, {}) } /** * @private */ _getModification(a) { var coords = a.coord1.concat(a.coord2) switch (a.type) { case 'LineString': { if (a.closed) coords.push(coords[0]) if (coords.length > 1) { if (a.geom.getCoordinates().length != coords.length) { a.coords = coords return true } } break } case 'MultiLineString': { if (a.closed) coords.push(coords[0]) if (coords.length > 1) { var c = a.geom.getCoordinates() if (c[a.lstring].length != coords.length) { c[a.lstring] = coords a.coords = c return true } } break } case 'Polygon': { if (a.closed) coords.push(coords[0]) if (coords.length > 3) { c = a.geom.getCoordinates() if (c[a.index].length != coords.length) { c[a.index] = coords a.coords = c return true } } break } case 'MultiPolygon': { if (a.closed) coords.push(coords[0]) if (coords.length > 3) { c = a.geom.getCoordinates() if (c[a.poly][a.index].length != coords.length) { c[a.poly][a.index] = coords a.coords = c return true } } break } case 'GeometryCollection': { a.type = a.typeg var geom = a.geom var geoms = geom.getGeometries() a.geom = geoms[a.g] var found = this._getModification(a) // Restore current arc geom.setGeometries(geoms) a.geom = geom a.type = 'GeometryCollection' return found } default: { //console.error('ol/interaction/ModifyFeature '+a.type+' not supported!'); break } } return false } /** Removes the vertex currently being pointed. * @private */ _removePoint(current, evt) { if (!this.arcs) return false this.overlayLayer_.getSource().clear() var found = false // Get all modifications this.arcs.forEach(function (a) { found = found || this._getModification(a) }.bind(this)) // Almost one point is removed if (found) { this.dispatchEvent({ type: 'modifystart', coordinate: current.coord, originalEvent: evt.originalEvent, features: this._modifiedFeatures }) this.arcs.forEach(function (a) { if (a.geom.getType() === 'GeometryCollection') { if (a.coords) { var geoms = a.geom.getGeometries() geoms[a.g].setCoordinates(a.coords) a.geom.setGeometries(geoms) } } else { if (a.coords) a.geom.setCoordinates(a.coords) } }.bind(this)) this.dispatchEvent({ type: 'modifyend', coordinate: current.coord, originalEvent: evt.originalEvent, features: this._modifiedFeatures }) } this.arcs = [] return found } /** * @private */ handleUpEvent(e) { if (!this.getActive()) return false if (!this.arcs || !this.arcs.length) return true this.overlayLayer_.getSource().clear() this.dispatchEvent({ type: 'modifyend', coordinate: e.coordinate, originalEvent: e.originalEvent, features: this._modifiedFeatures }) this.arcs = [] return true } /** * @private */ setArcCoordinates(a, coords) { var c switch (a.type) { case 'Point': { a.geom.setCoordinates(coords[0]) break } case 'MultiPoint': { c = a.geom.getCoordinates() c[a.index] = coords[0] a.geom.setCoordinates(c) break } case 'LineString': { a.geom.setCoordinates(coords) break } case 'MultiLineString': { c = a.geom.getCoordinates() c[a.lstring] = coords a.geom.setCoordinates(c) break } case 'Polygon': { c = a.geom.getCoordinates() c[a.index] = coords a.geom.setCoordinates(c) break } case 'MultiPolygon': { c = a.geom.getCoordinates() c[a.poly][a.index] = coords a.geom.setCoordinates(c) break } case 'GeometryCollection': { a.type = a.typeg var geom = a.geom var geoms = geom.getGeometries() a.geom = geoms[a.g] this.setArcCoordinates(a, coords) geom.setGeometries(geoms) a.geom = geom a.type = 'GeometryCollection' break } } } /** * @private */ handleDragEvent(e) { if (!this.getActive()) return false if (!this.arcs) return true // Show sketch this.overlayLayer_.getSource().clear() var p = new ol.Feature(new ol.geom.Point(e.coordinate)) this.overlayLayer_.getSource().addFeature(p) // Nothing to do if (!this.arcs.length) return true // Move arcs this.arcs.forEach(function (a) { var coords = a.coord1.concat([e.coordinate], a.coord2) if (a.closed) coords.push(e.coordinate) this.setArcCoordinates(a, coords) }.bind(this)) this.dispatchEvent({ type: 'modifying', coordinate: e.coordinate, originalEvent: e.originalEvent, features: this._modifiedFeatures }) return true } /** * @param {ol.MapBrowserEvent} evt Event. * @private */ handleMoveEvent(e) { if (!this.getActive()) return true this.overlayLayer_.getSource().clear() var current = this.getClosestFeature(e) // Draw sketch if (current) { var p = new ol.Feature(new ol.geom.Point(current.coord)) this.overlayLayer_.getSource().addFeature(p) } // Show cursor var element = e.map.getTargetElement() if (this.cursor_) { if (current) { if (element.style.cursor != this.cursor_) { this.previousCursor_ = element.style.cursor element.style.cursor = this.cursor_ } } else if (this.previousCursor_ !== undefined) { element.style.cursor = this.previousCursor_ this.previousCursor_ = undefined } } return true } /** Get the current feature to modify * @return {ol.Feature} */ getCurrentFeature() { return this.currentFeature } } /* 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). */ /** Modify interaction with a popup to delet a point on touch device * @constructor * @fires showpopup * @fires hidepopup * @extends {ol.interaction.Modify} * @param {olx.interaction.ModifyOptions} options * @param {String|undefined} options.title title to display, default "remove point" * @param {String|undefined} options.className CSS class name for the popup * @param {String|undefined} options.positioning positioning for the popup * @param {Number|Array|undefined} options.offsetBox offset box for the popup * @param {Boolean|undefined} options.usePopup use a popup, default true */ ol.interaction.ModifyTouch = class olinteractionModifyTouch extends ol.interaction.Modify { constructor(options) { options = options || {}; // Check if there is a feature to select var pixelTolerance = options.pixelTolerance || 0; var searchDist = pixelTolerance + 5; options.condition = function (e) { var features = this.getMap().getFeaturesAtPixel(e.pixel, { hitTolerance: searchDist }); var p0, p1, found = false; if (features) { var search = this._features; if (!search) { p0 = [e.pixel[0] - searchDist, e.pixel[1] - searchDist]; p1 = [e.pixel[0] + searchDist, e.pixel[1] + searchDist]; p0 = this.getMap().getCoordinateFromPixel(p0); p1 = this.getMap().getCoordinateFromPixel(p1); var ext = ol.extent.boundingExtent([p0, p1]); search = this._source.getFeaturesInExtent(ext); } if (search.getArray) search = search.getArray(); for (var i = 0, f; f = features[i]; i++) { if (search.indexOf(f) >= 0) break; } if (f) { p0 = e.pixel; p1 = f.getGeometry().getClosestPoint(e.coordinate); p1 = this.getMap().getPixelFromCoordinate(p1); var dx = p0[0] - p1[0]; var dy = p0[1] - p1[1]; found = (Math.sqrt(dx * dx + dy * dy) < searchDist); } } // Show popup if any this.showDeleteBt(found ? { type: 'show', feature: f, coordinate: e.coordinate } : { type: 'hide' }); return true; }; // Hide popup on insert options.insertVertexCondition = function () { this.showDeleteBt({ type: 'hide' }); return true; }; super(options); this._popup = new ol.Overlay.Popup({ popupClass: options.className || 'modifytouch', positioning: options.positioning || 'bottom-rigth', offsetBox: options.offsetBox || 10 }); this._source = options.source; this._features = options.features; // popup content var a = document.createElement('a'); a.appendChild(document.createTextNode(options.title || "remove point")); a.onclick = function () { this.removePoint(); }.bind(this); this.setPopupContent(a); this.on(['modifystart', 'modifyend'], function () { this.showDeleteBt({ type: 'hide', modifying: true }); }); // Use a popup ? this.set('usePopup', options.usePopup !== false); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) { this.getMap().removeOverlay(this._popup); } super.setMap(map); if (this.getMap()) { this.getMap().addOverlay(this._popup); } } /** Activate the interaction and remove popup * @param {Boolean} b */ setActive(b) { super.setActive(b); this.showDeleteBt({ type: 'hide' }); } /** * Remove the current point */ removePoint() { // Prevent touch + click on popup if (new Date() - this._timeout < 200) return; // Remove point super.removePoint(); this.showDeleteBt({ type: 'hide' }); } /** * Show the delete button (menu) * @param {Event} e * @api stable */ showDeleteBt(e) { if (!this._popup) return; if (this.get('usePopup') && e.type === 'show') { this._popup.show(e.coordinate, this._menu); } else { this._popup.hide(); } e.type += 'popup'; this.dispatchEvent(e); // Date if popup start a timeout to prevent touch + click on the popup this._timeout = new Date(); } /** * Change the popup content * @param {DOMElement} html */ setPopupContent(html) { this._menu = html; } /** * Get the popup content * @return {DOMElement} */ getPopupContent() { return this._menu; } } /* 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). */ /** Offset interaction for offseting feature geometry * @constructor * @extends {ol.interaction.Pointer} * @fires offsetstart * @fires offsetting * @fires offsetend * @param {any} options * @param {function} [options.filter] a function that takes a feature and a layer and return true if the feature can be modified * @param {ol.layer.Vector | Array} options.layers list of feature to transform * @param {ol.Collection.} options.features collection of feature to transform * @param {ol.source.Vector | undefined} options.source source to duplicate feature when ctrl key is down * @param {boolean} options.duplicate force feature to duplicate (source must be set) * @param {ol.style.Style | Array. | ol.style.StyleFunction | undefined} style style for the sketch */ ol.interaction.Offset = class olinteractionOffset extends ol.interaction.Pointer { constructor(options) { options = options || {}; // Extend pointer super({ handleDownEvent: function(e) { return self.handleDownEvent_(e) }, handleDragEvent: function(e) { return self.handleDragEvent_(e) }, handleMoveEvent: function(e) { return self.handleMoveEvent_(e) }, handleUpEvent: function(e) { return self.handleUpEvent_(e) }, }); var self = this; this._filter = options.filter; // Collection of feature to transform this.features_ = options.features; // List of layers to transform this.layers_ = options.layers ? (options.layers instanceof Array) ? options.layers : [options.layers] : null; // duplicate this.set('duplicate', options.duplicate); this.source_ = options.source; // Style this._style = (typeof (options.style) === 'function') ? options.style : function () { if (options.style) return options.style; else return ol.style.Style.defaultStyle(true); }; // init this.previousCursor_ = false; } /** Get Feature at pixel * @param {ol.MapBrowserEvent} evt Map browser event. * @return {any} a feature and the hit point * @private */ getFeatureAtPixel_(e) { var self = this; return this.getMap().forEachFeatureAtPixel(e.pixel, function (feature, layer) { var current; if (self._filter && !self._filter(feature, layer)) return false; // feature belong to a layer if (self.layers_) { for (var i = 0; i < self.layers_.length; i++) { if (self.layers_[i] === layer) { current = feature; break; } } } // feature in the collection else if (self.features_) { self.features_.forEach(function (f) { if (f === feature) { current = feature; } }); } // Others else { current = feature; } // Only poygon or linestring var typeGeom = current.getGeometry().getType(); if (current && /Polygon|LineString/.test(typeGeom)) { if (typeGeom === 'Polygon' && current.getGeometry().getCoordinates().length > 1) return false; // test distance var p = current.getGeometry().getClosestPoint(e.coordinate); var dx = p[0] - e.coordinate[0]; var dy = p[1] - e.coordinate[1]; var d = Math.sqrt(dx * dx + dy * dy) / e.frameState.viewState.resolution; if (d < 5) { return { feature: current, hit: p, coordinates: current.getGeometry().getCoordinates(), geom: current.getGeometry().clone(), geomType: typeGeom }; } else { return false; } } else { return false; } }, { hitTolerance: 5 }); } /** * @param {ol.MapBrowserEvent} e Map browser event. * @return {boolean} `true` to start the drag sequence. * @private */ handleDownEvent_(e) { this.current_ = this.getFeatureAtPixel_(e); if (this.current_) { this.currentStyle_ = this.current_.feature.getStyle(); if (this.source_ && (this.get('duplicate') || e.originalEvent.ctrlKey)) { this.current_.feature = this.current_.feature.clone(); this.current_.feature.setStyle(this._style(this.current_.feature)); this.source_.addFeature(this.current_.feature); } else { // Modify the current feature this.current_.feature.setStyle(this._style(this.current_.feature)); this._modifystart = true; } this.dispatchEvent({ type: 'offsetstart', feature: this.current_.feature, offset: 0 }); return true; } else { return false; } } /** * @param {ol.MapBrowserEvent} e Map browser event. * @private */ handleDragEvent_(e) { if (this._modifystart) { this.dispatchEvent({ type: 'modifystart', features: [this.current_.feature] }); this._modifystart = false; } var p = this.current_.geom.getClosestPoint(e.coordinate); var d = ol.coordinate.dist2d(p, e.coordinate); var seg, v1, v2, offset; switch (this.current_.geomType) { case 'Polygon': { seg = ol.coordinate.findSegment(p, this.current_.coordinates[0]).segment; if (seg) { v1 = [seg[1][0] - seg[0][0], seg[1][1] - seg[0][1]]; v2 = [e.coordinate[0] - p[0], e.coordinate[1] - p[1]]; if (v1[0] * v2[1] - v1[1] * v2[0] > 0) { d = -d; } offset = []; for (var i = 0; i < this.current_.coordinates.length; i++) { offset.push(ol.coordinate.offsetCoords(this.current_.coordinates[i], i == 0 ? d : -d)); } this.current_.feature.setGeometry(new ol.geom.Polygon(offset)); } break; } case 'LineString': { seg = ol.coordinate.findSegment(p, this.current_.coordinates).segment; if (seg) { v1 = [seg[1][0] - seg[0][0], seg[1][1] - seg[0][1]]; v2 = [e.coordinate[0] - p[0], e.coordinate[1] - p[1]]; if (v1[0] * v2[1] - v1[1] * v2[0] > 0) { d = -d; } offset = ol.coordinate.offsetCoords(this.current_.coordinates, d); this.current_.feature.setGeometry(new ol.geom.LineString(offset)); } break; } default: { break; } } this.dispatchEvent({ type: 'offsetting', feature: this.current_.feature, offset: d, segment: [p, e.coordinate], coordinate: e.coordinate }); } /** * @param {ol.MapBrowserEvent} e Map browser event. * @private */ handleUpEvent_(e) { if (!this._modifystart) { this.dispatchEvent({ type: 'offsetend', feature: this.current_.feature, coordinate: e.coordinate }); } this.current_.feature.setStyle(this.currentStyle_); this.current_ = false; } /** * @param {ol.MapBrowserEvent} e Event. * @private */ handleMoveEvent_(e) { var f = this.getFeatureAtPixel_(e); if (f) { if (this.previousCursor_ === false) { this.previousCursor_ = e.map.getTargetElement().style.cursor; } e.map.getTargetElement().style.cursor = 'pointer'; } else { e.map.getTargetElement().style.cursor = this.previousCursor_; this.previousCursor_ = false; } } } /* Water ripple effect. Original code (Java) by Neil Wallis @link http://www.neilwallis.com/java/water.html Original code (JS) by Sergey Chikuyonok (serge.che@gmail.com) @link http://chikuyonok.ru @link http://media.chikuyonok.ru/ripple/ Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). @link https://github.com/Viglino */ /** * @constructor * @extends {ol.interaction.Pointer} * @param {*} options * @param {ol/layer/Layer} options.layer layer to animate * @param {number} options.radius raindrop radius * @param {number} options.interval raindrop interval (in ms), default 1000 */ ol.interaction.Ripple = class olinteractionRipple extends ol.interaction.Pointer { constructor(options) { super({ handleDownEvent: function(e) { return this.rainDrop(e) }, handleMoveEvent: function(e) { return this.rainDrop(e) }, }); // Default options options = options || {}; this.riprad = options.radius || 3; this.ripplemap = []; this.last_map = []; // Generate random ripples this.rains(this.interval); options.layer.on(['postcompose', 'postrender'], this.postcompose_.bind(this)); } /** Generate random rain drop * @param {integer} interval */ rains(interval) { if (this.onrain) clearTimeout(this.onrain); var self = this; var vdelay = (typeof (interval) == "number" ? interval : 1000) / 2; var delay = 3 * vdelay / 2; var rnd = Math.random; function rain() { if (self.width) self.rainDrop([rnd() * self.width, rnd() * self.height]); self.onrain = setTimeout(rain, rnd() * vdelay + delay); } // Start raining if (delay) rain(); } /** Disturb water at specified point * @param {ol.Pixel|ol.MapBrowserEvent} */ rainDrop(e) { if (!this.width) return; var dx, dy; if (e.pixel) { dx = e.pixel[0] * this.ratio; dy = e.pixel[1] * this.ratio; } else { dx = e[0] * this.ratio; dy = e[1] * this.ratio; } dx <<= 0; dy <<= 0; for (var j = dy - this.riprad * this.ratio; j < dy + this.riprad * this.ratio; j++) { for (var k = dx - this.riprad * this.ratio; k < dx + this.riprad * this.ratio; k++) { this.ripplemap[this.oldind + (j * this.width) + k] += 128; } } } /** Postcompose function */ postcompose_(e) { var ctx = e.context; var canvas = ctx.canvas; // Initialize when canvas is ready / modified if (this.width != canvas.width || this.height != canvas.height) { this.width = canvas.width; this.height = canvas.height; this.ratio = e.frameState.pixelRatio; this.half_width = this.width >> 1; this.half_height = this.height >> 1; this.size = this.width * (this.height + 2) * 2; this.oldind = this.width; this.newind = this.width * (this.height + 3); for (var i = 0; i < this.size; i++) { this.last_map[i] = this.ripplemap[i] = 0; } } this.texture = ctx.getImageData(0, 0, this.width, this.height); this.ripple = ctx.getImageData(0, 0, this.width, this.height); // Run animation var a, b, data, cur_pixel, new_pixel; var t = this.oldind; this.oldind = this.newind; this.newind = t; i = 0; var _rd = this.ripple.data, _td = this.texture.data; for (var y = 0; y < this.height; y++) { for (var x = 0; x < this.width; x++) { var _newind = this.newind + i, _mapind = this.oldind + i; data = ( this.ripplemap[_mapind - this.width] + this.ripplemap[_mapind + this.width] + this.ripplemap[_mapind - 1] + this.ripplemap[_mapind + 1]) >> 1; data -= this.ripplemap[_newind]; data -= data >> 5; this.ripplemap[_newind] = data; //where data=0 then still, where data>0 then wave data = 1024 - data; if (this.last_map[i] != data) { this.last_map[i] = data; //offsets a = (((x - this.half_width) * data / 1024) << 0) + this.half_width; b = (((y - this.half_height) * data / 1024) << 0) + this.half_height; //bounds check if (a >= this.width) a = this.width - 1; if (a < 0) a = 0; if (b >= this.height) b = this.height - 1; if (b < 0) b = 0; new_pixel = (a + (b * this.width)) * 4; cur_pixel = i * 4; /**/ _rd[cur_pixel] = _td[new_pixel]; _rd[cur_pixel + 1] = _td[new_pixel + 1]; _rd[cur_pixel + 2] = _td[new_pixel + 2]; /*/ // only in blue pixels if (_td[new_pixel + 2]>_td[new_pixel + 1] && _td[new_pixel + 2]>_td[new_pixel]) { _rd[cur_pixel] = _td[new_pixel]; _rd[cur_pixel + 1] = _td[new_pixel + 1]; _rd[cur_pixel + 2] = _td[new_pixel + 2]; } else this.ripplemap[_newind] = 0; /**/ } ++i; } } ctx.putImageData(this.ripple, 0, 0); // tell OL3 to continue postcompose animation this.getMap().render(); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (http://www.cecill.info/). ol/interaction/SelectCluster is an interaction for selecting vector features in a cluster. */ /** * @classdesc * Interaction for selecting vector features in a cluster. * It can be used as an ol.interaction.Select. * When clicking on a cluster, it springs apart to reveal the features in the cluster. * Revealed features are selectable and you can pick the one you meant. * Revealed features are themselves a cluster with an attribute features that contain the original feature. * * @constructor * @extends {ol.interaction.Select} * @param {olx.interaction.SelectOptions=} options SelectOptions. * @param {ol.style} options.featureStyle used to style the revealed features as options.style is used by the Select interaction. * @param {boolean} options.selectCluster false if you don't want to get cluster selected * @param {Number} options.pointRadius to calculate distance between the features * @param {bool} options.spiral means you want the feature to be placed on a spiral (or a circle) * @param {Number} options.circleMaxObjects number of object that can be place on a circle * @param {Number} options.maxObjects number of object that can be drawn, other are hidden * @param {bool} options.animate if the cluster will animate when features spread out, default is false * @param {Number} options.animationDuration animation duration in ms, default is 500ms * @param {boolean} options.autoClose if selecting a cluster should close previously selected clusters. False to get toggle feature. Default is true * @fires ol.interaction.SelectEvent * @api stable */ ol.interaction.SelectCluster = class olinteractionSelectCluster extends ol.interaction.Select { constructor(options) { options = options || {} // Create a new overlay layer for var overlay = new ol.layer.Vector({ source: new ol.source.Vector({ features: new ol.Collection(), wrapX: options.wrapX, useSpatialIndex: true }), name: 'Cluster overlay', updateWhileAnimating: true, updateWhileInteracting: true, displayInLayerSwitcher: false, style: options.featureStyle }) // Add the overlay to selection if (options.layers) { if (typeof (options.layers) == "function") { var fnLayers = options.layers options.layers = function (layer) { return (layer === overlay || fnLayers(layer)) } } else if (options.layers.push) { options.layers.push(overlay) } } // Don't select links if (options.filter) { var fnFilter = options.filter options.filter = function (f, l) { //if (l===overlay && f.get("selectclusterlink")) return false; if (!l && f.get("selectclusterlink")) return false else return fnFilter(f, l) } } else options.filter = function (f, l) { //if (l===overlay && f.get("selectclusterlink")) return false; if (!l && f.get("selectclusterlink")) return false else return true } if ((options.autoClose === false) && !options.toggleCondition) { options.toggleCondition = ol.events.condition.singleClick } super(options) this.overlayLayer_ = overlay; this.filter_ = options.filter this.pointRadius = options.pointRadius || 12 this.circleMaxObjects = options.circleMaxObjects || 10 this.maxObjects = options.maxObjects || 60 this.spiral = (options.spiral !== false) this.animate = options.animate this.animationDuration = options.animationDuration || 500 this.selectCluster_ = (options.selectCluster !== false) this._autoClose = (options.autoClose !== false) this.on("select", this.selectCluster.bind(this)) } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) { this.getMap().removeLayer(this.overlayLayer_) } if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null super.setMap(map) this.overlayLayer_.setMap(map) // map.addLayer(this.overlayLayer_); if (map && map.getView()) { this._listener = map.getView().on('change:resolution', this.clear.bind(this)) } } /** * Clear the selection, close the cluster and remove revealed features * @api stable */ clear() { this.getFeatures().clear() this.overlayLayer_.getSource().clear() } /** * Get the layer for the revealed features * @api stable */ getLayer() { return this.overlayLayer_ } /** * Select a cluster * @param {ol.SelectEvent | ol.Feature} a cluster feature ie. a feature with a 'features' attribute. * @api stable */ selectCluster(e) { // It's a feature => convert to SelectEvent if (e instanceof ol.Feature) { e = { selected: [e] } } // Nothing selected if (!e.selected.length) { if (this._autoClose) { this.clear() } else { var deselectedFeatures = e.deselected deselectedFeatures.forEach(deselectedFeature => { var selectClusterFeatures = deselectedFeature.get('selectcluserfeatures') if (selectClusterFeatures) { selectClusterFeatures.forEach(selectClusterFeature => { this.overlayLayer_.getSource().removeFeature(selectClusterFeature) }) } }) } return } // Get selection var feature = e.selected[0] // It's one of ours if (feature.get('selectclusterfeature')) return // Clic out of the cluster => close it var source = this.overlayLayer_.getSource() if (this._autoClose) { source.clear() } var cluster = feature.get('features') // Not a cluster (or just one feature) if (!cluster || cluster.length == 1) return // Remove cluster from selection if (!this.selectCluster_) this.getFeatures().clear() var center = feature.getGeometry().getCoordinates() // Pixel size in map unit var pix = this.getMap().getView().getResolution() var r, a, i, max var p, cf, lk // The features var features = [] // Draw on a circle if (!this.spiral || cluster.length <= this.circleMaxObjects) { max = Math.min(cluster.length, this.circleMaxObjects) r = pix * this.pointRadius * (0.5 + max / 4) for (i = 0; i < max; i++) { a = 2 * Math.PI * i / max if (max == 2 || max == 4) a += Math.PI / 4 p = [center[0] + r * Math.sin(a), center[1] + r * Math.cos(a)] cf = new ol.Feature({ 'selectclusterfeature': true, 'features': [cluster[i]], geometry: new ol.geom.Point(p) }) cf.setStyle(cluster[i].getStyle()) features.push(cf) lk = new ol.Feature({ 'selectclusterlink': true, geometry: new ol.geom.LineString([center, p]) }) features.push(lk) } } // Draw on a spiral else { // Start angle a = 0 var d = 2 * this.pointRadius max = Math.min(this.maxObjects, cluster.length) // Feature on a spiral for (i = 0; i < max; i++) { // New radius => increase d in one turn r = d / 2 + d * a / (2 * Math.PI) // Angle a = a + (d + 0.1) / r var dx = pix * r * Math.sin(a) var dy = pix * r * Math.cos(a) p = [center[0] + dx, center[1] + dy] cf = new ol.Feature({ 'selectclusterfeature': true, 'features': [cluster[i]], geometry: new ol.geom.Point(p) }) cf.setStyle(cluster[i].getStyle()) features.push(cf) lk = new ol.Feature({ 'selectclusterlink': true, geometry: new ol.geom.LineString([center, p]) }) features.push(lk) } } feature.set('selectcluserfeatures', features) if (this.animate) { this.animateCluster_(center, features) } else { source.addFeatures(features) } } /** * Animate the cluster and spread out the features * @param {ol.Coordinates} the center of the cluster */ animateCluster_(center, features) { // Stop animation (if one is running) if (this.listenerKey_) { ol.Observable.unByKey(this.listenerKey_) } // Features to animate // var features = this.overlayLayer_.getSource().getFeatures(); if (!features.length) return var style = this.overlayLayer_.getStyle() var stylefn = (typeof (style) == 'function') ? style : style.length ? function () { return style } : function () { return [style] } var duration = this.animationDuration || 500 var start = new Date().getTime() // Animate function function animate(event) { var vectorContext = event.vectorContext || ol.render.getVectorContext(event) // Retina device var ratio = event.frameState.pixelRatio var res = this.getMap().getView().getResolution() var e = ol.easing.easeOut((event.frameState.time - start) / duration) for (var i = 0, feature; feature = features[i]; i++) if (feature.get('features')) { var pt = feature.getGeometry().getCoordinates() pt[0] = center[0] + e * (pt[0] - center[0]) pt[1] = center[1] + e * (pt[1] - center[1]) var geo = new ol.geom.Point(pt) // Image style var st = stylefn(feature, res) for (var s = 0; s < st.length; s++) { var sc // OL < v4.3 : setImageStyle doesn't check retina var imgs = ol.Map.prototype.getFeaturesAtPixel ? false : st[s].getImage() if (imgs) { sc = imgs.getScale() imgs.setScale(ratio) } // OL3 > v3.14 if (vectorContext.setStyle) { vectorContext.setStyle(st[s]) vectorContext.drawGeometry(geo) } // older version else { vectorContext.setImageStyle(imgs) vectorContext.drawPointGeometry(geo) } if (imgs) imgs.setScale(sc) } } // Stop animation and restore cluster visibility if (e > 1.0) { ol.Observable.unByKey(this.listenerKey_) this.overlayLayer_.getSource().addFeatures(features) this.overlayLayer_.changed() return } // tell OL3 to continue postcompose animation event.frameState.animate = true } // Start a new postcompose animation this.listenerKey_ = this.overlayLayer_.on(['postcompose', 'postrender'], animate.bind(this)) // Start animation with a ghost feature var feature = new ol.Feature(new ol.geom.Point(this.getMap().getView().getCenter())) feature.setStyle(new ol.style.Style({ image: new ol.style.Circle({}) })) this.overlayLayer_.getSource().addFeature(feature) } /** Helper function to get the extent of a cluster * @param {ol.feature} feature * @return {ol.extent|null} the extent or null if extent is empty (no cluster or superimposed points) */ getClusterExtent(feature) { if (!feature.get('features')) return null var extent = ol.extent.createEmpty() feature.get('features').forEach(function (f) { extent = ol.extent.extend(extent, f.getGeometry().getExtent()) }) if (extent[0] === extent[2] && extent[1] === extent[3]) return null return extent } } /* 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). */ /** Interaction to snap to guidelines * @constructor * @extends {ol.interaction.Interaction} * @param {*} options * @param {number | undefined} options.pixelTolerance distance (in px) to snap to a guideline, default 10 px * @param {bool | undefined} options.enableInitialGuides whether to draw initial guidelines based on the maps orientation, default false. * @param {ol.style.Style | Array | undefined} options.style Style for the sektch features. * @param {*} options.vectorClass a vector layer class to create the guides with ol6, use ol/layer/VectorImage using ol6 */ ol.interaction.SnapGuides = class olinteractionSnapGuides extends ol.interaction.Interaction { constructor(options) { options = options || {} // Intersect 2 guides function getIntersectionPoint(d1, d2) { var d1x = d1[1][0] - d1[0][0] var d1y = d1[1][1] - d1[0][1] var d2x = d2[1][0] - d2[0][0] var d2y = d2[1][1] - d2[0][1] var det = d1x * d2y - d1y * d2x if (det != 0) { var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det return [d2[0][0] + k * d2x, d2[0][1] + k * d2y] } else return false } function dist2D(p1, p2) { var dx = p1[0] - p2[0] var dy = p1[1] - p2[1] return Math.sqrt(dx * dx + dy * dy) } // Use snap interaction super({ handleEvent: function (e) { if (this.getActive()) { var features = this.overlaySource_.getFeatures() var prev = null var p = null var res = e.frameState.viewState.resolution for (var i = 0, f; f = features[i]; i++) { var c = f.getGeometry().getClosestPoint(e.coordinate) if (dist2D(c, e.coordinate) / res < this.snapDistance_) { // Intersection on 2 lines if (prev) { var c2 = getIntersectionPoint(prev.getGeometry().getCoordinates(), f.getGeometry().getCoordinates()) if (c2) { if (dist2D(c2, e.coordinate) / res < this.snapDistance_) { p = c2 } } } else { p = c } prev = f } } if (p) e.coordinate = p } return true } }) // Snap distance (in px) this.snapDistance_ = options.pixelTolerance || 10 this.enableInitialGuides_ = options.enableInitialGuides || false // Default style var sketchStyle = [ new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#ffcc33', lineDash: [8, 5], width: 1.25 }) }) ] // Custom style if (options.style) { sketchStyle = options.style instanceof Array ? options.style : [options.style] } // Create a new overlay for the sketch this.overlaySource_ = new ol.source.Vector({ features: new ol.Collection(), useSpatialIndex: false }) // Use ol/layer/VectorImage to render the snap guides as an image to improve performance on rerenderers var vectorClass = options.vectorClass || ol.layer.Vector this.overlayLayer_ = new vectorClass({ // render the snap guides as an image to improve performance on rerenderers renderMode: 'image', source: this.overlaySource_, style: function () { return sketchStyle }, name: 'Snap overlay', displayInLayerSwitcher: false }) this.overlayLayer_.setVisible(this.getActive()); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_) super.setMap(map) this.overlayLayer_.setMap(map) if (map) this.projExtent_ = map.getView().getProjection().getExtent() } /** Activate or deactivate the interaction. * @param {boolean} active */ setActive(active) { if (this.overlayLayer_) this.overlayLayer_.setVisible(active) super.setActive(active) } /** Clear previous added guidelines * @param {Array | undefined} features a list of feature to remove, default remove all feature */ clearGuides(features) { if (!features) { this.overlaySource_.clear() } else { for (var i = 0, f; f = features[i]; i++) { try { this.overlaySource_.removeFeature(f) } catch (e) { /* nothing to to */ } } } } /** Get guidelines * @return {ol.Collection} guidelines features */ getGuides() { return this.overlaySource_.getFeaturesCollection() } /** Add a new guide to snap to * @param {Array} v the direction vector * @return {ol.Feature} feature guide */ addGuide(v, ortho) { if (v) { var map = this.getMap() // Limit extent var extent = map.getView().calculateExtent(map.getSize()) var guideLength = Math.max( this.projExtent_[2] - this.projExtent_[0], this.projExtent_[3] - this.projExtent_[1] ) extent = ol.extent.buffer(extent, guideLength * 1.5) //extent = ol.extent.boundingExtent(extent, this.projExtent_); if (extent[0] < this.projExtent_[0]) extent[0] = this.projExtent_[0] if (extent[1] < this.projExtent_[1]) extent[1] = this.projExtent_[1] if (extent[2] > this.projExtent_[2]) extent[2] = this.projExtent_[2] if (extent[3] > this.projExtent_[3]) extent[3] = this.projExtent_[3] var dx = v[0][0] - v[1][0] var dy = v[0][1] - v[1][1] var d = 1 / Math.sqrt(dx * dx + dy * dy) var generateLine = function (loopDir) { var p, g = [] var loopCond = guideLength * loopDir * 2 for (var i = 0; loopDir > 0 ? i < loopCond : i > loopCond; i += (guideLength * loopDir) / 100) { if (ortho) p = [v[0][0] + dy * d * i, v[0][1] - dx * d * i] else p = [v[0][0] + dx * d * i, v[0][1] + dy * d * i] if (ol.extent.containsCoordinate(extent, p)) g.push(p) else break } return new ol.Feature(new ol.geom.LineString([g[0], g[g.length - 1]])) } var f0 = generateLine(1) var f1 = generateLine(-1) this.overlaySource_.addFeature(f0) this.overlaySource_.addFeature(f1) return [f0, f1] } } /** Add a new orthogonal guide to snap to * @param {Array} v the direction vector * @return {ol.Feature} feature guide */ addOrthoGuide(v) { return this.addGuide(v, true) } /** Listen to draw event to add orthogonal guidelines on the first and last point. * @param {_ol_interaction_Draw_} drawi a draw interaction to listen to * @api */ setDrawInteraction(drawi) { var self = this // Number of points currently drawing var nb = 0 // Current guidelines var features = [] function setGuides(e) { var coord = e.target.getCoordinates() var s = 2 switch (e.target.getType()) { case 'Point': return case 'Polygon': coord = coord[0].slice(0, -1) break default: break } var l = coord.length if (l === s && self.enableInitialGuides_) { var x = coord[0][0] var y = coord[0][1] coord = [[x, y], [x, y - 1]] } if (l != nb && (self.enableInitialGuides_ ? l >= s : l > s)) { self.clearGuides(features) // use try catch to remove a bug on freehand draw... try { var p1 = coord[l - s], p2 = coord[l - s - 1] if (l > s && !(p1[0] === p2[0] && p1[1] === p2[1])) { features = self.addOrthoGuide([coord[l - s], coord[l - s - 1]]) } features = features.concat(self.addGuide([coord[0], coord[1]])) features = features.concat(self.addOrthoGuide([coord[0], coord[1]])) nb = l } catch (e) { /* ok*/ } } } // New drawing drawi.on("drawstart", function (e) { // When geom is changing add a new orthogonal direction e.feature.getGeometry().on("change", setGuides) }) // end drawing / deactivate => clear directions drawi.on(["drawend", "change:active"], function (e) { self.clearGuides(features) if (e.feature) e.feature.getGeometry().un("change", setGuides) nb = 0 features = [] }) } /** Listen to modify event to add orthogonal guidelines relative to the currently dragged point * @param {_ol_interaction_Modify_} modifyi a modify interaction to listen to * @api */ setModifyInteraction(modifyi) { function mod(d, n) { return ((d % n) + n) % n } var self = this // Current guidelines var features = [] function computeGuides(e) { var modifyVertex = e.coordinate if (!modifyVertex) { var selectedVertex = e.target.vertexFeature_ if (!selectedVertex) return modifyVertex = selectedVertex.getGeometry().getCoordinates() } var f = e.target.getModifiedFeatures()[0] var geom = f.getGeometry() var coord = geom.getCoordinates() switch (geom.getType()) { case 'Point': return case 'Polygon': coord = coord[0].slice(0, -1) break default: break } var idx = coord.findIndex(function (c) { return c[0] === modifyVertex[0] && c[1] === modifyVertex[1] }) var l = coord.length self.clearGuides(features) features = self.addOrthoGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]]) features = features.concat(self.addGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]])) features = features.concat(self.addGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]])) features = features.concat(self.addOrthoGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]])) } function setGuides(e) { // This callback is called before ol adds the vertex to the feature, so // defer a moment for openlayers to add the new vertex setTimeout(computeGuides, 0, e) } function drawEnd() { self.clearGuides(features) features = [] } // New drawing modifyi.on('modifystart', setGuides) // end drawing, clear directions modifyi.on('modifyend', drawEnd) } } /* 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). */ /** An interaction to snap on pixel on a layer * The CenterTouch interaction modifies map browser event coordinate and pixel properties to force pointer on the viewport center to any interaction that them. * @constructor * @extends {ol.interaction.Interaction} * @param {olx.interaction.InteractionOptions} options Options * @param {ol.layer.Layer} options.layer layer to snap on */ ol.interaction.SnapLayerPixel = class olinteractionSnapLayerPixel extends ol.interaction.Interaction { constructor(options) { options = options || {}; var radius = options.radius || 8; var size = 2 * radius; super({ handleEvent: function (e) { if (this._layer.getVisible() && this._layer.getOpacity() && ol.events.condition.altKeyOnly(e) && this.getMap()) { var x0 = e.pixel[0] - radius; var y0 = e.pixel[1] - radius; var imgd = this._ctx.getImageData(x0, y0, size, size); var pix = imgd.data; // Loop over each pixel and invert the color. var x, y, xm, ym, max = -1; var t = []; for (x = 0; x < size; x++) { t.push([]); for (y = 0; y < size; y++) { var l = pix[3 + 4 * (x + y * size)]; t[x].push(l > 10 ? l : 0); } } for (x = 1; x < size - 1; x++) { for (y = 1; y < size - 1; y++) { var m = t[x][y + 1] + t[x][y] + t[x][y + 1] + t[x - 1][y - 1] + t[x - 1][y] + t[x - 1][y + 1] + t[x + 1][y - 1] + t[x + 1][y] + t[x + 1][y + 1]; if (m > max) { max = m; xm = x; ym = y; } } } e.pixel = [x0 + xm, y0 + ym]; e.coordinate = this.getMap().getCoordinateFromPixel(e.pixel); /* e.coordinate = this.getMap().getView().getCenter(); e.pixel = this.getMap().getSize(); e.pixel = [ e.pixel[0]/2, e.pixel[1]/2 ]; */ } return true; } }); // Get layer canevas context this._layer = options.layer; this._layer.on(['postcompose', 'postrender'], function (e) { this._ctx = e.context; }.bind(this)); } } /* 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). */ /** Interaction split interaction for splitting feature geometry * @constructor * @extends {ol.interaction.Interaction} * @fires beforesplit, aftersplit, pointermove * @param {*} * @param {ol.source.Vector|Array} [options.sources] a list of source to split (configured with useSpatialIndex set to true), if none use map visible layers. * @param {ol.Collection.} options.features collection of feature to split (instead of a list of sources) * @param {integer} options.snapDistance distance (in px) to snap to an object, default 25px * @param {string|undefined} options.cursor cursor name to display when hovering an objet * @param {function|undefined} options.filter a filter that takes a feature and return true if it can be clipped, default always split. * @param ol.style.Style | Array | false | undefined} options.featureStyle Style for the selected features, choose false if you don't want feature selection. By default the default edit style is used. * @param {ol.style.Style | Array | undefined} options.sketchStyle Style for the sektch features. * @param {function|undefined} options.tolerance Distance between the calculated intersection and a vertex on the source geometry below which the existing vertex will be used for the split. Default is 1e-10. */ ol.interaction.Split = class olinteractionSplit extends ol.interaction.Interaction { constructor(options) { if (!options) options = {} super({ handleEvent: function (e) { switch (e.type) { case "singleclick": return this.handleDownEvent(e) case "pointermove": return this.handleMoveEvent(e) default: return true } //return true; } }) // Snap distance (in px) this.snapDistance_ = options.snapDistance || 25 // Split tolerance between the calculated intersection and the geometry this.tolerance_ = options.tolerance || 1e-10 // Cursor this.cursor_ = options.cursor // List of source to split this.setSources(options.sources) if (options.features) { if (!this.sources_) this.sources_ = []; this.sources_.push(new ol.source.Vector({ features: options.features })) } // Get all features candidate this.filterSplit_ = options.filter || function () { return true } // Default style var white = [255, 255, 255, 1] var blue = [0, 153, 255, 1] var width = 3 var fill = new ol.style.Fill({ color: 'rgba(255,255,255,0.4)' }) var stroke = new ol.style.Stroke({ color: '#3399CC', width: 1.25 }) var sketchStyle = [ new ol.style.Style({ image: new ol.style.Circle({ fill: fill, stroke: stroke, radius: 5 }), fill: fill, stroke: stroke }) ] var featureStyle = [ new ol.style.Style({ stroke: new ol.style.Stroke({ color: white, width: width + 2 }) }), new ol.style.Style({ image: new ol.style.Circle({ radius: 2 * width, fill: new ol.style.Fill({ color: blue }), stroke: new ol.style.Stroke({ color: white, width: width / 2 }) }), stroke: new ol.style.Stroke({ color: blue, width: width }) }), ] // Custom style if (options.sketchStyle) sketchStyle = options.sketchStyle instanceof Array ? options.sketchStyle : [options.sketchStyle] if (options.featureStyle) featureStyle = options.featureStyle instanceof Array ? options.featureStyle : [options.featureStyle] // Create a new overlay for the sketch this.overlayLayer_ = new ol.layer.Vector({ source: new ol.source.Vector({ useSpatialIndex: false }), name: 'Split overlay', displayInLayerSwitcher: false, style: function (f) { if (f._sketch_) return sketchStyle else return featureStyle } }) } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this.getMap()) { this.getMap().removeLayer(this.overlayLayer_) } super.setMap(map) this.overlayLayer_.setMap(map) } /** Get sources to split features in * @return {Array} */ getSources() { if (!this.sources_ && this.getMap()) { var sources = [] var getSources = function (layers) { layers.forEach(function (layer) { if (layer.getVisible()) { if (layer.getSource && layer.getSource() instanceof ol.source.Vector) { sources.unshift(layer.getSource()) } else if (layer.getLayers) { getSources(layer.getLayers()) } } }) } getSources(this.getMap().getLayers()) return sources } return this.sources_ || [] } /** Set sources to split features in * @param {ol.source.Vector|Array|boolean} [sources] if not defined get all map vector sources */ setSources(sources) { this.sources_ = sources ? (sources instanceof Array ? sources || false : [sources]) : false } /** Get closest feature at pixel * @param {ol.Pixel} * @return {ol.feature} * @private */ getClosestFeature(e) { var source, f, c, g, d = this.snapDistance_ + 1 // Look for closest point in the sources this.getSources().forEach(function (si) { var fi = si.getClosestFeatureToCoordinate(e.coordinate) if (fi && fi.getGeometry().splitAt) { var ci = fi.getGeometry().getClosestPoint(e.coordinate) var gi = new ol.geom.LineString([e.coordinate, ci]) var di = gi.getLength() / e.frameState.viewState.resolution if (di < d) { source = si d = di f = fi g = gi c = ci } } }) // Snap ? if (d > this.snapDistance_) { return false } else { // Snap to node var coord = this.getNearestCoord(c, f.getGeometry().getCoordinates()) var p = this.getMap().getPixelFromCoordinate(coord) if (ol.coordinate.dist2d(e.pixel, p) < this.snapDistance_) { c = coord } // return { source: source, feature: f, coord: c, link: g } } } /** Get nearest coordinate in a list * @param {ol.coordinate} pt the point to find nearest * @param {Array} coords list of coordinates * @return {ol.coordinate} the nearest coordinate in the list */ getNearestCoord(pt, coords) { var d, dm = Number.MAX_VALUE, p0 for (var i = 0; i < coords.length; i++) { d = ol.coordinate.dist2d(pt, coords[i]) if (d < dm) { dm = d p0 = coords[i] } } return p0 } /** * @param {ol.MapBrowserEvent} evt Map browser event. * @return {boolean} `true` to start the drag sequence. */ handleDownEvent(evt) { // Something to split ? var current = this.getClosestFeature(evt) if (current) { var self = this self.overlayLayer_.getSource().clear() var split = current.feature.getGeometry().splitAt(current.coord, this.tolerance_) var i if (split.length > 1) { var tosplit = [] for (i = 0; i < split.length; i++) { var f = current.feature.clone() f.setGeometry(split[i]) tosplit.push(f) } self.dispatchEvent({ type: 'beforesplit', original: current.feature, features: tosplit }) current.source.dispatchEvent({ type: 'beforesplit', original: current.feature, features: tosplit }) current.source.removeFeature(current.feature) for (i = 0; i < tosplit.length; i++) { current.source.addFeature(tosplit[i]) } self.dispatchEvent({ type: 'aftersplit', original: current.feature, features: tosplit }) current.source.dispatchEvent({ type: 'aftersplit', original: current.feature, features: tosplit }) } } return false } /** * @param {ol.MapBrowserEvent} evt Event. */ handleMoveEvent(e) { var map = e.map this.overlayLayer_.getSource().clear() var current = this.getClosestFeature(e) if (current && this.filterSplit_(current.feature)) { var p, l // Draw sketch this.overlayLayer_.getSource().addFeature(current.feature) p = new ol.Feature(new ol.geom.Point(current.coord)) p._sketch_ = true this.overlayLayer_.getSource().addFeature(p) // l = new ol.Feature(current.link) l._sketch_ = true this.overlayLayer_.getSource().addFeature(l) // move event this.dispatchEvent({ type: 'pointermove', coordinate: e.coordinate, frameState: e.frameState, originalEvent: e.originalEvent, map: e.map, pixel: e.pixel, feature: current.feature, linkGeometry: current.link }) } else { this.dispatchEvent(e) } var element = map.getTargetElement() if (this.cursor_) { if (current) { if (element.style.cursor != this.cursor_) { this.previousCursor_ = element.style.cursor element.style.cursor = this.cursor_ } } else if (this.previousCursor_ !== undefined) { element.style.cursor = this.previousCursor_ this.previousCursor_ = undefined } } } } /* 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). */ /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ /** Interaction splitter: acts as a split feature agent while editing vector features (LineString). * @constructor * @extends {ol.interaction.Interaction} * @fires beforesplit, aftersplit * @param {options} * @param {ol.source.Vector|Array} options.source The target source (or array of source) with features to be split (configured with useSpatialIndex set to true) * @param {ol.source.Vector} options.triggerSource Any newly created or modified features from this source will be used to split features on the target source. If none is provided the target source is used instead. * @param {ol.Collection.} options.features A collection of feature to be split (replace source target). * @param {ol.Collection.} options.triggerFeatures Any newly created or modified features from this collection will be used to split features on the target source (replace triggerSource). * @param {function|undefined} options.filter a filter that takes a feature and return true if the feature is eligible for splitting, default always split. * @param {function|undefined} options.tolerance Distance between the calculated intersection and a vertex on the source geometry below which the existing vertex will be used for the split. Default is 1e-10. * @todo verify auto intersection on features that split. */ ol.interaction.Splitter = class olinteractionSplitter extends ol.interaction.Interaction { constructor(options) { options = options || {} super({ handleEvent: function (e) { // Hack to get only one changeFeature when draging with ol.interaction.Modify on. if (e.type != "pointermove" && e.type != "pointerdrag") { if (this.lastEvent_) { this.splitSource(this.lastEvent_.feature) this.lastEvent_ = null } this.moving_ = false } else { this.moving_ = true } return true }, }) // Features added / remove this.added_ = [] this.removed_ = [] // Source to split if (options.features) { this.source_ = new ol.source.Vector({ features: options.features }) } else { this.source_ = options.source ? options.source : new ol.source.Vector({ features: new ol.Collection() }) } var trigger = this.triggerSource if (options.triggerFeatures) { trigger = new ol.source.Vector({ features: options.triggerFeatures }) } if (trigger) { trigger.on("addfeature", this.onAddFeature.bind(this)) trigger.on("changefeature", this.onChangeFeature.bind(this)) trigger.on("removefeature", this.onRemoveFeature.bind(this)) } else { this.source_.on("addfeature", this.onAddFeature.bind(this)) this.source_.on("changefeature", this.onChangeFeature.bind(this)) this.source_.on("removefeature", this.onRemoveFeature.bind(this)) } // Split tolerance between the calculated intersection and the geometry this.tolerance_ = options.tolerance || 1e-10 // Get all features candidate this.filterSplit_ = options.filter || function () { return true } } /** Calculate intersection on 2 segs * @param {Array<_ol_coordinate_>} s1 first seg to intersect (2 points) * @param {Array<_ol_coordinate_>} s2 second seg to intersect (2 points) * @return { boolean | _ol_coordinate_ } intersection point or false no intersection */ intersectSegs(s1, s2) { var tol = this.tolerance_ // Solve var x12 = s1[0][0] - s1[1][0] var x34 = s2[0][0] - s2[1][0] var y12 = s1[0][1] - s1[1][1] var y34 = s2[0][1] - s2[1][1] var det = x12 * y34 - y12 * x34 // No intersection if (Math.abs(det) < tol) { return false } else { // Outside segement var r1 = ((s1[0][0] - s2[1][0]) * y34 - (s1[0][1] - s2[1][1]) * x34) / det if (Math.abs(r1) < tol) return s1[0] if (Math.abs(1 - r1) < tol) return s1[1] if (r1 < 0 || r1 > 1) return false var r2 = ((s1[0][1] - s2[1][1]) * x12 - (s1[0][0] - s2[1][0]) * y12) / det if (Math.abs(r2) < tol) return s2[1] if (Math.abs(1 - r2) < tol) return s2[0] if (r2 < 0 || r2 > 1) return false // Intersection var a = s1[0][0] * s1[1][1] - s1[0][1] * s1[1][0] var b = s2[0][0] * s2[1][1] - s2[0][1] * s2[1][0] var p = [(a * x34 - b * x12) / det, (a * y34 - b * y12) / det] // Test start / end /* console.log("r1: "+r1) console.log("r2: "+r2) console.log ("s10: "+(_ol_coordinate_.dist2d(p,s1[0]) wait end of other interactions if (change) { this._splitSource(feature) } else { setTimeout(function () { this._splitSource(feature) }.bind(this)) } } /** Split the source using a feature * @param {ol.Feature} feature The feature to use to split. * @private */ _splitSource(feature) { var i, k, f2 this.splitting = true this.added_ = [] this.removed_ = [] var c = feature.getGeometry().getCoordinates() // Geom type switch (feature.getGeometry().getType()) { case 'Point': { c = [c]; break; } case 'LineString': { break; } default: { c = []; break; } } var seg, split = [] function intersect(f) { if (f !== feature && f.getGeometry().splitAt) { var c2 = f.getGeometry().getCoordinates() for (var j = 0; j < c2.length - 1; j++) { var p = this.intersectSegs(seg, [c2[j], c2[j + 1]]) if (p) { split.push(p) g = f.getGeometry().splitAt(p, this.tolerance_) if (g && g.length > 1) { found = f return true } } } } return false } // Split with a point if (c.length === 1) { seg = [c[0], c[0]] var extent = ol.extent.buffer(ol.extent.boundingExtent(seg), this.tolerance_ /*0.01*/) this.source_.forEachFeatureIntersectingExtent(extent, function(f) { if (f.getGeometry().splitAt) { var g = f.getGeometry().splitAt(c[0], this.tolerance_) if (g.length > 1) { this.source_.removeFeature(f) for (k = 0; k < g.length; k++) { f2 = f.clone() f2.setGeometry(g[k]) this.source_.addFeature(f2) } } } }.bind(this)) } // Split existing features for (i = 0; i < c.length - 1; i++) { seg = [c[i], c[i + 1]] var extent = ol.extent.buffer(ol.extent.boundingExtent(seg), this.tolerance_ /*0.01*/) var g while (true) { var found = false this.source_.forEachFeatureIntersectingExtent(extent, intersect.bind(this)) // Split feature if (found) { var f = found this.source_.removeFeature(f) for (k = 0; k < g.length; k++) { f2 = f.clone() f2.setGeometry(g[k]) this.source_.addFeature(f2) } } else break } } // Auto intersect for (i = 0; i < c.length - 2; i++) { for (var j = i + 1; j < c.length - 1; j++) { var p = this.intersectSegs([c[i], c[i + 1]], [c[j], c[j + 1]]) if (p && p != c[i + 1]) { split.push(p) } } } // Split original var splitOriginal = false if (split.length) { var result = feature.getGeometry().splitAt(split, this.tolerance_) if (result.length > 1) { for (k = 0; k < result.length; k++) { f2 = feature.clone() f2.setGeometry(result[k]) this.source_.addFeature(f2) } splitOriginal = true } } // End splitting setTimeout(function () { if (splitOriginal) this.source_.removeFeature(feature) this.source_.dispatchEvent({ type: 'aftersplit', featureAdded: this.added_, featureRemoved: this.removed_, source: this.source_ }) this.dispatchEvent({ type: 'aftersplit', featureAdded: this.added_, featureRemoved: this.removed_, source: this.source_ }) // Finish this.splitting = false }.bind(this)) } /** New feature source is added * @private */ onAddFeature(e) { this.splitSource(e.feature) if (this.splitting) { this.added_.push(e.feature) } } /** Feature source is removed > count features added/removed * @private */ onRemoveFeature(e) { if (this.splitting) { var n = this.added_.indexOf(e.feature) if (n == -1) { this.removed_.push(e.feature) } else { this.added_.splice(n, 1) } } } /** Feature source is changing * @private */ onChangeFeature(e) { if (this.moving_) { this.lastEvent_ = e } else { this.splitSource(e.feature, true) } } } /* 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). */ /** Interaction synchronize * @constructor * @extends {ol.interaction.Interaction} * @param {*} options * @param {Array} options maps An array of maps to synchronize with the map of the interaction */ ol.interaction.Synchronize = class olinteractionSynchronize extends ol.interaction.Interaction { constructor(options) { options = options || {} super({ handleEvent: function (e) { if (e.type == "pointermove") { this.handleMove_(e)} return true } }) this.maps = options.maps || [] if (options.active === false) this.setActive(false) } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._listener) { ol.Observable.unByKey(this._listener.center) ol.Observable.unByKey(this._listener.rotation) ol.Observable.unByKey(this._listener.resolution) this.getMap().getTargetElement().removeEventListener('mouseout', this._listener.mouseout) } this._listener = null super.setMap(map) if (map) { this._listener = {} this._listener.center = this.getMap().getView().on('change:center', this.syncMaps.bind(this)) this._listener.rotation = this.getMap().getView().on('change:rotation', this.syncMaps.bind(this)) this._listener.resolution = this.getMap().getView().on('change:resolution', this.syncMaps.bind(this)) this._listener.mouseout = this.handleMouseOut_.bind(this) if (this.getMap().getTargetElement()) { this.getMap().getTargetElement().addEventListener('mouseout', this._listener.mouseout) } this.syncMaps() } } /** Auto activate/deactivate controls in the bar * @param {boolean} b activate/deactivate */ setActive(b) { super.setActive(b) this.syncMaps() } /** Synchronize the maps */ syncMaps() { if (!this.getActive()) return var map = this.getMap() if (map) { if (map.get('lockView')) return for (var i = 0; i < this.maps.length; i++) { this.maps[i].set('lockView', true) // sync if (this.maps[i].getView().getRotation() != map.getView().getRotation()) { this.maps[i].getView().setRotation(map.getView().getRotation()) } if (this.maps[i].getView().getCenter() != map.getView().getCenter()) { this.maps[i].getView().setCenter(map.getView().getCenter()) } if (this.maps[i].getView().getResolution() != map.getView().getResolution()) { this.maps[i].getView().setResolution(map.getView().getResolution()) } this.maps[i].set('lockView', false) } } } /** Cursor move > tells other maps to show the cursor * @param {ol.event} e "move" event */ handleMove_(e) { for (var i = 0; i < this.maps.length; i++) { this.maps[i].showTarget(e.coordinate) } this.getMap().showTarget() } /** Cursor out of map > tells other maps to hide the cursor * @param {event} e "mouseOut" event */ handleMouseOut_( /*e*/) { for (var i = 0; i < this.maps.length; i++) { if (this.maps[i]._targetOverlay) this.maps[i]._targetOverlay.setPosition(undefined) } } } /** Show a target overlay at coord * @param {ol.coordinate} coord */ ol.Map.prototype.showTarget = function(coord) { if (!this._targetOverlay) { var elt = document.createElement("div"); elt.classList.add("ol-target"); this._targetOverlay = new ol.Overlay({ element: elt }); this._targetOverlay.setPositioning('center-center'); this.addOverlay(this._targetOverlay); elt.parentElement.classList.add("ol-target-overlay"); // hack to render targetOverlay before positioning it this._targetOverlay.setPosition([0,0]); } this._targetOverlay.setPosition(coord); }; /** Hide the target overlay */ ol.Map.prototype.hideTarget = function() { this.removeOverlay(this._targetOverlay); this._targetOverlay = undefined; }; /* Tinker Bell effect on maps. Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). @link https://github.com/Viglino */ /** * @constructor * @extends {ol.interaction.Pointer} * @param {ol.interaction.TinkerBell.options} options flashlight param * @param {ol.color} [options.color] color of the sparkles */ ol.interaction.TinkerBell = class olinteractionTinkerBell extends ol.interaction.Pointer { constructor(options) { options = options || {} super({ handleDownEvent: function(e) { this.onMove(e) }, handleMoveEvent: function(e) { this.onMove(e) } }) this.set('color', options.color ? ol.color.asString(options.color) : "#fff") this.sparkle = [0, 0] this.sparkles = [] this.lastSparkle = this.time = new Date() var self = this this.out_ = function () { self.isout_ = true } this.isout_ = true } /** Set the map > start postcompose */ setMap(map) { if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null if (this.getMap()) { map.getViewport().removeEventListener('mouseout', this.out_, false) this.getMap().render() } super.setMap(map) if (map) { this._listener = map.on('postcompose', this.postcompose_.bind(this)) map.getViewport().addEventListener('mouseout', this.out_, false) } } onMove(e) { this.sparkle = e.pixel this.isout_ = false this.getMap().render() } /** Postcompose function */ postcompose_(e) { var delta = 15 var ctx = e.context || ol.ext.getMapCanvas(this.getMap()).getContext('2d') var dt = e.frameState.time - this.time this.time = e.frameState.time if (e.frameState.time - this.lastSparkle > 30 && !this.isout_) { this.lastSparkle = e.frameState.time this.sparkles.push({ p: [this.sparkle[0] + Math.random() * delta - delta / 2, this.sparkle[1] + Math.random() * delta], o: 1 }) } ctx.save() ctx.scale(e.frameState.pixelRatio, e.frameState.pixelRatio) ctx.fillStyle = this.get("color") for (var i = this.sparkles.length - 1, p; p = this.sparkles[i]; i--) { if (p.o < 0.2) { this.sparkles.splice(0, i + 1) break } ctx.globalAlpha = p.o ctx.beginPath() ctx.arc(p.p[0], p.p[1], 2.2, 0, 2 * Math.PI, false) ctx.fill() p.o *= 0.98 p.p[0] += (Math.random() - 0.5) p.p[1] += dt * (1 + Math.random()) / 30 } ctx.restore() // continue postcompose animation if (this.sparkles.length) this.getMap().render() } } /* 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). */ /** Interaction splitter: acts as a split feature agent while editing vector features (LineString). * @constructor * @extends {ol.interaction.Pointer} * @param {Object} options * @param {function|undefined} onDrag Function handling "drag" events. It provides a dpixel and a traction (in projection) vector form the center of the compas * @param {Number} options.size size of the compass in px, default 80 * @param {Number} options.alpha opacity of the compass, default 0.5 */ ol.interaction.TouchCompass = class olinteractionTouchCompass extends ol.interaction.Pointer { constructor(options) { options = options || {}; var opt = {}; // Click on the compass opt.handleDownEvent = function (e) { var s = this.getCenter_(); var dx = e.pixel[0] - s[0]; var dy = e.pixel[1] - s[1]; this.start = e; return (Math.sqrt(dx * dx + dy * dy) < this.size / 2); }; // Pn drag opt.handleDragEvent = function (e) { if (!this.pos) { this.pos = this.start; try { this.getMap().renderSync(); } catch (e) { /* ok */ } } this.pos = e; }; // Stop drag opt.handleUpEvent = function () { this.pos = false; return true; }; super(opt); this.ondrag_ = options.onDrag; this.size = options.size || 80; this.alpha = options.alpha || 0.5; if (!this.compass) { var canvas = this.compass = document.createElement('canvas'); var ctx = canvas.getContext("2d"); var s = canvas.width = canvas.height = this.size; var w = s / 10; var r = s / 2; var r2 = 0.22 * r; ctx.translate(r, r); ctx.fillStyle = "#999"; ctx.strokeStyle = "#ccc"; ctx.lineWidth = w; ctx.beginPath(); ctx.arc(0, 0, s * 0.42, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#99f"; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(r, 0); ctx.lineTo(r2, r2); ctx.moveTo(0, 0); ctx.lineTo(-r, 0); ctx.lineTo(-r2, -r2); ctx.moveTo(0, 0); ctx.lineTo(0, r); ctx.lineTo(-r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, -r); ctx.lineTo(r2, -r2); ctx.moveTo(0, 0); ctx.fill(); ctx.fillStyle = "#eee"; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(r, 0); ctx.lineTo(r2, -r2); ctx.moveTo(0, 0); ctx.lineTo(-r, 0); ctx.lineTo(-r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, r); ctx.lineTo(r2, r2); ctx.moveTo(0, 0); ctx.lineTo(0, -r); ctx.lineTo(-r2, -r2); ctx.moveTo(0, 0); ctx.fill(); } } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {_ol_Map_} map Map. * @api stable */ setMap(map) { if (this._listener) ol.Observable.unByKey(this._listener); this._listener = null; super.setMap(map); if (map) { this._listener = map.on('postcompose', this.drawCompass_.bind(this)); ol.ext.getMapCanvas(map); } } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @observable * @api */ setActive(b) { super.setActive(b); if (this.getMap()) { try { this.getMap().renderSync(); } catch (e) { /* ok */ } } } /** * Get the center of the compass * @param {_ol_coordinate_} * @private */ getCenter_() { var margin = 10; var s = this.size; var c = this.getMap().getSize(); return [c[0] / 2, c[1] - margin - s / 2]; } /** * Draw the compass on post compose * @private */ drawCompass_(e) { if (!this.getActive()) return; var ctx = e.context || ol.ext.getMapCanvas(this.getMap()).getContext('2d'); var ratio = e.frameState.pixelRatio; ctx.save(); ctx.scale(ratio, ratio); ctx.globalAlpha = this.alpha; ctx.strokeStyle = "#fff"; ctx.lineWidth = 5; var s = this.size; var c = this.getCenter_(); ctx.drawImage(this.compass, 0, 0, this.compass.width, this.compass.height, c[0] - s / 2, c[1] - s / 2, s, s); if (this.pos) { var dx = this.pos.pixel[0] - this.start.pixel[0]; var dy = this.pos.pixel[1] - this.start.pixel[1]; for (var i = 1; i <= 4; i++) { ctx.beginPath(); ctx.arc(c[0] + dx / 4 * i, c[1] + dy / 4 * i, s / 2 * (0.6 + 0.4 * i / 4), 0, 2 * Math.PI); ctx.stroke(); } } ctx.restore(); if (this.pos) { // Get delta if (this.ondrag_) { var r = this.getMap().getView().getResolution(); var delta = { dpixel: [this.pos.pixel[0] - this.start.pixel[0], this.pos.pixel[1] - this.start.pixel[1]] }; delta.traction = [delta.dpixel[0] * r, -delta.dpixel[1] * r]; this.ondrag_(delta, this.pos); } // Continue animation e.frameState.animate = true; } } } /* 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). */ /** Handle a touch cursor to defer event position on overlay position * It can be used as abstract base class used for creating subclasses. * The TouchCursor interaction modifies map browser event coordinate and pixel properties to force pointer on the graphic cursor on the screen to any interaction that them. * @constructor * @extends {ol.interaction.DragOverlay} * @param {olx.interaction.InteractionOptions} options Options * @param {string} options.className cursor class name * @param {ol.coordinate} options.coordinate position of the cursor * @param {Array<*>} options.buttons an array of buttons * @param {number} options.maxButtons maximum number of buttons (default 5) */ ol.interaction.TouchCursor = class olinteractionTouchCursor extends ol.interaction.DragOverlay { constructor(options) { options = options || {} // Add Overlay var overlay = new ol.Overlay.Fixed({ className: ('ol-touch-cursor ' + (options.className || '')).trim(), positioning: 'top-left', element: ol.ext.element.create('DIV', {}), stopEvent: false, }) super({ centerOnClick: false, //offset: [-20,-20], overlays: overlay }) this.overlay = overlay; // List of listerner on the object this._listeners = {} // Interaction to defer position on top of the interaction // this is done to enable other coordinates manipulation inserted after the interaction (snapping) var offset = [-35, -35] this.ctouch = new ol.interaction.Interaction({ handleEvent: function (e) { if (!/drag/.test(e.type) && this.getMap()) { e.coordinate = this.overlay.getPosition() e.pixel = this.getMap().getPixelFromCoordinate(e.coordinate) this._lastEvent = e } else { var res = e.frameState.viewState.resolution var cosa = Math.cos(e.frameState.viewState.rotation) var sina = Math.sin(e.frameState.viewState.rotation) e.coordinate = [ e.coordinate[0] + cosa * offset[0] * res + sina * offset[1] * res, e.coordinate[1] + sina * offset[0] * res - cosa * offset[1] * res ] e.pixel = this.getMap().getPixelFromCoordinate(e.coordinate) } return true }.bind(this) }) // Force interaction on top this.ctouch.set('onTop', true) this.setPosition(options.coordinate, true) this.set('maxButtons', options.maxButtons || 5) if (options.buttons) { if (options.buttons.length > this.get('maxButtons')) this.set('maxButtons', options.buttons.length) var elt = this.overlay.element var begin = options.buttons.length > 4 ? 0 : 1 options.buttons.forEach((function (b, i) { if (i < 5) { ol.ext.element.create('DIV', { className: ((b.className || '') + ' ol-button ol-button-' + (i + begin)).trim(), html: ol.ext.element.create('DIV', { html: b.html }), click: b.click, on: b.on, parent: elt }) } })) } // Replace events to handle click var dragging = false var start = false this.on('dragstart', function (e) { this._pixel = this.getMap().getPixelFromCoordinate(this.overlay.getPosition()) start = e return !e.overlay }) this.on('dragend', function (e) { this._pixel = this.getMap().getPixelFromCoordinate(this.overlay.getPosition()) if (!e.overlay) return true if (dragging) { this.dispatchEvent({ type: 'dragend', dragging: dragging, originalEvent: e.originalEvent, frameState: e.frameState, pixel: this._pixel, coordinate: this.overlay.getPosition() }) dragging = false } else { if (e.originalEvent.target === this.overlay.element) { this.dispatchEvent({ type: 'click', dragging: dragging, originalEvent: e.originalEvent, frameState: e.frameState, pixel: this._pixel, coordinate: this.overlay.getPosition() }) } } return false }.bind(this)) this.on('dragging', function (e) { this._pixel = this.getMap().getPixelFromCoordinate(this.overlay.getPosition()) if (!e.overlay) return true dragging = true if (start) { this.dispatchEvent({ type: 'dragstart', dragging: dragging, originalEvent: start.originalEvent, frameState: e.frameState, pixel: this._pixel, coordinate: start.coordinate }) start = false } this.dispatchEvent({ type: 'dragging', dragging: dragging, originalEvent: e.originalEvent, frameState: e.frameState, pixel: this._pixel, coordinate: this.overlay.getPosition() }) return false }.bind(this)) } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {_ol_Map_} map Map. * @api stable */ setMap(map) { // Reset if (this.getMap()) { this.getMap().removeInteraction(this.ctouch) if (this.getActive()) this.getMap().removeOverlay(this.overlay) } for (var l in this._listeners) { ol.Observable.unByKey(this._listeners[l]) } this._listeners = {} super.setMap(map) // Set listeners if (map) { if (this.getActive()) { map.addOverlay(this.overlay) setTimeout(function () { this.setPosition(this.getPosition() || map.getView().getCenter()) }.bind(this)) } map.addInteraction(this.ctouch) this._listeners.addInteraction = map.getInteractions().on('add', function (e) { // Move on top if (!e.element.get('onTop')) { map.removeInteraction(this.ctouch) map.addInteraction(this.ctouch) } }.bind(this)) } } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @param {ol.coordinate|null} position position of the cursor (when activating), default viewport center. * @observable * @api */ setActive(b, position) { if (b !== this.getActive()) { if (this.ctouch) this.ctouch.setActive(b) if (!b) { this.setPosition() this.overlay.element.classList.remove('active') if (this._activate) clearTimeout(this._activate) if (this.getMap()) this.getMap().removeOverlay(this.overlay) } else { if (this.getMap()) { this.getMap().addOverlay(this.overlay) } if (position) { this.setPosition(position) } else if (this.getMap()) { this.setPosition(this.getMap().getView().getCenter()) } this._activate = setTimeout(function () { this.overlay.element.classList.add('active') }.bind(this), 100) } super.setActive(b) } else if (position) { this.setPosition(position) } else if (this.getMap()) { this.setPosition(this.getMap().getView().getCenter()) } } /** Set the position of the target * @param {ol.coordinate} coord */ setPosition(coord) { this.overlay.setPosition(coord, true) if (this.getMap() && coord) { this._pixel = this.getMap().getPixelFromCoordinate(coord) } } /** Offset the target position * @param {ol.coordinate} coord */ offsetPosition(coord) { var pos = this.overlay.getPosition() if (pos) this.overlay.setPosition([pos[0] + coord[0], pos[1] + coord[1]]) } /** Get the position of the target * @return {ol.coordinate} */ getPosition() { return this.overlay.getPosition() } /** Get pixel position * @return {ol.pixel} */ getPixel() { if (this.getMap()) return this.getMap().getPixelFromCoordinate(this.getPosition()) } /** Get cursor overlay * @return {ol.Overlay} */ getOverlay() { return this.overlay } /** Get cursor overlay element * @return {Element} */ getOverlayElement() { return this.overlay.element } /** Get cursor button element * @param {string|number} button the button className or the button index * @return {Element} */ getButtonElement(button) { if (typeof (button) === 'number') return this.getOverlayElement().getElementsByClassName('ol-button')[button] return this.getOverlayElement().getElementsByClassName(button)[0] } /** Remove a button element * @param {string|number|undefined} button the button className or the button index, if undefined remove all buttons, default remove all * @return {Element} */ removeButton(button) { if (button === undefined) { var buttons = this.getOverlayElement().getElementsByClassName('ol-button') for (var i = buttons.length - 1; i >= 0; i--) { this.getOverlayElement().removeChild(buttons[i]) } } else { var elt = this.getButtonElement(button) if (elt) this.getOverlayElement().removeChild(elt) } } /** Add a button element * @param {*} button * @param {string} options.className button class name * @param {DOMElement|string} options.html button content * @param {function} options.click onclick function * @param {*} options.on an object with * @param {boolean} options.before */ addButton(b) { var buttons = this.getOverlayElement().getElementsByClassName('ol-button') var max = (this.get('maxButtons') || 5) if (buttons.length >= max) { console.error('[ol/interaction/TouchCursor~addButton] too many button on the cursor (max=' + max + ')...') return } var button = ol.ext.element.create('DIV', { className: ((b.className || '') + ' ol-button').trim(), html: ol.ext.element.create('DIV', { html: b.html }), click: b.click, on: b.on }) if (!b.before || buttons.length === 0) this.getOverlayElement().appendChild(button) else this.getOverlayElement().insertBefore(button, buttons[0]) // Reorder buttons var start = buttons.length >= max ? 0 : 1 for (var i = 0; i < buttons.length; i++) { buttons[i].className = buttons[i].className.replace(/ol-button-\d/g, '').trim() + ' ol-button-' + (i + start) } } } /* 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). */ /** TouchCursor interaction + ModifyFeature * @constructor * @extends {ol.interaction.TouchCursor} * @fires drawend * @fires change:type * @param {olx.interaction.InteractionOptions} options Options * @param {string} options.className cursor class name * @param {ol.coordinate} options.coordinate cursor position * @param {string} options.type geometry type * @param {Array} options.types geometry types avaliable, default none * @param {ol.source.Vector} options.source Destination source for the drawn features * @param {ol.Collection} options.features Destination collection for the drawn features * @param {number} options.clickTolerance The maximum distance in pixels for "click" event to add a point/vertex to the geometry being drawn. default 6 * @param {number} options.snapTolerance Pixel distance for snapping to the drawing finish, default 12 * @param {number} options.maxPoints The number of points that can be drawn before a polygon ring or line string is finished. By default there is no restriction. * @param {number} options.minPoints The number of points that must be drawn before a polygon ring or line string can be finished. Default is 3 for polygon rings and 2 for line strings. * @param {ol.style.Style} options.style Style for sketch features. * @param {function} options.geometryFunction Function that is called when a geometry's coordinates are updated. * @param {string} options.geometryName Geometry name to use for features created by the draw interaction. * @param {boolean} options.wrapX Wrap the world horizontally on the sketch overlay, default false */ ol.interaction.TouchCursorDraw = class olinteractionTouchCursorDraw extends ol.interaction.TouchCursor { constructor(options) { options = options || {}; // Create cursor super({ className: options.className, coordinate: options.coordinate, }); this.getOverlayElement().classList.add('nodrawing'); // Draw var sketch = this.sketch = new ol.layer.SketchOverlay({ type: options.type }); sketch.on('drawend', function (e) { if (e.valid && options.source) options.source.addFeature(e.feature); this.getOverlayElement().classList.add('nodrawing'); this.dispatchEvent(e); }.bind(this)); sketch.on('drawstart', function (e) { this.getOverlayElement().classList.remove('nodrawing'); this.dispatchEvent(e); }.bind(this)); sketch.on('drawabort', function (e) { this.getOverlayElement().classList.add('nodrawing'); this.dispatchEvent(e); }.bind(this)); this.set('types', options.types); this.setType(options.type); this.on('click', function () { this.sketch.addPoint(this.getPosition()); }.bind(this)); this.on('dragging', function () { this.sketch.setPosition(this.getPosition()); }.bind(this)); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {_ol_Map_} map Map. * @api stable */ setMap(map) { super.setMap(map); this.sketch.setMap(map); if (map) { this._listeners.movend = map.on('moveend', function () { this.sketch.setPosition(this.getPosition()); }.bind(this)); } } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @param {ol.coordinate|null} position position of the cursor (when activating), default viewport center. * @observable * @api */ setActive(b, position) { super.setActive(b, position); if (this.sketch) { this.sketch.abortDrawing(); this.sketch.setPosition(position); this.sketch.setVisible(b); } } /** * Set Geometry type * @param {string} type Geometry type */ setType(type) { this.removeButton(); var sketch = this.sketch; this.getOverlayElement().classList.remove(sketch.getGeometryType()); // Set type var oldValue = sketch.setGeometryType(); type = sketch.setGeometryType(type); this.getOverlayElement().classList.add(type); this.dispatchEvent({ type: 'change:type', oldValue: oldValue }); // Next type var types = this.get('types'); if (types && types.length) { var next = types[(types.indexOf(type) + 1) % types.length]; this.addButton({ className: 'ol-button-type ' + next, click: function () { this.setType(next); }.bind(this) }); } // Add buttons if (type !== 'Point') { // Cancel drawing this.addButton({ className: 'ol-button-x', click: function () { sketch.abortDrawing(); } }); if (type !== 'Circle') { // Add a new point (nothing to do, just click) this.addButton({ className: 'ol-button-check', click: function () { sketch.finishDrawing(true); } }); // Remove last point this.addButton({ className: 'ol-button-remove', click: function () { sketch.removeLastPoint(); } }); } } } /** Get geometry type */ getType() { return this.sketch.getGeometryType(); } } /* 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). */ /** TouchCursor interaction + ModifyFeature * @constructor * @extends {ol.interaction.TouchCursor} * @param {olx.interaction.InteractionOptions} options Options * @param {string} options.className cursor class name * @param {ol.coordinate} options.coordinate cursor position * @param {ol.source.Vector} options.source a source to modify (configured with useSpatialIndex set to true) * @param {ol.source.Vector|Array} options.sources a list of source to modify (configured with useSpatialIndex set to true) * @param {ol.Collection.} options.features collection of feature to modify * @param {function|undefined} options.filter a filter that takes a feature and return true if it can be modified, default always true. * @param {number} pixelTolerance Pixel tolerance for considering the pointer close enough to a segment or vertex for editing, default 10 * @param {ol.style.Style | Array | undefined} options.style Style for the sketch features. * @param {boolean} options.wrapX Wrap the world horizontally on the sketch overlay, default false */ ol.interaction.TouchCursorModify = class olinteractionTouchCursorModify extends ol.interaction.TouchCursor { constructor(options) { options = options || {}; var drag = false; // enable drag var dragging = false; // dragging a point var del = false; // deleting a point super({ className: ('disable ' + options.className).trim(), coordinate: options.coordinate, buttons: [{ // Dragging button className: 'ol-button-move', on: { pointerdown: function () { drag = true; }, pointerup: function () { drag = false; } } }, { // Add a new point to a line className: 'ol-button-add', click: function () { dragging = true; mod.handleDownEvent(self._lastEvent); mod.handleUpEvent(self._lastEvent); dragging = false; } }, { // Remove a point className: 'ol-button-remove', click: function () { del = true; mod.handleDownEvent(self._lastEvent); del = false; } }] }); var self = this; // Modify interaction var mod = this._modify = new ol.interaction.ModifyFeature({ source: options.source, sources: options.sources, features: options.features, pixelTolerance: options.pixelTolerance, filter: options.filter, style: options.style || [ new ol.style.Style({ image: new ol.style.RegularShape({ points: 4, radius: 10, radius2: 0, stroke: new ol.style.Stroke({ color: [255, 255, 255, .5], width: 3 }) }) }), new ol.style.Style({ image: new ol.style.RegularShape({ points: 4, radius: 10, radius2: 0, stroke: new ol.style.Stroke({ color: [0, 153, 255, 1], width: 1.25 }) }) }) ], wrapX: options.wrapX, condition: function (e) { return e.dragging || dragging; }, deleteCondition: function () { return del; } }); // Show when modification is active mod.on('select', function (e) { if (e.selected.length) { this.getOverlayElement().classList.remove('disable'); } else { this.getOverlayElement().classList.add('disable'); } }.bind(this)); // Handle dragging, prevent drag outside the control this.on('dragstart', function () { if (drag) { mod.handleDownEvent(this._lastEvent); } }.bind(this)); this.on('dragging', function (e) { if (drag) mod.handleDragEvent(e); }); this.on('dragend', function (e) { mod.handleUpEvent(e); drag = false; }); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {_ol_Map_} map Map. * @api stable */ setMap(map) { if (this.getMap()) { this.getMap().removeInteraction(this._modify); } if (map) { map.addInteraction(this._modify); } super.setMap(map); } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @param {ol.coordinate|null} position position of the cursor (when activating), default viewport center. * @observable * @api */ setActive(b, position) { super.setActive(b, position); if (this._modify) this._modify.setActive(b); } /** * Get the modify interaction. * @retunr {ol.interaction.ModifyFeature} * @observable * @api */ getInteraction() { return this._modify; } } /* 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). */ /** A TouchCursor to select objects on hovering the cursor * @constructor * @extends {ol.interaction.DragOverlay} * @param {olx.interaction.InteractionOptions} options Options * @param {string} options.className cursor class name * @param {ol.coordinate} options.coordinate position of the cursor */ ol.interaction.TouchCursorSelect = class olinteractionTouchCursorSelect extends ol.interaction.TouchCursor { constructor(options) { options = options || {}; super({ className: 'ol-select ' + (options.className || ''), coordinate: options.coordinate }); this._selection = null; this._layerFilter = options.layerFilter; this._filter = options.filter; this._style = options.style || ol.style.Style.defaultStyle(true); this.set('hitTolerance', options.hitTolerance || 0); this.on(['change:active', 'dragging'], function () { this.select(); }); } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {_ol_Map_} map Map. * @api stable */ setMap(map) { super.setMap(map); if (map) { // Select on move end this._listeners.movend = map.on('moveend', function () { this.select(); }.bind(this)); } } /** Get current selection * @return {ol.Feature|null} */ getSelection() { return this._selection ? this._selection.feature : null; } /** Set position * @param {ol.coordinate} coord */ setPosition(coord) { super.setPosition(coord); this.select(); } /** Select feature * @param {ol.Feature|undefined} f a feature to select or select at the cursor position */ select(f) { var current = this._selection; if (this.getActive() && this.getPosition()) { if (!f) { var sel = this.getMap().getFeaturesAtPixel(this.getPixel(), { layerFilter: this._layerFilter, filter: this._filter, hitTolerance: this.get('hitTolerance') }); f = sel ? sel[0] : null; } if (f) { if (current && f === current.feature) { current = null; } else { this._selection = { feature: f, style: f.getStyle() }; f.setStyle(this._style); this.dispatchEvent({ type: 'select', selected: [f], deselected: current ? [current.feature] : [] }); } } else { this._selection = null; this.dispatchEvent({ type: 'select', selected: [], deselected: current ? [current.feature] : [] }); } } else { this._selection = null; this.dispatchEvent({ type: 'select', selected: [], deselected: current ? [current.feature] : [] }); } // Restore current style if (current) { current.feature.setStyle(current.style); } // if (this._selection) this.getOverlayElement().classList.remove('disable'); else this.getOverlayElement().classList.add('disable'); } } /** Interaction rotate * @constructor * @extends {ol.interaction.Pointer} * @fires select | rotatestart | rotating | rotateend | translatestart | translating | translateend | scalestart | scaling | scaleend * @param {any} options * @param {function} options.filter A function that takes a Feature and a Layer and returns true if the feature may be transformed or false otherwise. * @param {Array} options.layers array of layers to transform, * @param {ol.Collection} options.features collection of feature to transform, * @param {ol.EventsConditionType|undefined} options.condition A function that takes an ol.MapBrowserEvent and a feature collection and returns a boolean to indicate whether that event should be handled. default: ol.events.condition.always. * @param {ol.EventsConditionType|undefined} options.addCondition A function that takes an ol.MapBrowserEvent and returns a boolean to indicate whether that event should be handled ie. the feature will be added to the transforms features. default: ol.events.condition.never. * @param {number | undefined} options.hitTolerance Tolerance to select feature in pixel, default 0 * @param {bool} options.translateFeature Translate when click on feature * @param {bool} options.translate Can translate the feature * @param {bool} options.translateBBox Enable translate when the user drags inside the bounding box * @param {bool} options.stretch can stretch the feature * @param {bool} options.scale can scale the feature * @param {bool} options.rotate can rotate the feature * @param {bool} options.noFlip prevent the feature geometry to flip, default false * @param {bool} options.selection the intraction handle selection/deselection, if not use the select prototype to add features to transform, default true * @param {ol.events.ConditionType | undefined} options.keepAspectRatio A function that takes an ol.MapBrowserEvent and returns a boolean to keep aspect ratio, default ol.events.condition.shiftKeyOnly. * @param {ol.events.ConditionType | undefined} options.modifyCenter A function that takes an ol.MapBrowserEvent and returns a boolean to apply scale & strech from the center, default ol.events.condition.metaKey or ol.events.condition.ctrlKey. * @param {boolean} options.enableRotatedTransform Enable transform when map is rotated * @param {boolean} [options.keepRectangle=false] keep rectangle when possible * @param {number} [options.buffer] Increase the extent used as bounding box, default 0 * @param {*} options.style list of ol.style for handles * @param {number|Array|function} [options.pointRadius=0] radius for points or a function that takes a feature and returns the radius (or [radiusX, radiusY]). If not null show handles to transform the points */ ol.interaction.Transform = class olinteractionTransform extends ol.interaction.Pointer { constructor(options) { options = options || {} // Extend pointer super({ handleDownEvent: function(e) { return self.handleDownEvent_(e) }, handleDragEvent: function(e) { return this.handleDragEvent_(e) }, handleMoveEvent: function(e) { return this.handleMoveEvent_(e) }, handleUpEvent: function(e) { return this.handleUpEvent_(e) }, }) var self = this this.selection_ = new ol.Collection() // Create a new overlay layer for the sketch this.handles_ = new ol.Collection() this.overlayLayer_ = new ol.layer.Vector({ source: new ol.source.Vector({ features: this.handles_, useSpatialIndex: false, wrapX: false // For vector editing across the -180° and 180° meridians to work properly, this should be set to false }), name: 'Transform overlay', displayInLayerSwitcher: false, // Return the style according to the handle type style: function (feature) { return (self.style[(feature.get('handle') || 'default') + (feature.get('constraint') || '') + (feature.get('option') || '')]) }, }) // Collection of feature to transform this.features_ = options.features // Filter or list of layers to transform if (typeof (options.filter) === 'function') this._filter = options.filter this.layers_ = options.layers ? (options.layers instanceof Array) ? options.layers : [options.layers] : null this._handleEvent = options.condition || function () { return true } this.addFn_ = options.addCondition || function () { return false } this.setPointRadius(options.pointRadius) /* Translate when click on feature */ this.set('translateFeature', (options.translateFeature !== false)) /* Can translate the feature */ this.set('translate', (options.translate !== false)) /* Translate when click on the bounding box */ this.set('translateBBox', (options.translateBBox === true)) /* Can stretch the feature */ this.set('stretch', (options.stretch !== false)) /* Can scale the feature */ this.set('scale', (options.scale !== false)) /* Can rotate the feature */ this.set('rotate', (options.rotate !== false)) /* Keep aspect ratio */ this.set('keepAspectRatio', (options.keepAspectRatio || function (e) { return e.originalEvent.shiftKey })) /* Modify center */ this.set('modifyCenter', (options.modifyCenter || function (e) { return e.originalEvent.metaKey || e.originalEvent.ctrlKey })) /* Prevent flip */ this.set('noFlip', (options.noFlip || false)) /* Handle selection */ this.set('selection', (options.selection !== false)) /* */ this.set('hitTolerance', (options.hitTolerance || 0)) /* Enable view rotated transforms */ this.set('enableRotatedTransform', (options.enableRotatedTransform || false)) /* Keep rectangle angles 90 degrees */ this.set('keepRectangle', (options.keepRectangle || false)) /* Add buffer to the feature's extent */ this.set('buffer', (options.buffer || 0)) // Force redraw when changed this.on('propertychange', function () { this.drawSketch_() }) // setstyle this.setDefaultStyle() } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { var oldMap = this.getMap() if (oldMap) { var targetElement = oldMap.getTargetElement() oldMap.removeLayer(this.overlayLayer_) if (this.previousCursor_ && targetElement) { targetElement.style.cursor = this.previousCursor_ } this.previousCursor_ = undefined } super.setMap(map) this.overlayLayer_.setMap(map) if (map === null) { this.select(null) } if (map !== null) { this.isTouch = /touch/.test(map.getViewport().className) this.setDefaultStyle() } } /** * Activate/deactivate interaction * @param {bool} * @api stable */ setActive(b) { this.select(null) if (this.overlayLayer_) this.overlayLayer_.setVisible(b) super.setActive(b) } /** Set default sketch style * @param {Object|undefined} options * @param {ol.style.Stroke} stroke stroke style for selection rectangle * @param {ol.style.Fill} fill fill style for selection rectangle * @param {ol.style.Stroke} pointStroke stroke style for handles * @param {ol.style.Fill} pointFill fill style for handles */ setDefaultStyle(options) { options = options || {} // Style var stroke = options.pointStroke || new ol.style.Stroke({ color: [255, 0, 0, 1], width: 1 }) var strokedash = options.stroke || new ol.style.Stroke({ color: [255, 0, 0, 1], width: 1, lineDash: [4, 4] }) var fill0 = options.fill || new ol.style.Fill({ color: [255, 0, 0, 0.01] }) var fill = options.pointFill || new ol.style.Fill({ color: [255, 255, 255, 0.8] }) var circle = new ol.style.RegularShape({ fill: fill, stroke: stroke, radius: this.isTouch ? 12 : 6, displacement: this.isTouch ? [24, -24] : [12, -12], points: 15 }) // Old version with no displacement if (!circle.setDisplacement) circle.getAnchor()[0] = this.isTouch ? -10 : -5 var bigpt = new ol.style.RegularShape({ fill: fill, stroke: stroke, radius: this.isTouch ? 16 : 8, points: 4, angle: Math.PI / 4 }) var smallpt = new ol.style.RegularShape({ fill: fill, stroke: stroke, radius: this.isTouch ? 12 : 6, points: 4, angle: Math.PI / 4 }) function createStyle(img, stroke, fill) { return [new ol.style.Style({ image: img, stroke: stroke, fill: fill })] } /** Style for handles */ this.style = { 'default': createStyle(bigpt, strokedash, fill0), 'translate': createStyle(bigpt, stroke, fill), 'rotate': createStyle(circle, stroke, fill), 'rotate0': createStyle(bigpt, stroke, fill), 'scale': createStyle(bigpt, stroke, fill), 'scale1': createStyle(bigpt, stroke, fill), 'scale2': createStyle(bigpt, stroke, fill), 'scale3': createStyle(bigpt, stroke, fill), 'scalev': createStyle(smallpt, stroke, fill), 'scaleh1': createStyle(smallpt, stroke, fill), 'scalev2': createStyle(smallpt, stroke, fill), 'scaleh3': createStyle(smallpt, stroke, fill), } this.drawSketch_() } /** * Set sketch style. * @param {style} style Style name: 'default','translate','rotate','rotate0','scale','scale1','scale2','scale3','scalev','scaleh1','scalev2','scaleh3' * @param {ol.style.Style|Array} olstyle * @api stable */ setStyle(style, olstyle) { if (!olstyle) return if (olstyle instanceof Array) this.style[style] = olstyle else this.style[style] = [olstyle] for (var i = 0; i < this.style[style].length; i++) { var im = this.style[style][i].getImage() if (im) { if (style == 'rotate') { im.getAnchor()[0] = -5 } if (this.isTouch) im.setScale(1.8) } var tx = this.style[style][i].getText() if (tx) { if (style == 'rotate') tx.setOffsetX(this.isTouch ? 14 : 7) if (this.isTouch) tx.setScale(1.8) } } this.drawSketch_() } /** Get Feature at pixel * @param {ol.Pixel} * @return {ol.feature} * @private */ getFeatureAtPixel_(pixel) { var self = this return this.getMap().forEachFeatureAtPixel(pixel, function (feature, layer) { var found = false // Overlay ? if (!layer) { if (feature === self.bbox_) { if (self.get('translateBBox')) { return { feature: feature, handle: 'translate', constraint: '', option: '' } } else { return false } } self.handles_.forEach(function (f) { if (f === feature) found = true }) if (found) return { feature: feature, handle: feature.get('handle'), constraint: feature.get('constraint'), option: feature.get('option') } } // No seletion if (!self.get('selection')) { // Return the currently selected feature the user is interacting with. if (self.selection_.getArray().some(function (f) { return feature === f })) { return { feature: feature } } return null } // filter condition if (self._filter) { if (self._filter(feature, layer)) return { feature: feature } else return null } // feature belong to a layer else if (self.layers_) { for (var i = 0; i < self.layers_.length; i++) { if (self.layers_[i] === layer) return { feature: feature } } return null } // feature in the collection else if (self.features_) { self.features_.forEach(function (f) { if (f === feature) found = true }) if (found) return { feature: feature } else return null } // Others else return { feature: feature } }, { hitTolerance: this.get('hitTolerance') } ) || {} } /** Rotate feature from map view rotation * @param {ol.Feature} f the feature * @param {boolean} clone clone resulting geom * @param {ol.geom.Geometry} rotated geometry */ getGeometryRotateToZero_(f, clone) { var origGeom = f.getGeometry() var viewRotation = this.getMap().getView().getRotation() if (viewRotation === 0 || !this.get('enableRotatedTransform')) { return (clone) ? origGeom.clone() : origGeom } var rotGeom = origGeom.clone() rotGeom.rotate(viewRotation * -1, this.getMap().getView().getCenter()) return rotGeom } /** Test if rectangle * @param {ol.Geometry} geom * @returns {boolean} * @private */ _isRectangle(geom) { if (this.get('keepRectangle') && geom.getType() === 'Polygon') { var coords = geom.getCoordinates()[0] return coords.length === 5 } return false } /** Draw transform sketch * @param {boolean} draw only the center */ drawSketch_(center) { var i, f, geom var keepRectangle = this.selection_.item(0) && this._isRectangle(this.selection_.item(0).getGeometry()) this.overlayLayer_.getSource().clear() if (!this.selection_.getLength()) return var viewRotation = this.getMap().getView().getRotation() var ext = this.getGeometryRotateToZero_(this.selection_.item(0)).getExtent() var coords if (keepRectangle) { coords = this.getGeometryRotateToZero_(this.selection_.item(0)).getCoordinates()[0].slice(0, 4) coords.unshift(coords[3]) } // Clone and extend ext = ol.extent.buffer(ext, this.get('buffer')) this.selection_.forEach(function (f) { var extendExt = this.getGeometryRotateToZero_(f).getExtent() ol.extent.extend(ext, extendExt) }.bind(this)) var ptRadius = (this.selection_.getLength() === 1 ? this._pointRadius(this.selection_.item(0)) : 0) if (ptRadius && !(ptRadius instanceof Array)) ptRadius = [ptRadius, ptRadius] if (center === true) { if (!this.ispt_) { this.overlayLayer_.getSource().addFeature(new ol.Feature({ geometry: new ol.geom.Point(this.center_), handle: 'rotate0' })) geom = ol.geom.Polygon.fromExtent(ext) if (this.get('enableRotatedTransform') && viewRotation !== 0) { geom.rotate(viewRotation, this.getMap().getView().getCenter()) } f = this.bbox_ = new ol.Feature(geom) this.overlayLayer_.getSource().addFeature(f) } } else { if (this.ispt_) { // Calculate extent arround the point var p = this.getMap().getPixelFromCoordinate([ext[0], ext[1]]) if (p) { var dx = ptRadius ? ptRadius[0] || 10 : 10 var dy = ptRadius ? ptRadius[1] || 10 : 10 ext = ol.extent.boundingExtent([ this.getMap().getCoordinateFromPixel([p[0] - dx, p[1] - dy]), this.getMap().getCoordinateFromPixel([p[0] + dx, p[1] + dy]) ]) } } geom = keepRectangle ? new ol.geom.Polygon([coords]) : ol.geom.Polygon.fromExtent(ext) if (this.get('enableRotatedTransform') && viewRotation !== 0) { geom.rotate(viewRotation, this.getMap().getView().getCenter()) } f = this.bbox_ = new ol.Feature(geom) var features = [] var g = geom.getCoordinates()[0] if (!this.ispt_ || ptRadius) { features.push(f) // Middle if (!this.iscircle_ && !this.ispt_ && this.get('stretch') && this.get('scale')) for (i = 0; i < g.length - 1; i++) { f = new ol.Feature({ geometry: new ol.geom.Point([(g[i][0] + g[i + 1][0]) / 2, (g[i][1] + g[i + 1][1]) / 2]), handle: 'scale', constraint: i % 2 ? "h" : "v", option: i }) features.push(f) } // Handles if (this.get('scale')) for (i = 0; i < g.length - 1; i++) { f = new ol.Feature({ geometry: new ol.geom.Point(g[i]), handle: 'scale', option: i }) features.push(f) } // Center if (this.get('translate') && !this.get('translateFeature')) { f = new ol.Feature({ geometry: new ol.geom.Point([(g[0][0] + g[2][0]) / 2, (g[0][1] + g[2][1]) / 2]), handle: 'translate' }) features.push(f) } } // Rotate if (!this.iscircle_ && this.get('rotate')) { f = new ol.Feature({ geometry: new ol.geom.Point(g[3]), handle: 'rotate' }) features.push(f) } // Add sketch this.overlayLayer_.getSource().addFeatures(features) } } /** Select a feature to transform * @param {ol.Feature} feature the feature to transform * @param {boolean} add true to add the feature to the selection, default false */ select(feature, add) { if (!feature) { if (this.selection_) { this.selection_.clear() this.drawSketch_() } return } if (!feature.getGeometry || !feature.getGeometry()) return // Add to selection if (add) { this.selection_.push(feature) } else { var index = this.selection_.getArray().indexOf(feature) this.selection_.removeAt(index) } this.ispt_ = (this.selection_.getLength() === 1 ? (this.selection_.item(0).getGeometry().getType() == "Point") : false) this.iscircle_ = (this.selection_.getLength() === 1 ? (this.selection_.item(0).getGeometry().getType() == "Circle") : false) this.drawSketch_() this.watchFeatures_() // select event this.dispatchEvent({ type: 'select', feature: feature, features: this.selection_ }) } /** Update the selection collection. * @param {ol.Collection} features the features to transform */ setSelection(features) { this.selection_.clear() features.forEach(function (feature) { this.selection_.push(feature) }.bind(this)) this.ispt_ = (this.selection_.getLength() === 1 ? (this.selection_.item(0).getGeometry().getType() == "Point") : false) this.iscircle_ = (this.selection_.getLength() === 1 ? (this.selection_.item(0).getGeometry().getType() == "Circle") : false) this.drawSketch_() this.watchFeatures_() // select event this.dispatchEvent({ type: 'select', features: this.selection_ }) } /** Watch selected features * @private */ watchFeatures_() { // Listen to feature modification if (this._featureListeners) { this._featureListeners.forEach(function (l) { ol.Observable.unByKey(l) }) } this._featureListeners = [] this.selection_.forEach(function (f) { this._featureListeners.push( f.on('change', function () { if (!this.isUpdating_) { this.drawSketch_() } }.bind(this)) ) }.bind(this)) } /** * @param {ol.MapBrowserEvent} evt Map browser event. * @return {boolean} `true` to start the drag sequence. * @private */ handleDownEvent_(evt) { if (!this._handleEvent(evt, this.selection_)) return var sel = this.getFeatureAtPixel_(evt.pixel) var feature = sel.feature if (this.selection_.getLength() && this.selection_.getArray().indexOf(feature) >= 0 && ((this.ispt_ && this.get('translate')) || this.get('translateFeature'))) { sel.handle = 'translate' } if (sel.handle) { this.mode_ = sel.handle this.opt_ = sel.option this.constraint_ = sel.constraint // Save info var viewRotation = this.getMap().getView().getRotation() this.coordinate_ = evt.coordinate this.pixel_ = evt.pixel this.geoms_ = [] this.rotatedGeoms_ = [] var extent = ol.extent.createEmpty() var rotExtent = ol.extent.createEmpty() for (var i = 0, f; f = this.selection_.item(i); i++) { this.geoms_.push(f.getGeometry().clone()) extent = ol.extent.extend(extent, f.getGeometry().getExtent()) if (this.get('enableRotatedTransform') && viewRotation !== 0) { var rotGeom = this.getGeometryRotateToZero_(f, true) this.rotatedGeoms_.push(rotGeom) rotExtent = ol.extent.extend(rotExtent, rotGeom.getExtent()) } } this.extent_ = (ol.geom.Polygon.fromExtent(extent)).getCoordinates()[0] if (this.get('enableRotatedTransform') && viewRotation !== 0) { this.rotatedExtent_ = (ol.geom.Polygon.fromExtent(rotExtent)).getCoordinates()[0] } if (this.mode_ === 'rotate') { this.center_ = this.getCenter() || ol.extent.getCenter(extent) // we are now rotating (cursor down on rotate mode), so apply the grabbing cursor var element = evt.map.getTargetElement() element.style.cursor = this.Cursors.rotate0 this.previousCursor_ = element.style.cursor } else { this.center_ = ol.extent.getCenter(extent) } this.angle_ = Math.atan2(this.center_[1] - evt.coordinate[1], this.center_[0] - evt.coordinate[0]) this.dispatchEvent({ type: this.mode_ + 'start', feature: this.selection_.item(0), features: this.selection_, pixel: evt.pixel, coordinate: evt.coordinate }) return true } else if (this.get('selection')) { if (feature) { if (!this.addFn_(evt)) this.selection_.clear() var index = this.selection_.getArray().indexOf(feature) if (index < 0) this.selection_.push(feature) else this.selection_.removeAt(index) } else { this.selection_.clear() } this.ispt_ = this.selection_.getLength() === 1 ? (this.selection_.item(0).getGeometry().getType() == "Point") : false this.iscircle_ = (this.selection_.getLength() === 1 ? (this.selection_.item(0).getGeometry().getType() == "Circle") : false) this.drawSketch_() this.watchFeatures_() this.dispatchEvent({ type: 'select', feature: feature, features: this.selection_, pixel: evt.pixel, coordinate: evt.coordinate }) return false } } /** * Get the rotation center * @return {ol.coordinate|undefined} */ getCenter() { return this.get('center') } /** * Set the rotation center * @param {ol.coordinate|undefined} c the center point, default center on the objet */ setCenter(c) { return this.set('center', c) } /** * @param {ol.MapBrowserEvent} evt Map browser event. * @private */ handleDragEvent_(evt) { if (!this._handleEvent(evt, this.features_)) return var viewRotation = this.getMap().getView().getRotation() var i, j, f, geometry var pt0 = [this.coordinate_[0], this.coordinate_[1]] var pt = [evt.coordinate[0], evt.coordinate[1]] this.isUpdating_ = true switch (this.mode_) { case 'rotate': { var a = Math.atan2(this.center_[1] - pt[1], this.center_[0] - pt[0]) if (!this.ispt) { // var geometry = this.geom_.clone(); // geometry.rotate(a-this.angle_, this.center_); // this.feature_.setGeometry(geometry); for (i = 0, f; f = this.selection_.item(i); i++) { geometry = this.geoms_[i].clone() geometry.rotate(a - this.angle_, this.center_) // bug: ol, bad calculation circle geom extent if (geometry.getType() == 'Circle') geometry.setCenterAndRadius(geometry.getCenter(), geometry.getRadius()) f.setGeometry(geometry) } } this.drawSketch_(true) this.dispatchEvent({ type: 'rotating', feature: this.selection_.item(0), features: this.selection_, angle: a - this.angle_, pixel: evt.pixel, coordinate: evt.coordinate }) break } case 'translate': { var deltaX = pt[0] - pt0[0] var deltaY = pt[1] - pt0[1] //this.feature_.getGeometry().translate(deltaX, deltaY); for (i = 0, f; f = this.selection_.item(i); i++) { f.getGeometry().translate(deltaX, deltaY) } this.handles_.forEach(function (f) { f.getGeometry().translate(deltaX, deltaY) }) this.coordinate_ = evt.coordinate this.dispatchEvent({ type: 'translating', feature: this.selection_.item(0), features: this.selection_, delta: [deltaX, deltaY], pixel: evt.pixel, coordinate: evt.coordinate }) break } case 'scale': { var center = this.center_ if (this.get('modifyCenter')(evt)) { var extentCoordinates = this.extent_ if (this.get('enableRotatedTransform') && viewRotation !== 0) { extentCoordinates = this.rotatedExtent_ } center = extentCoordinates[(Number(this.opt_) + 2) % 4] } var keepRectangle = (this.geoms_.length == 1 && this._isRectangle(this.geoms_[0])) var stretch = this.constraint_ var opt = this.opt_ var downCoordinate = this.coordinate_ var dragCoordinate = evt.coordinate if (this.get('enableRotatedTransform') && viewRotation !== 0) { var downPoint = new ol.geom.Point(this.coordinate_) downPoint.rotate(viewRotation * -1, center) downCoordinate = downPoint.getCoordinates() var dragPoint = new ol.geom.Point(evt.coordinate) dragPoint.rotate(viewRotation * -1, center) dragCoordinate = dragPoint.getCoordinates() } var scx = ((dragCoordinate)[0] - (center)[0]) / (downCoordinate[0] - (center)[0]) var scy = ((dragCoordinate)[1] - (center)[1]) / (downCoordinate[1] - (center)[1]) var displacementVector = [dragCoordinate[0] - downCoordinate[0], (dragCoordinate)[1] - downCoordinate[1]] if (this.get('enableRotatedTransform') && viewRotation !== 0) { var centerPoint = new ol.geom.Point(center) centerPoint.rotate(viewRotation * -1, this.getMap().getView().getCenter()) center = centerPoint.getCoordinates() } if (this.get('noFlip')) { if (scx < 0) scx = -scx if (scy < 0) scy = -scy } if (this.constraint_) { if (this.constraint_ == "h") scx = 1 else scy = 1 } else { if (this.get('keepAspectRatio')(evt)) { scx = scy = Math.min(scx, scy) } } for (i = 0, f; f = this.selection_.item(i); i++) { geometry = (viewRotation === 0 || !this.get('enableRotatedTransform')) ? this.geoms_[i].clone() : this.rotatedGeoms_[i].clone() geometry.applyTransform(function (g1, g2, dim) { if (dim < 2) return g2 if (!keepRectangle) { for (j = 0; j < g1.length; j += dim) { if (scx != 1) g2[j] = center[0] + (g1[j] - center[0]) * scx if (scy != 1) g2[j + 1] = center[1] + (g1[j + 1] - center[1]) * scy } } else { var pointArray = [[6], [0, 8], [2], [4]] var pointA = [g1[0], g1[1]] var pointB = [g1[2], g1[3]] var pointC = [g1[4], g1[5]] var pointD = [g1[6], g1[7]] var pointA1 = [g1[8], g1[9]] if (stretch) { var base = (opt % 2 === 0) ? this._countVector(pointA, pointB) : this._countVector(pointD, pointA) var projectedVector = this._projectVectorOnVector(displacementVector, base) var nextIndex = opt + 1 < pointArray.length ? opt + 1 : 0 var coordsToChange = [...pointArray[opt], ...pointArray[nextIndex]] for (j = 0; j < g1.length; j += dim) { g2[j] = coordsToChange.includes(j) ? g1[j] + projectedVector[0] : g1[j] g2[j + 1] = coordsToChange.includes(j) ? g1[j + 1] + projectedVector[1] : g1[j + 1] } } else { var projectedLeft, projectedRight switch (opt) { case 0: displacementVector = this._countVector(pointD, dragCoordinate) projectedLeft = this._projectVectorOnVector(displacementVector, this._countVector(pointC, pointD)) projectedRight = this._projectVectorOnVector(displacementVector, this._countVector(pointA, pointD)); [g2[0], g2[1]] = this._movePoint(pointA, projectedLeft); [g2[4], g2[5]] = this._movePoint(pointC, projectedRight); [g2[6], g2[7]] = this._movePoint(pointD, displacementVector); [g2[8], g2[9]] = this._movePoint(pointA1, projectedLeft) break case 1: displacementVector = this._countVector(pointA, dragCoordinate) projectedLeft = this._projectVectorOnVector(displacementVector, this._countVector(pointD, pointA)) projectedRight = this._projectVectorOnVector(displacementVector, this._countVector(pointB, pointA)); [g2[0], g2[1]] = this._movePoint(pointA, displacementVector); [g2[2], g2[3]] = this._movePoint(pointB, projectedLeft); [g2[6], g2[7]] = this._movePoint(pointD, projectedRight); [g2[8], g2[9]] = this._movePoint(pointA1, displacementVector) break case 2: displacementVector = this._countVector(pointB, dragCoordinate) projectedLeft = this._projectVectorOnVector(displacementVector, this._countVector(pointA, pointB)) projectedRight = this._projectVectorOnVector(displacementVector, this._countVector(pointC, pointB)); [g2[0], g2[1]] = this._movePoint(pointA, projectedRight); [g2[2], g2[3]] = this._movePoint(pointB, displacementVector); [g2[4], g2[5]] = this._movePoint(pointC, projectedLeft); [g2[8], g2[9]] = this._movePoint(pointA1, projectedRight) break case 3: displacementVector = this._countVector(pointC, dragCoordinate) projectedLeft = this._projectVectorOnVector(displacementVector, this._countVector(pointB, pointC)) projectedRight = this._projectVectorOnVector(displacementVector, this._countVector(pointD, pointC)); [g2[2], g2[3]] = this._movePoint(pointB, projectedRight); [g2[4], g2[5]] = this._movePoint(pointC, displacementVector); [g2[6], g2[7]] = this._movePoint(pointD, projectedLeft) break } } } // bug: ol, bad calculation circle geom extent if (geometry.getType() == 'Circle') geometry.setCenterAndRadius(geometry.getCenter(), geometry.getRadius()) return g2 }.bind(this)) if (this.get('enableRotatedTransform') && viewRotation !== 0) { //geometry.rotate(viewRotation, rotationCenter); geometry.rotate(viewRotation, this.getMap().getView().getCenter()) } f.setGeometry(geometry) } this.drawSketch_() this.dispatchEvent({ type: 'scaling', feature: this.selection_.item(0), features: this.selection_, scale: [scx, scy], pixel: evt.pixel, coordinate: evt.coordinate }) break } default: break } this.isUpdating_ = false } /** * @param {ol.MapBrowserEvent} evt Event. * @private */ handleMoveEvent_(evt) { if (!this._handleEvent(evt, this.features_)) return // console.log("handleMoveEvent"); if (!this.mode_) { var sel = this.getFeatureAtPixel_(evt.pixel) var element = evt.map.getTargetElement() if (sel.feature) { var c = sel.handle ? this.Cursors[(sel.handle || 'default') + (sel.constraint || '') + (sel.option || '')] : this.Cursors.select if (this.previousCursor_ === undefined) { this.previousCursor_ = element.style.cursor } element.style.cursor = c } else { if (this.previousCursor_ !== undefined) element.style.cursor = this.previousCursor_ this.previousCursor_ = undefined } } } /** * @param {ol.MapBrowserEvent} evt Map browser event. * @return {boolean} `false` to stop the drag sequence. */ handleUpEvent_(evt) { // remove rotate0 cursor on Up event, otherwise it's stuck on grab/grabbing if (this.mode_ === 'rotate') { var element = evt.map.getTargetElement() element.style.cursor = this.Cursors.default this.previousCursor_ = undefined } //dispatchEvent this.dispatchEvent({ type: this.mode_ + 'end', feature: this.selection_.item(0), features: this.selection_, oldgeom: this.geoms_[0], oldgeoms: this.geoms_ }) this.drawSketch_() this.mode_ = null return false } /** Set the point radius to calculate handles on points * @param {number|Array|function} [pointRadius=0] radius for points or a function that takes a feature and returns the radius (or [radiusX, radiusY]). If not null show handles to transform the points */ setPointRadius(pointRadius) { if (typeof (pointRadius) === 'function') { this._pointRadius = pointRadius } else { this._pointRadius = function () { return pointRadius } } } /** Get the features that are selected for transform * @return ol.Collection */ getFeatures() { return this.selection_; } /** * @private */ _projectVectorOnVector(displacement_vector, base) { var k = (displacement_vector[0] * base[0] + displacement_vector[1] * base[1]) / (base[0] * base[0] + base[1] * base[1]); return [base[0] * k, base[1] * k]; } /** * @private */ _countVector(start, end) { return [end[0] - start[0], end[1] - start[1]]; } /** * @private */ _movePoint(point, displacementVector) { return [point[0]+displacementVector[0], point[1]+displacementVector[1]]; } } /** Cursors for transform */ ol.interaction.Transform.prototype.Cursors = { 'default': 'auto', 'select': 'pointer', 'translate': 'move', 'rotate': 'move', 'rotate0': 'move', 'scale': 'nesw-resize', 'scale1': 'nwse-resize', 'scale2': 'nesw-resize', 'scale3': 'nwse-resize', 'scalev': 'ew-resize', 'scaleh1': 'ns-resize', 'scalev2': 'ew-resize', 'scaleh3': 'ns-resize' }; /** Undo/redo interaction * @constructor * @extends {ol.interaction.Interaction} * @fires undo * @fires redo * @fires change:add * @fires change:remove * @fires change:clear * @param {Object} options * @param {number=} options.maxLength max undo stack length (0=Infinity), default Infinity * @param {Array} options.layers array of layers to undo/redo */ ol.interaction.UndoRedo = class olinteractionUndoRedo extends ol.interaction.Interaction { constructor(options) { options = options || {} super({ handleEvent: function () { return true } }) //array of layers to undo/redo this._layers = options.layers this._undoStack = new ol.Collection() this._redoStack = new ol.Collection() // Zero level stack this._undo = [] this._redo = [] this._undoStack.on('add', function (e) { if (e.element.level === undefined) { e.element.level = this._level if (!e.element.level) { e.element.view = { center: this.getMap().getView().getCenter(), zoom: this.getMap().getView().getZoom() } this._undo.push(e.element) } } else { if (!e.element.level) this._undo.push(this._redo.shift()) } if (!e.element.level) { this.dispatchEvent({ type: 'stack:add', action: e.element }) } this._reduce() }.bind(this)) this._undoStack.on('remove', function (e) { if (!e.element.level) { if (this._doShift) { this._undo.shift() } else { if (this._undo.length) this._redo.push(this._undo.pop()) } if (!this._doClear) { this.dispatchEvent({ type: 'stack:remove', action: e.element, shift: this._doShift }) } } }.bind(this)) // Block counter this._block = 0 this._level = 0 // Shift an undo action ? this._doShift = false // Start recording this._record = true // Custom definitions this._defs = {} } /** Add a custom undo/redo * @param {string} action the action key name * @param {function} undoFn function called when undoing * @param {function} redoFn function called when redoing * @api */ define(action, undoFn, redoFn) { this._defs[action] = { undo: undoFn, redo: redoFn } } /** Get first level undo / redo length * @param {string} [type] get redo stack length, default get undo * @return {number} */ length(type) { return (type === 'redo') ? this._redo.length : this._undo.length } /** Set undo stack max length * @param {number} length */ setMaxLength(length) { length = parseInt(length) if (length && length < 0) length = 0 this.set('maxLength', length) this._reduce() } /** Get undo / redo size (includes all block levels) * @param {string} [type] get redo stack length, default get undo * @return {number} */ size(type) { return (type === 'redo') ? this._redoStack.getLength() : this._undoStack.getLength() } /** Set undo stack max size * @param {number} size */ setMaxSize(size) { size = parseInt(size) if (size && size < 0) size = 0 this.set('maxSize', size) this._reduce() } /** Reduce stack: shift undo to set size * @private */ _reduce() { if (this.get('maxLength')) { while (this.length() > this.get('maxLength')) { this.shift() } } if (this.get('maxSize')) { while (this.length() > 1 && this.size() > this.get('maxSize')) { this.shift() } } } /** Get first level undo / redo first level stack * @param {string} [type] get redo stack, default get undo * @return {Array<*>} */ getStack(type) { return (type === 'redo') ? this._redo : this._undo } /** Add a new custom undo/redo * @param {string} action the action key name * @param {any} prop an object that will be passed in the undo/redo functions of the action * @param {string} name action name * @return {boolean} true if the action is defined */ push(action, prop, name) { if (this._defs[action]) { this._undoStack.push({ type: action, name: name, custom: true, prop: prop }) return true } else { console.warn('[UndoRedoInteraction]: "' + action + '" is not defined.') return false } } /** Remove undo action from the beginning of the stack. * The action is not returned. */ shift() { this._doShift = true var a = this._undoStack.removeAt(0) this._doShift = false // Remove all block if (a.type === 'blockstart') { a = this._undoStack.item(0) while (this._undoStack.getLength() && a.level > 0) { this._undoStack.removeAt(0) a = this._undoStack.item(0) } } } /** Activate or deactivate the interaction, ie. records or not events on the map. * @param {boolean} active * @api stable */ setActive(active) { super.setActive(active) this._record = active } /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._mapListener) { this._mapListener.forEach(function (l) { ol.Observable.unByKey(l) }) } this._mapListener = [] super.setMap(map) // Watch blocks if (map) { this._mapListener.push(map.on('undoblockstart', this.blockStart.bind(this))) this._mapListener.push(map.on('undoblockend', this.blockEnd.bind(this))) } // Watch sources this._watchSources() this._watchInteractions() } /** Watch for changes in the map sources * @private */ _watchSources() { var map = this.getMap() // Clear listeners if (this._sourceListener) { this._sourceListener.forEach(function (l) { ol.Observable.unByKey(l) }) } this._sourceListener = [] var self = this // Ges vector layers function getVectorLayers(layers, init) { if (!init) init = [] layers.forEach(function (l) { if (l instanceof ol.layer.Vector) { if (!self._layers || self._layers.indexOf(l) >= 0) { init.push(l) } } else if (l.getLayers) { getVectorLayers(l.getLayers(), init) } }) return init } if (map) { // Watch the vector sources in the map var vectors = getVectorLayers(map.getLayers()) vectors.forEach((function (l) { var s = l.getSource() this._sourceListener.push(s.on(['addfeature', 'removefeature'], this._onAddRemove.bind(this))) this._sourceListener.push(s.on('clearstart', function () { this.blockStart('clear') }.bind(this))) this._sourceListener.push(s.on('clearend', this.blockEnd.bind(this))) }).bind(this)) // Watch new inserted/removed this._sourceListener.push(map.getLayers().on(['add', 'remove'], this._watchSources.bind(this))) } } /** Watch for interactions * @private */ _watchInteractions() { var map = this.getMap() // Clear listeners if (this._interactionListener) { this._interactionListener.forEach(function (l) { ol.Observable.unByKey(l) }) } this._interactionListener = [] if (map) { // Watch the interactions in the map map.getInteractions().forEach((function (i) { this._interactionListener.push(i.on( ['setattributestart', 'modifystart', 'rotatestart', 'translatestart', 'scalestart', 'deletestart', 'deleteend', 'beforesplit', 'aftersplit'], this._onInteraction.bind(this) )) }).bind(this)) // Watch new inserted / unwatch removed this._interactionListener.push(map.getInteractions().on( ['add', 'remove'], this._watchInteractions.bind(this) )) } } /** A feature is added / removed */ _onAddRemove(e) { if (this._record) { this._redoStack.clear() this._redo.length = 0 this._undoStack.push({ type: e.type, source: e.target, feature: e.feature }) } } /** Perform an interaction * @private */ _onInteraction(e) { var fn = this._onInteraction[e.type] if (fn) fn.call(this, e) } /** Start an undo block * @param {string} [name] name f the action * @api */ blockStart(name) { this._redoStack.clear() this._redo.length = 0 this._undoStack.push({ type: 'blockstart', name: name }) this._level++ } /** End an undo block * @api */ blockEnd() { this._undoStack.push({ type: 'blockend' }) this._level-- } /** handle undo/redo * @private */ _handleDo(e, undo) { // Not active if (!this.getActive()) return // Stop recording while undoing this._record = false if (e.custom) { if (this._defs[e.type]) { if (undo) this._defs[e.type].undo(e.prop) else this._defs[e.type].redo(e.prop) } else { console.warn('[UndoRedoInteraction]: "' + e.type + '" is not defined.') } } else { switch (e.type) { case 'addfeature': { if (undo) e.source.removeFeature(e.feature) else e.source.addFeature(e.feature) break } case 'removefeature': { if (undo) e.source.addFeature(e.feature) else e.source.removeFeature(e.feature) break } case 'changegeometry': { var geom = e.feature.getGeometry() e.feature.setGeometry(e.oldGeom) e.oldGeom = geom break } case 'changeattribute': { var newp = e.newProperties var oldp = e.oldProperties for (var p in oldp) { if (oldp === undefined) e.feature.unset(p) else e.feature.set(p, oldp[p]) } e.oldProperties = newp e.newProperties = oldp break } case 'blockstart': { this._block += undo ? -1 : 1 break } case 'blockend': { this._block += undo ? 1 : -1 break } default: { console.warn('[UndoRedoInteraction]: "' + e.type + '" is not defined.') } } } // Handle block if (this._block < 0) this._block = 0 if (this._block) { if (undo) this.undo() else this.redo() } this._record = true // Dispatch event this.dispatchEvent({ type: undo ? 'undo' : 'redo', action: e }) } /** Undo last operation * @api */ undo() { var e = this._undoStack.item(this._undoStack.getLength() - 1) if (!e) return this._redoStack.push(e) this._undoStack.pop() this._handleDo(e, true) } /** Redo last operation * @api */ redo() { var e = this._redoStack.item(this._redoStack.getLength() - 1) if (!e) return this._undoStack.push(e) this._redoStack.pop() this._handleDo(e, false) } /** Clear undo stack * @api */ clear() { this._doClear = true this._undo.length = this._redo.length = 0 this._undoStack.clear() this._redoStack.clear() this._doClear = false this.dispatchEvent({ type: 'stack:clear' }) } /** Check if undo is avaliable * @return {number} the number of undo * @api */ hasUndo() { return this._undoStack.getLength() } /** Check if redo is avaliable * @return {number} the number of redo * @api */ hasRedo() { return this._redoStack.getLength() } } /** Set attribute * @private */ ol.interaction.UndoRedo.prototype._onInteraction.setattributestart = function(e) { this.blockStart(e.target.get('name') || 'setattribute'); var newp = Object.assign({}, e.properties); e.features.forEach(function(f) { var oldp = {}; for (var p in newp) { oldp[p] = f.get(p); } this._undoStack.push({ type: 'changeattribute', feature: f, newProperties: newp, oldProperties: oldp }); }.bind(this)); this.blockEnd(); }; ol.interaction.UndoRedo.prototype._onInteraction.rotatestart = ol.interaction.UndoRedo.prototype._onInteraction.translatestart = ol.interaction.UndoRedo.prototype._onInteraction.scalestart = ol.interaction.UndoRedo.prototype._onInteraction.modifystart = function (e) { this.blockStart(e.type.replace(/start$/,'')); e.features.forEach(function(m) { this._undoStack.push({ type: 'changegeometry', feature: m, oldGeom: m.getGeometry().clone() }); }.bind(this)); this.blockEnd(); }; /** @private */ ol.interaction.UndoRedo.prototype._onInteraction.beforesplit = function() { // Check modify before split var l = this._undoStack.getLength(); if (l>2 && this._undoStack.item(l-1).type === 'blockend' && this._undoStack.item(l-2).type === 'changegeometry') { this._undoStack.pop(); } else { this.blockStart('split'); } }; ol.interaction.UndoRedo.prototype._onInteraction.deletestart = function() { this.blockStart('delete'); } /** @private */ ol.interaction.UndoRedo.prototype._onInteraction.aftersplit = ol.interaction.UndoRedo.prototype._onInteraction.deleteend = ol.interaction.UndoRedo.prototype.blockEnd; /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Abstract base class; normally only used for creating subclasses. Bin collector for data * @constructor * @extends {ol.source.Vector} * @param {Object} options ol.source.VectorOptions + grid option * @param {ol.source.Vector} options.source Source * @param {boolean} options.listenChange listen changes (move) on source features to recalculate the bin, default true * @param {fucntion} [options.geometryFunction] Function that takes an ol.Feature as argument and returns an ol.geom.Point as feature's center. * @param {function} [options.flatAttributes] Function takes a bin and the features it contains and aggragate the features in the bin attributes when saving */ ol.source.BinBase = class olsourceBinBase extends ol.source.Vector { constructor(options) { options = options || {}; super(options); this._bindModify = this._onModifyFeature.bind(this); this._watch = true; this._origin = options.source; this._listen = (options.listenChange !== false); // Geometry function this._geomFn = options.geometryFunction || ol.coordinate.getFeatureCenter || function (f) { return f.getGeometry().getFirstCoordinate(); }; // Future features this._origin.on('addfeature', this._onAddFeature.bind(this)); this._origin.on('removefeature', this._onRemoveFeature.bind(this)); this._origin.on('clearstart', this._onClearFeature.bind(this)); this._origin.on('clearend', this._onClearFeature.bind(this)); if (typeof (options.flatAttributes) === 'function') { this._flatAttributes = options.flatAttributes; } // Handle exsting feature (should be called from children when fully created) // this.reset() } /** * On add feature * @param {ol.events.Event} e * @param {ol.Feature} bin * @private */ _onAddFeature(e, bin, listen) { var f = e.feature || e.target; bin = bin || this.getBinAt(this._geomFn(f), true); if (bin) bin.get('features').push(f); if (this._listen && listen !== false) f.on('change', this._bindModify); } /** * On remove feature * @param {ol.events.Event} e * @param {ol.Feature} bin * @private */ _onRemoveFeature(e, bin, listen) { if (!this._watch) return; var f = e.feature || e.target; bin = bin || this.getBinAt(this._geomFn(f)); if (bin) { // Remove feature from bin var features = bin.get('features'); for (var i = 0, fi; fi = features[i]; i++) { if (fi === f) { features.splice(i, 1); break; } } // Remove bin if no features if (!features.length) { this.removeFeature(bin); } } else { // console.log("[ERROR:Bin] remove feature: feature doesn't exists anymore."); } if (this._listen && listen !== false) f.un('change', this._bindModify); } /** When clearing features remove the listener * @private */ _onClearFeature(e) { if (e.type === 'clearstart') { if (this._listen) { this._origin.getFeatures().forEach(function (f) { f.un('change', this._bindModify); }.bind(this)); } this.clear(); this._watch = false; } else { this._watch = true; } } /** * Get the bin that contains a feature * @param {ol.Feature} f the feature * @return {ol.Feature} the bin or null it doesn't exit */ getBin(feature) { var bins = this.getFeatures(); for (var i = 0, b; b = bins[i]; i++) { var features = b.get('features'); for (var j = 0, f; f = features[j]; j++) { if (f === feature) return b; } } return null; } /** Get the grid geometry at the coord * @param {ol.Coordinate} coord * @param {Object} attributes add key/value to this object to add properties to the grid feature * @returns {ol.geom.Polygon} * @api */ getGridGeomAt(coord /*, attributes */) { return new ol.geom.Polygon([coord]); } /** Get the bean at a coord * @param {ol.Coordinate} coord * @param {boolean} create true to create if doesn't exit * @return {ol.Feature} the bin or null it doesn't exit */ getBinAt(coord, create) { var attributes = {}; var g = this.getGridGeomAt(coord, attributes); if (!g) return null; var center = g.getInteriorPoint ? g.getInteriorPoint().getCoordinates() : g.getInteriorPoints().getCoordinates()[0]; // ol.extent.getCenter(g.getExtent()); var features = this.getFeaturesAtCoordinate(center); var bin = features[0]; if (!bin && create) { attributes.geometry = g; attributes.features = []; attributes.center = center; bin = new ol.Feature(attributes); this.addFeature(bin); } return bin || null; } /** * A feature has been modified * @param {ol.events.Event} e * @private */ _onModifyFeature(e) { var bin = this.getBin(e.target); var bin2 = this.getBinAt(this._geomFn(e.target), 'create'); if (bin !== bin2) { // remove from the bin if (bin) { this._onRemoveFeature(e, bin, false); } // insert in the new bin if (bin2) { this._onAddFeature(e, bin2, false); } } this.changed(); } /** Clear all bins and generate a new one. */ reset() { this.clear(); var features = this._origin.getFeatures(); for (var i = 0, f; f = features[i]; i++) { this._onAddFeature({ feature: f }); } this.changed(); } /** * Get features without circular dependencies (vs. getFeatures) * @return {Array} */ getGridFeatures() { var features = []; this.getFeatures().forEach(function (f) { var bin = new ol.Feature(f.getGeometry().clone()); for (var i in f.getProperties()) { if (i !== 'features' && i !== 'geometry') { bin.set(i, f.get(i)); } } bin.set('nb', f.get('features').length); this._flatAttributes(bin, f.get('features')); features.push(bin); }.bind(this)); return features; } /** Create bin attributes using the features it contains when exporting * @param {ol.Feature} bin the bin to export * @param {Array} features the features it contains */ _flatAttributes( /*bin, features*/) { } /** Set the flatAttribute function * @param {function} fn Function that takes a bin and the features it contains and aggragate the features in the bin attributes when saving */ setFlatAttributesFn(fn) { if (typeof (fn) === 'function') this._flatAttributes = fn; } /** * Get the orginal source * @return {ol.source.Vector} */ getSource() { return this._origin; } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). @classdesc ol.source.DBPedia is a DBPedia layer source that load DBPedia located content in a vector layer. olx.source.DBPedia: olx.source.Vector { url: {string} Url for DBPedia SPARQL } Inherits from: */ /** * @constructor ol.source.DBPedia * @extends {ol.source.Vector} * @param {olx.source.DBPedia=} opt_options */ ol.source.DBPedia = class olsourceDBPedia extends ol.source.Vector { constructor(opt_options) { var options = opt_options || {} /** Default attribution */ if (!options.attributions) options.attributions = [ '© DBpedia CC-by-SA'] // Bbox strategy : reload at each move if (!options.strategy) options.strategy = ol.loadingstrategy.bbox super(options) this.setLoader(this._loaderFn) /** Url for DBPedia SPARQL */ this._url = options.url || 'http://fr.dbpedia.org/sparql' /** Max resolution to load features */ this._maxResolution = options.maxResolution || 100 /** Result language */ this._lang = options.lang || "fr" /** Query limit */ this._limit = options.limit || 1000 } /** Decode RDF attributes and choose to add feature to the layer * @param {feature} the feature * @param {attributes} RDF attributes * @param {lastfeature} last feature added (null if none) * @return {boolean} true: add the feature to the layer * @API stable */ readFeature(feature, attributes, lastfeature) { // Copy RDF attributes values for (var i in attributes) { if (attributes[i].type === 'uri') attributes[i].value = encodeURI(attributes[i].value) feature.set(i, attributes[i].value) } // Prevent same feature with different type duplication if (lastfeature && lastfeature.get("subject") == attributes.subject.value) { // Kepp dbpedia.org type ? // if (bindings[i].type.match ("dbpedia.org") lastfeature.get("type") = bindings[i].type.value; // Concat types lastfeature.set("type", lastfeature.get("type") + "\n" + attributes.type.value) return false } else { return true } } /** Set RDF query subject, default: select label, thumbnail, abstract and type * @API stable */ querySubject() { return "?subject rdfs:label ?label. " + "OPTIONAL {?subject dbo:thumbnail ?thumbnail}." + "OPTIONAL {?subject dbo:abstract ?abstract} . " + "OPTIONAL {?subject rdf:type ?type}" } /** Set RDF query filter, default: select language * @API stable */ queryFilter() { return "lang(?label) = '" + this._lang + "' " + "&& lang(?abstract) = '" + this._lang + "'" // Filter on type //+ "&& regex (?type, 'Monument|Sculpture|Museum', 'i')" } /** Loader function used to load features. * @private */ _loaderFn(extent, resolution, projection) { if (resolution > this._maxResolution) return var self = this var bbox = ol.proj.transformExtent(extent, projection, "EPSG:4326") // SPARQL request: for more info @see http://fr.dbpedia.org/ var query = "PREFIX geo: " + "SELECT DISTINCT * WHERE { " + "?subject geo:lat ?lat . " + "?subject geo:long ?long . " + this.querySubject() + " . " + "FILTER(" + this.queryFilter() + ") . " // Filter bbox + "FILTER(xsd:float(?lat) <= " + bbox[3] + " && " + bbox[1] + " <= xsd:float(?lat) " + "&& xsd:float(?long) <= " + bbox[2] + " && " + bbox[0] + " <= xsd:float(?long) " + ") . " + "} LIMIT " + this._limit // Ajax request to get the tile ol.ext.Ajax.get({ url: this._url, data: { query: query, format: "json" }, success: function (data) { var bindings = data.results.bindings var features = [] var att, pt, feature, lastfeature = null for (var i in bindings) { att = bindings[i] pt = [Number(bindings[i].long.value), Number(bindings[i].lat.value)] feature = new ol.Feature(new ol.geom.Point(ol.proj.transform(pt, "EPSG:4326", projection))) if (self.readFeature(feature, att, lastfeature)) { features.push(feature) lastfeature = feature } } self.addFeatures(features) } }) } } ol.style.clearDBPediaStyleCache; ol.style.dbPediaStyleFunction; (function(){ // Style cache var styleCache = {}; /** Reset the cache (when fonts are loaded) */ ol.style.clearDBPediaStyleCache = function() { styleCache = {}; } /** Get a default style function for dbpedia * @param {} options * @param {string|function|undefined} options.glyph a glyph name or a function that takes a feature and return a glyph * @param {number} options.radius radius of the symbol, default 8 * @param {ol.style.Fill} options.fill style for fill, default navy * @param {ol.style.stroke} options.stroke style for stroke, default 2px white * @param {string} options.prefix a prefix if many style used for the same type * * @require ol.style.FontSymbol and FontAwesome defs are required for dbPediaStyleFunction() */ ol.style.dbPediaStyleFunction = function(options) { if (!options) options={}; // Get font function using dbPedia type var getFont; switch (typeof(options.glyph)) { case "function": getFont = options.glyph; break; case "string": getFont = function(){ return options.glyph; }; break; default: { getFont = function (f) { var type = f.get("type"); if (type) { if (type.match("/Museum")) return "fa-camera"; else if (type.match("/Monument")) return "fa-building"; else if (type.match("/Sculpture")) return "fa-android"; else if (type.match("/Religious")) return "fa-institution"; else if (type.match("/Castle")) return "fa-key"; else if (type.match("Water")) return "fa-tint"; else if (type.match("Island")) return "fa-leaf"; else if (type.match("/Event")) return "fa-heart"; else if (type.match("/Artwork")) return "fa-asterisk"; else if (type.match("/Stadium")) return "fa-futbol-o"; else if (type.match("/Place")) return "fa-street-view"; } return "fa-star"; } break; } } // Default values var radius = options.radius || 8; var fill = options.fill || new ol.style.Fill({ color:"navy"}); var stroke = options.stroke || new ol.style.Stroke({ color: "#fff", width: 2 }); var prefix = options.prefix ? options.prefix+"_" : ""; // Vector style function return function (feature) { var glyph = getFont(feature); var k = prefix + glyph; var style = styleCache[k]; if (!style) { styleCache[k] = style = new ol.style.Style ({ image: new ol.style.FontSymbol({ glyph: glyph, radius: radius, fill: fill, stroke: stroke }) }); } return [style]; } }; })(); /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** DFCI source: a source to display the French DFCI grid on a map * @see http://ccffpeynier.free.fr/Files/dfci.pdf * @constructor ol.source.DFCI * @extends {ol/source/Vector} * @param {any} options Vector source options * @param {Array} resolutions a list of resolution to change the drawing level, default [1000,100,20] */ ol.source.DFCI = class olsourceDFCI extends ol.source.Vector { constructor(options) { options = options || {} options.loader = function(extent, resolution, projection) { return this._calcGrid(extent, resolution, projection) } options.strategy = function (extent, resolution) { if (this.resolution && this.resolution != resolution) { this.clear() this.refresh() } return [extent] } super(options) this._bbox = [[0, 1600000], [11 * 100000, 1600000 + 10 * 100000]] this.set('resolutions', options.resolutions || [1000, 100, 20]) // Add Lambert IIe proj if (!proj4.defs["EPSG:27572"]) proj4.defs("EPSG:27572", "+proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +a=6378249.2 +b=6356515 +towgs84=-168,-60,320,0,0,0,0 +pm=paris +units=m +no_defs") ol.proj.proj4.register(proj4) } /** Cacluate grid according extent/resolution */ _calcGrid(extent, resolution, projection) { // Show step 0 var f, ext, res = this.get('resolutions') if (resolution > (res[0] || 1000)) { if (this.resolution != resolution) { if (!this._features0) { ext = [this._bbox[0][0], this._bbox[0][1], this._bbox[1][0], this._bbox[1][1]] this._features0 = this._getFeatures(0, ext, projection) } this.addFeatures(this._features0) } } else if (resolution > (res[1] || 100)) { this.clear() ext = ol.proj.transformExtent(extent, projection, 'EPSG:27572') f = this._getFeatures(1, ext, projection) this.addFeatures(f) } else if (resolution > (res[2] || 0)) { this.clear() ext = ol.proj.transformExtent(extent, projection, 'EPSG:27572') f = this._getFeatures(2, ext, projection) this.addFeatures(f) } else { this.clear() ext = ol.proj.transformExtent(extent, projection, 'EPSG:27572') f = this._getFeatures(3, ext, projection) this.addFeatures(f) } // reset load this.resolution = resolution } /** * Get middle point * @private */ _midPt(p1, p2) { return [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2] } /** * Get feature with geom * @private */ _trFeature(geom, id, level, projection) { var g = new ol.geom.Polygon([geom]) var f = new ol.Feature(g.transform('EPSG:27572', projection)) f.set('id', id) f.set('level', level) return f } /** Get features * */ _getFeatures(level, extent, projection) { var features = [] var i var step = 100000 if (level > 0) step /= 5 if (level > 1) step /= 10 var p0 = [ Math.max(this._bbox[0][0], Math.floor(extent[0] / step) * step), Math.max(this._bbox[0][1], Math.floor(extent[1] / step) * step) ] var p1 = [ Math.min(this._bbox[1][0] + 99999, Math.floor(extent[2] / step) * step), Math.min(this._bbox[1][1] + 99999, Math.floor(extent[3] / step) * step) ] for (var x = p0[0]; x <= p1[0]; x += step) { for (var y = p0[1]; y <= p1[1]; y += step) { var p, geom = [[x, y], [x + step, y], [x + step, y + step], [x, y + step], [x, y]] if (level > 2) { var m = this._midPt(geom[0], geom[2]) // .5 var g = [] for (i = 0; i < geom.length; i++) { g.push(this._midPt(geom[i], m)) } features.push(this._trFeature(g, ol.coordinate.toDFCI([x, y], 2) + '.5', level, projection)) // .1 > .4 for (i = 0; i < 4; i++) { g = [] g.push(geom[i]) g.push(this._midPt(geom[i], geom[(i + 1) % 4])) g.push(this._midPt(m, g[1])) g.push(this._midPt(m, geom[i])) p = this._midPt(geom[i], geom[(i + 3) % 4]) g.push(this._midPt(m, p)) g.push(p) g.push(geom[i]) features.push(this._trFeature(g, ol.coordinate.toDFCI([x, y], 2) + '.' + (4 - i), level, projection)) } } else { features.push(this._trFeature(geom, ol.coordinate.toDFCI([x, y], level), level, projection)) } } } return features } } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** DayNight source: a source to display day/night on a map * @constructor * @extends {ol.source.Vector} * @param {any} options Vector source options * @param {string|Date} time source date time * @param {number} step step in degree for coordinate precision */ ol.source.DayNight = class olsourceDayNight extends ol.source.Vector { constructor(options) { options = options || {}; options.strategy = ol.loadingstrategy.all; super(options); this.setLoader(this._loader); this.set('time', options.time || new Date()); this.set('step', options.step || 1); } /** Compute the position of the Sun in ecliptic coordinates at julianDay. * @see http://en.wikipedia.org/wiki/Position_of_the_Sun * @param {number} julianDay * @private */ static _sunEclipticPosition(julianDay) { var deg2rad = Math.PI / 180; // Days since start of J2000.0 var n = julianDay - 2451545.0; // mean longitude of the Sun var L = 280.460 + 0.9856474 * n; L %= 360; // mean anomaly of the Sun var g = 357.528 + 0.9856003 * n; g %= 360; // ecliptic longitude of Sun var lambda = L + 1.915 * Math.sin(g * deg2rad) + 0.02 * Math.sin(2 * g * deg2rad); // distance from Sun in AU var R = 1.00014 - 0.01671 * Math.cos(g * deg2rad) - 0.0014 * Math.cos(2 * g * deg2rad); return { lambda: lambda, R: R }; } /** * @see http://en.wikipedia.org/wiki/Axial_tilt#Obliquity_of_the_ecliptic_.28Earth.27s_axial_tilt.29 * @param {number} julianDay * @private */ static _eclipticObliquity(julianDay) { var n = julianDay - 2451545.0; // Julian centuries since J2000.0 var T = n / 36525; var epsilon = 23.43929111 - T * (46.836769 / 3600 - T * (0.0001831 / 3600 + T * (0.00200340 / 3600 - T * (0.576e-6 / 3600 - T * 4.34e-8 / 3600)))); return epsilon; } /* Compute the Sun's equatorial position from its ecliptic position. * @param {number} sunEclLng sun lon in degrees * @param {number} eclObliq secliptic position in degrees * @return {number} position in degrees * @private */ static _sunEquatorialPosition(sunEclLon, eclObliq) { var rad2deg = 180 / Math.PI; var deg2rad = Math.PI / 180; var alpha = Math.atan(Math.cos(eclObliq * deg2rad) * Math.tan(sunEclLon * deg2rad)) * rad2deg; var delta = Math.asin(Math.sin(eclObliq * deg2rad) * Math.sin(sunEclLon * deg2rad)) * rad2deg; var lQuadrant = Math.floor(sunEclLon / 90) * 90; var raQuadrant = Math.floor(alpha / 90) * 90; alpha = alpha + (lQuadrant - raQuadrant); return { alpha: alpha, delta: delta }; } /** Get the day/night separation latitude * @param {number} lon * @param {Date} time * @returns {number} */ static getNightLat(lon, time) { var rad2deg = 180 / Math.PI; var deg2rad = Math.PI / 180; var date = time ? new Date(time) : new Date(); // Calculate the present UTC Julian Date. // Function is valid after the beginning of the UNIX epoch 1970-01-01 and ignores leap seconds. var julianDay = (date / 86400000) + 2440587.5; // Calculate Greenwich Mean Sidereal Time (low precision equation). // http://aa.usno.navy.mil/faq/docs/GAST.php var gst = (18.697374558 + 24.06570982441908 * (julianDay - 2451545.0)) % 24; var sunEclPos = ol.source.DayNight._sunEclipticPosition(julianDay); var eclObliq = ol.source.DayNight._eclipticObliquity(julianDay); var sunEqPos = ol.source.DayNight._sunEquatorialPosition(sunEclPos.lambda, eclObliq); // Hour angle (indegrees) of the sun for a longitude on Earth. var ha = (gst * 15 + lon) - sunEqPos.alpha; // Latitude var lat = Math.atan(-Math.cos(ha * deg2rad) / Math.tan(sunEqPos.delta * deg2rad)) * rad2deg; return lat; } /** Loader * @private */ _loader(extent, resolution, projection) { var lonlat = this.getCoordinates(this.get('time')); var geom = new ol.geom.Polygon([lonlat]); geom.transform('EPSG:4326', projection); this.addFeature(new ol.Feature(geom)); } /** Set source date time * @param {string|Date} time source date time */ setTime(time) { this.set('time', time); this.refresh(); } /** Get sun coordinates on earth * @param {string} time DateTime string, default yet * @returns {ol.coordinate} position in lonlat */ getSunPosition(time) { var date = time ? new Date(time) : new Date(); // Calculate the present UTC Julian Date. // Function is valid after the beginning of the UNIX epoch 1970-01-01 and ignores leap seconds. var julianDay = (date / 86400000) + 2440587.5; // Calculate Greenwich Mean Sidereal Time (low precision equation). // http://aa.usno.navy.mil/faq/docs/GAST.php var gst = (18.697374558 + 24.06570982441908 * (julianDay - 2451545.0)) % 24; var sunEclPos = ol.source.DayNight._sunEclipticPosition(julianDay); var eclObliq = ol.source.DayNight._eclipticObliquity(julianDay); var sunEqPos = ol.source.DayNight._sunEquatorialPosition(sunEclPos.lambda, eclObliq); return [sunEqPos.alpha - gst * 15, sunEqPos.delta]; } /** Get night-day separation line * @param {string} time DateTime string, default yet * @param {string} options use 'line' to get the separation line, 'day' to get the day polygon, 'night' to get the night polygon or 'daynight' to get both polygon, default 'night' * @return {Array|Array>} */ getCoordinates(time, options) { var rad2deg = 180 / Math.PI; var deg2rad = Math.PI / 180; var date = time ? new Date(time) : new Date(); // Calculate the present UTC Julian Date. // Function is valid after the beginning of the UNIX epoch 1970-01-01 and ignores leap seconds. var julianDay = (date / 86400000) + 2440587.5; // Calculate Greenwich Mean Sidereal Time (low precision equation). // http://aa.usno.navy.mil/faq/docs/GAST.php var gst = (18.697374558 + 24.06570982441908 * (julianDay - 2451545.0)) % 24; var lonlat = []; var sunEclPos = ol.source.DayNight._sunEclipticPosition(julianDay); var eclObliq = ol.source.DayNight._eclipticObliquity(julianDay); var sunEqPos = ol.source.DayNight._sunEquatorialPosition(sunEclPos.lambda, eclObliq); var step = this.get('step') || 1; for (var i = -180; i <= 180; i += step) { var lon = i; // Hour angle (indegrees) of the sun for a longitude on Earth. var ha = (gst * 15 + lon) - sunEqPos.alpha; // Latitude var lat = Math.atan(-Math.cos(ha * deg2rad) / Math.tan(sunEqPos.delta * deg2rad)) * rad2deg; // New point lonlat.push([lon, lat]); } switch (options) { case 'line': break; case 'day': sunEqPos.delta *= -1; // fallthrough default: { // Close polygon lat = (sunEqPos.delta < 0) ? 90 : -90; for (var tlon = 180; tlon >= -180; tlon -= step) { lonlat.push([tlon, lat]); } lonlat.push(lonlat[0]); break; } } // Return night + day polygon if (options === 'daynight') { var day = []; lonlat.forEach(function (t) { day.push(t.slice()); }); day[0][1] = -day[0][1]; day[day.length - 1][1] = -day[0][1]; day[day.length - 1][1] = -day[0][1]; lonlat = [lonlat, day]; } // Return polygon return lonlat; } } /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ /** Delaunay source * Calculate a delaunay triangulation from points in a source * @param {*} options extend ol/source/Vector options * @param {ol/source/Vector} options.source the source that contains the points */ ol.source.Delaunay = class olsourceDelaunay extends ol.source.Vector { constructor(options) { options = options || {} var source = options.source delete options.source super(options) // Source this._nodes = source; // Convex hull this.hull = [] // A new node is added to the source node: calculate the new triangulation this._nodes.on('addfeature', this._onAddNode.bind(this)) // A new node is removed from the source node: calculate the new triangulation this._nodes.on('removefeature', this._onRemoveNode.bind(this)) this.set('epsilon', options.epsilon || .0001) } /** Clear source (and points) * @param {boolean} opt_fast */ clear(opt_fast) { super.clear(opt_fast) this.getNodeSource().clear(opt_fast) } /** Add a new triangle in the source * @param {Array
              } pts */ _addTriangle(pts) { pts.push(pts[0]) var triangle = new ol.Feature(new ol.geom.Polygon([pts])) this.addFeature(triangle) this.flip.push(triangle) return triangle } /** Get nodes */ getNodes() { return this._nodes.getFeatures() } /** Get nodes source */ getNodeSource() { return this._nodes } /** * A point has been removed * @param {ol/source/Vector.Event} evt */ _onRemoveNode(evt) { // console.log(evt) var pt = evt.feature.getGeometry().getCoordinates() if (!pt) return // Still there (when removing duplicated points) if (this.getNodesAt(pt).length) return // console.log('removenode', evt.feature) // Get associated triangles var triangles = this.getTrianglesAt(pt) this.flip = [] // Get hole var i var edges = [] while (triangles.length) { var tr = triangles.pop() this.removeFeature(tr) tr = tr.getGeometry().getCoordinates()[0] var pts = [] for (i = 0; i < 3; i++) { p = tr[i] if (!ol.coordinate.equal(p, pt)) { pts.push(p) } } edges.push(pts) } pts = edges.pop() /* DEBUG var se = ''; edges.forEach(function(e){ se += ' - '+this.listpt(e); }.bind(this)); console.log('EDGES', se); */ i = 0 function testEdge(p0, p1, index) { if (ol.coordinate.equal(p0, pts[index])) { if (index) pts.push(p1) else pts.unshift(p1) return true } return false } while (true) { var e = edges[i] if (testEdge(e[0], e[1], 0) || testEdge(e[1], e[0], 0) || testEdge(e[0], e[1], pts.length - 1) || testEdge(e[1], e[0], pts.length - 1)) { edges.splice(i, 1) i = 0 } else { i++ } if (!edges.length) break if (i >= edges.length) { // console.log(this.listpt(pts), this.listpt(edges)); throw '[DELAUNAY:removePoint] No edge found' } } // Closed = interior // console.log('PTS', this.listpt(pts)) var closed = ol.coordinate.equal(pts[0], pts[pts.length - 1]) if (closed) pts.pop() // Update convex hull: remove pt + add new ones var p for (i; p = this.hull[i]; i++) { if (ol.coordinate.equal(pt, p)) { this.hull.splice(i, 1) break } } this.hull = ol.coordinate.convexHull(this.hull.concat(pts)) // select.getFeatures().clear(); // var clockwise = function (t) { var i1, s = 0 for (var i = 0; i < t.length; i++) { i1 = (i + 1) % t.length s += (t[i1][0] - t[i][0]) * (t[i1][1] + t[i][1]) } // console.log(s) return (s >= 0 ? 1 : -1) } // Add ears // interior point : ear area and object area have the same sign // extrior point : add a new point and close var clock var enveloppe = pts.slice() if (closed) { clock = clockwise(pts) } else { // console.log('ouvert', pts, pts.slice().push(pt)) enveloppe.push(pt) clock = clockwise(enveloppe) } // console.log('S=',clock,'CLOSED',closed) // console.log('E=',this.listpt(enveloppe)) for (i = 0; i <= pts.length + 1; i++) { if (pts.length < 3) break var t = [ pts[i % pts.length], pts[(i + 1) % pts.length], pts[(i + 2) % pts.length] ] if (clockwise(t) === clock) { var ok = true for (var k = i + 3; k < i + pts.length; k++) { // console.log('test '+k, this.listpt([pts[k % pts.length]])) if (this.inCircle(pts[k % pts.length], t)) { ok = false break } } if (ok) { // console.log(this.listpt(t),'ok'); this._addTriangle(t) // remove pts.splice((i + 1) % pts.length, 1) // and restart i = -1 } } // else console.log(this.listpt(t),'nok'); } /* DEBUG * / if (pts.length>3) console.log('oops'); console.log('LEAV',this.listpt(pts)); var ul = $('ul.triangles').html(''); $('
            1. ') .text('E:'+this.listpt(enveloppe)+' - '+clock+' - '+closed) .data('triangle', new ol.Feature(new ol.geom.Polygon([enveloppe]))) .click(function(){ var t = $(this).data('triangle'); select.getFeatures().clear(); select.getFeatures().push(t); }) .appendTo(ul); for (var i=0; i') .text(this.listpt(this.flip[i].getGeometry().getCoordinates()[0]) +' - ' + clockwise(this.flip[i].getGeometry().getCoordinates()[0])) .data('triangle', this.flip[i]) .click(function(){ var t = $(this).data('triangle'); select.getFeatures().clear(); select.getFeatures().push(t); }) .appendTo(ul); } /**/ // Flip? this.flipTriangles() } /** * A new point has been added * @param {ol/source/VectorEvent} e */ _onAddNode(e) { var finserted = e.feature var i, p // Not a point! if (finserted.getGeometry().getType() !== 'Point') { this._nodes.removeFeature(finserted) return } // Reset flip table this.flip = [] var nodes = this.getNodes() // The point var pt = finserted.getGeometry().getCoordinates() // Test existing point if (this.getNodesAt(pt).length > 1) { // console.log('remove duplicated points') this._nodes.removeFeature(finserted) return } // Triangle needs at least 3 points if (nodes.length <= 3) { if (nodes.length === 3) { var pts = [] for (i = 0; i < 3; i++) pts.push(nodes[i].getGeometry().getCoordinates()) this._addTriangle(pts) this.hull = ol.coordinate.convexHull(pts) } return } // Get the triangle var t = this.getFeaturesAtCoordinate(pt)[0] if (t) { this.removeFeature(t) t.set('del', true) var c = t.getGeometry().getCoordinates()[0] for (i = 0; i < 3; i++) { this._addTriangle([pt, c[i], c[(i + 1) % 3]]) } } else { // Calculate new convex hull var hull2 = this.hull.slice() hull2.push(pt) hull2 = ol.coordinate.convexHull(hull2) // Search for points for (i = 0; p = hull2[i]; i++) { if (ol.coordinate.equal(p, pt)) break } i = (i !== 0 ? i - 1 : hull2.length - 1) var p0 = hull2[i] var stop = hull2[(i + 2) % hull2.length] for (i = 0; p = this.hull[i]; i++) { if (ol.coordinate.equal(p, p0)) break } // Connect to the hull while (true) { // DEBUG: prevent infinit loop if (i > 1000) { console.error('[DELAUNAY:addPoint] Too many iterations') break } i++ p = this.hull[i % this.hull.length] this._addTriangle([pt, p, p0]) p0 = p if (p[0] === stop[0] && p[1] === stop[1]) break } this.hull = hull2 } this.flipTriangles() } /** Flipping algorithme: test new inserted triangle and flip */ flipTriangles() { var count = 1000 // Count to prevent too many iterations var pi while (this.flip.length) { // DEBUG: prevent infinite loop if (count-- < 0) { console.error('[DELAUNAY:flipTriangles] Too many iterations') break } var tri = this.flip.pop() if (tri.get('del')) continue var ti = tri.getGeometry().getCoordinates()[0] for (var k = 0; k < 3; k++) { // Get facing triangles var mid = [(ti[(k + 1) % 3][0] + ti[k][0]) / 2, (ti[(k + 1) % 3][1] + ti[k][1]) / 2] var triangles = this.getTrianglesAt(mid) var pt1 = null // Get opposite point if (triangles.length > 1) { var t0 = triangles[0].getGeometry().getCoordinates()[0] var t1 = triangles[1].getGeometry().getCoordinates()[0] for (pi = 0; pi < t1.length; pi++) { if (!this._ptInTriangle(t1[pi], t0)) { pt1 = t1[pi] break } } } if (pt1) { // Is in circle ? if (this.inCircle(pt1, t0)) { var pt2 // Get opposite point for (pi = 0; pi < t0.length; pi++) { if (!this._ptInTriangle(t0[pi], t1)) { pt2 = t0.splice(pi, 1)[0] break } } // Flip triangles if (this.intersectSegs([pt1, pt2], t0)) { while (triangles.length) { var tmp = triangles.pop() tmp.set('del', true) this.removeFeature(tmp) } this._addTriangle([pt1, pt2, t0[0]]) this._addTriangle([pt1, pt2, t0[1]]) } } } } } } /** Test intersection beetween 2 segs * @param {Array} d1 * @param {Array} d2 * @return {bbolean} */ intersectSegs(d1, d2) { var d1x = d1[1][0] - d1[0][0] var d1y = d1[1][1] - d1[0][1] var d2x = d2[1][0] - d2[0][0] var d2y = d2[1][1] - d2[0][1] var det = d1x * d2y - d1y * d2x if (det != 0) { var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det // Intersection: return [d2[0][0] + k*d2x, d2[0][1] + k*d2y]; return (0 < k && k < 1) } else return false } /** Test pt is a triangle's node * @param {ol.coordinate} pt * @param {Array} triangle * @return {boolean} */ _ptInTriangle(pt, triangle) { for (var i = 0, p; p = triangle[i]; i++) { if (ol.coordinate.equal(pt, p)) return true } return false } /** List points in a triangle (assume points get an id) for debug purposes * @param {Array} pts * @return {String} ids list */ listpt(pts) { var s = '' for (var i = 0, p; p = pts[i]; i++) { var c = this._nodes.getClosestFeatureToCoordinate(p) if (!ol.coordinate.equal(c.getGeometry().getCoordinates(), p)) c = null s += (s ? ', ' : '') + (c ? c.get('id') : '?') } return s } /** Test if coord is within triangle's circumcircle * @param {ol.coordinate} coord * @param {Array} triangle * @return {boolean} */ inCircle(coord, triangle) { var c = this.getCircumCircle(triangle) return ol.coordinate.dist2d(coord, c.center) < c.radius } /** Calculate the circumcircle of a triangle * @param {Array} triangle * @return {*} */ getCircumCircle(triangle) { var x1 = triangle[0][0] var y1 = triangle[0][1] var x2 = triangle[1][0] var y2 = triangle[1][1] var x3 = triangle[2][0] var y3 = triangle[2][1] var m1 = (x1 - x2) / (y2 - y1) var m2 = (x1 - x3) / (y3 - y1) var b1 = ((y1 + y2) / 2) - m1 * (x1 + x2) / 2 var b2 = ((y1 + y3) / 2) - m2 * (x1 + x3) / 2 var cx = (b2 - b1) / (m1 - m2) var cy = m1 * cx + b1 var center = [cx, cy] return { center: center, radius: ol.coordinate.dist2d(center, triangle[0]) } } /** Get triangles at a point */ getTrianglesAt(coord) { var extent = ol.extent.buffer(ol.extent.boundingExtent([coord]), this.get('epsilon')) var result = [] this.forEachFeatureIntersectingExtent(extent, function (f) { result.push(f) }) return result } /** Get nodes at a point */ getNodesAt(coord) { var extent = ol.extent.buffer(ol.extent.boundingExtent([coord]), this.get('epsilon')) return this._nodes.getFeaturesInExtent(extent) } /** Get Voronoi * @param {boolean} border include border, default false * @return { Array< ol.geom.Polygon > } */ calculateVoronoi(border) { var voronoi = [] this.getNodes().forEach(function (f) { var pt = f.getGeometry().getCoordinates() var isborder = false if (border !== true) { for (var i = 0; i < this.hull.length; i++) { if (ol.coordinate.equal(pt, this.hull[i])) { isborder = true break } } } if (!isborder) { var tr = this.getTrianglesAt(pt) var pts = [] tr.forEach(function (triangle) { var c = this.getCircumCircle(triangle.getGeometry().getCoordinates()[0]) pts.push({ pt: c.center, d: Math.atan2(c.center[1] - pt[1], c.center[0] - pt[0]) }) }.bind(this)) pts.sort(function (a, b) { return a.d - b.d }) var poly = [] pts.forEach(function (p) { poly.push(p.pt) }) poly.push(poly[0]) var prop = f.getProperties() prop.geometry = new ol.geom.Polygon([poly]) voronoi.push(new ol.Feature(prop)) } }.bind(this)) return voronoi } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A source that use a set of feature to collect data on it. * If a binSource is provided the bin is recalculated when features change. * @constructor * @extends {ol.source.BinBase} * @param {Object} options ol.source.VectorOptions + grid option * @param {ol.source.Vector} options.source source to collect in the bin * @param {ol.source.Vector} options.binSource a source to use as bin collector, default none * @param {Array} options.features the features, ignored if binSource is provided, default none * @param {number} [options.size] size of the grid in meter, default 200m * @param {function} [options.geometryFunction] Function that takes an ol.Feature as argument and returns an ol.geom.Point as feature's center. * @param {function} [options.flatAttributes] Function takes a bin and the features it contains and aggragate the features in the bin attributes when saving */ ol.source.FeatureBin = class olsourceFeatureBin extends ol.source.BinBase { constructor(options) { options = options || {}; super(options); if (options.binSource) { this._sourceFeature = options.binSource; // When features change recalculate the bin... var timout; this._sourceFeature.on(['addfeature', 'changefeature', 'removefeature'], function () { if (timout) { // Do it only one time clearTimeout(timout); } timout = setTimeout(function () { this.reset(); }.bind(this)); }.bind(this)); } else { this._sourceFeature = new ol.source.Vector({ features: options.features || [] }); } // Handle existing features this.reset(); } /** Set features to use as bin collector * @param {ol.Feature} features */ setFeatures(features) { this._sourceFeature.clear(); this._sourceFeature.addFeatures(features || []); this.reset(); } /** Get the grid geometry at the coord * @param {ol.Coordinate} coord * @returns {ol.geom.Polygon} * @api */ getGridGeomAt(coord, attributes) { var f = this._sourceFeature.getFeaturesAtCoordinate(coord)[0]; if (!f) return null; var a = f.getProperties(); for (var i in a) { if (i !== 'geometry') attributes[i] = a[i]; } return f.getGeometry(); } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). ol.source.GeoImage is a layer source with georeferencement to place it on a map. */ /** @typedef {Object} GeoImageOptions * @property {url} url url of the static image * @property {image} image the static image, if not provided, use url to load an image * @property {ol.Coordinate} imageCenter coordinate of the center of the image * @property {ol.Size|number} imageScale [scalex, scaley] of the image * @property {number} imageRotate angle of the image in radian, default 0 * @property {ol.Extent} imageCrop of the image to be show (in the image) default: [0,0,imageWidth,imageHeight] * @property {Array.} imageMask linestring to mask the image on the map */ /** Layer source with georeferencement to place it on a map * @constructor * @extends {ol.source.ImageCanvas} * @param {GeoImageOptions} options */ ol.source.GeoImage = class olsourceGeoImage extends ol.source.ImageCanvas { constructor(opt_options) { var options = { attributions: opt_options.attributions, logo: opt_options.logo, projection: opt_options.projection } // Draw image on canvas options.canvasFunction = function (extent, resolution, pixelRatio, size) { return this.calculateImage(extent, resolution, pixelRatio, size) } super(options) // options.projection = opt_options.projection; // Load Image this._image = (opt_options.image ? opt_options.image : new Image) this._image.crossOrigin = opt_options.crossOrigin // 'anonymous'; // Show image on load this._image.onload = function () { this.setCrop(this.crop) this.changed() }.bind(this) if (!opt_options.image) this._image.src = opt_options.url // Coordinate of the image center this.center = opt_options.imageCenter // Image scale this.setScale(opt_options.imageScale) // Rotation of the image this.rotate = opt_options.imageRotate ? opt_options.imageRotate : 0 // Crop of the image this.crop = opt_options.imageCrop // Mask of the image this.mask = opt_options.imageMask // Crop this.setCrop(this.crop) // Calculate extent on change this.on('change', function () { this.set('extent', this.calculateExtent()) }.bind(this)) } /** calculate image at extent / resolution * @param {ol/extent/Extent} extent * @param {number} resolution * @param {number} pixelRatio * @param {ol/size/Size} size * @return {HTMLCanvasElement} */ calculateImage(extent, resolution, pixelRatio, size) { if (!this.center) return var canvas = document.createElement('canvas') canvas.width = size[0] canvas.height = size[1] var ctx = canvas.getContext('2d') if (!this._imageSize) return canvas // transform coords to pixel function tr(xy) { return [ (xy[0] - extent[0]) / (extent[2] - extent[0]) * size[0], (xy[1] - extent[3]) / (extent[1] - extent[3]) * size[1] ] } // Clipping mask if (this.mask) { ctx.beginPath() var p = tr(this.mask[0]) ctx.moveTo(p[0], p[1]) for (var i = 1; i < this.mask.length; i++) { p = tr(this.mask[i]) ctx.lineTo(p[0], p[1]) } ctx.clip() } // Draw var pixel = tr(this.center) var dx = (this._image.naturalWidth / 2 - this.crop[0]) * this.scale[0] / resolution * pixelRatio var dy = (this._image.naturalHeight / 2 - this.crop[1]) * this.scale[1] / resolution * pixelRatio var sx = this._imageSize[0] * this.scale[0] / resolution * pixelRatio var sy = this._imageSize[1] * this.scale[1] / resolution * pixelRatio ctx.translate(pixel[0], pixel[1]) if (this.rotate) ctx.rotate(this.rotate) ctx.drawImage(this._image, this.crop[0], this.crop[1], this._imageSize[0], this._imageSize[1], -dx, -dy, sx, sy) return canvas } /** * Get coordinate of the image center. * @return {ol.Coordinate} coordinate of the image center. * @api stable */ getCenter() { return this.center } /** * Set coordinate of the image center. * @param {ol.Coordinate} coordinate of the image center. * @api stable */ setCenter(center) { this.center = center this.changed() } /** * Get image scale. * @return {ol.size} image scale (along x and y axis). * @api stable */ getScale() { return this.scale } /** * Set image scale. * @param {ol.size|Number} image scale (along x and y axis or both). * @api stable */ setScale(scale) { switch (typeof (scale)) { case 'number': scale = [scale, scale] break case 'object': if (scale.length != 2) return break default: return } this.scale = scale this.changed() } /** * Get image rotation. * @return {Number} rotation in radian. * @api stable */ getRotation() { return this.rotate } /** * Set image rotation. * @param {Number} rotation in radian. * @api stable */ setRotation(angle) { this.rotate = angle this.changed() } /** * Get the image. * @api stable */ getGeoImage() { return this._image } /** * Get image crop extent. * @return {ol.extent} image crop extent. * @api stable */ getCrop() { return this.crop } /** * Set image mask. * @param {ol.geom.LineString} coords of the mask * @api stable */ setMask(mask) { this.mask = mask this.changed() } /** * Get image mask. * @return {ol.geom.LineString} coords of the mask * @api stable */ getMask() { return this.mask } /** * Set image crop extent. * @param {ol.extent|Number} image crop extent or a number to crop from original size. * @api stable */ setCrop(crop) { // Image not loaded => get it latter if (!this._image.naturalWidth) { this.crop = crop return } if (crop) { switch (typeof (crop)) { case 'number': crop = [crop, crop, this._image.naturalWidth - crop, this._image.naturalHeight - crop] break case 'object': if (crop.length != 4) return break default: return } crop = ol.extent.boundingExtent([[crop[0], crop[1]], [crop[2], crop[3]]]) this.crop = [Math.max(0, crop[0]), Math.max(0, crop[1]), Math.min(this._image.naturalWidth, crop[2]), Math.min(this._image.naturalHeight, crop[3])] } else this.crop = [0, 0, this._image.naturalWidth, this._image.naturalHeight] if (this.crop[2] <= this.crop[0]) this.crop[2] = this.crop[0] + 1 if (this.crop[3] <= this.crop[1]) this.crop[3] = this.crop[1] + 1 this._imageSize = [this.crop[2] - this.crop[0], this.crop[3] - this.crop[1]] this.changed() } /** Get the extent of the source. * @param {module:ol/extent~Extent} extent If provided, no new extent will be created. Instead, that extent's coordinates will be overwritten. * @return {ol.extent} */ getExtent(opt_extent) { var ext = this.get('extent') if (!ext) ext = this.calculateExtent() if (opt_extent) { for (var i = 0; i < opt_extent.length; i++) { opt_extent[i] = ext[i] } } return ext } /** Calculate the extent of the source image. * @param {boolean} usemask return the mask extent, default return the image extent * @return {ol.extent} */ calculateExtent(usemask) { var polygon if (usemask !== false && this.getMask()) { polygon = new ol.geom.Polygon([this.getMask()]) } else { var center = this.getCenter() var scale = this.getScale() var width = this.getGeoImage().width * scale[0] var height = this.getGeoImage().height * scale[1] var extent = ol.extent.boundingExtent([ [center[0] - width / 2, center[1] - height / 2], [center[0] + width / 2, center[1] + height / 2] ]) polygon = ol.geom.Polygon.fromExtent(extent) polygon.rotate(-this.getRotation(), center) } var ext = polygon.getExtent() return ext } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** ol.source.GeoRSS is a source that load Wikimedia Commons content in a vector layer. * @constructor * @extends {ol.source.Vector} * @param {*} options source options * @param {string} options.url GeoRSS feed url */ ol.source.GeoRSS = class olsourceGeoRSS extends ol.source.Vector { constructor(options) { options = options || {}; options.loader = function(extent, resolution, projection) { return this._loaderFn(extent, resolution, projection); } super(options); } /** Loader function used to load features. * @private */ _loaderFn(extent, resolution, projection) { // Ajax request to get source ol.ext.Ajax.get({ url: this.getUrl(), dataType: 'XML', error: function () { console.log('oops'); }, success: function (xml) { var features = (new ol.format.GeoRSS()).readFeatures(xml, { featureProjection: projection }); this.addFeatures(features); }.bind(this) }); } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** IGN's Geoportail WMTS source * @constructor * @extends {ol.source.WMTS} * @param {olx.source.Geoportail=} options WMTS options * @param {string=} options.layer Geoportail layer name * @param {number} options.minZoom * @param {number} options.maxZoom * @param {string} options.server * @param {string} options.gppKey api key, default 'choisirgeoportail' * @param {string} options.authentication basic authentication associated with the gppKey as btoa("login:pwd") * @param {string} options.format image format, default 'image/jpeg' * @param {string} options.style layer style, default 'normal' * @param {string} options.crossOrigin default 'anonymous' * @param {string} options.wrapX default true */ ol.source.Geoportail = class olsourceGeoportail extends ol.source.WMTS { constructor(layer, options) { options = options || {} if (layer.layer) { options = layer layer = options.layer } var matrixIds = new Array() var resolutions = new Array() //[156543.03392804103,78271.5169640205,39135.75848201024,19567.879241005125,9783.939620502562,4891.969810251281,2445.9849051256406,1222.9924525628203,611.4962262814101,305.74811314070485,152.87405657035254,76.43702828517625,38.218514142588134,19.109257071294063,9.554628535647034,4.777314267823517,2.3886571339117584,1.1943285669558792,0.5971642834779396,0.29858214173896974,0.14929107086948493,0.07464553543474241]; var size = ol.extent.getWidth(ol.proj.get('EPSG:3857').getExtent()) / 256 for (var z = 0; z <= (options.maxZoom ? options.maxZoom : 20); z++) { matrixIds[z] = z resolutions[z] = size / Math.pow(2, z) } var tg = new ol.tilegrid.WMTS({ origin: [-20037508, 20037508], resolutions: resolutions, matrixIds: matrixIds }) tg.minZoom = (options.minZoom ? options.minZoom : 0) var attr = [ ol.source.Geoportail.defaultAttribution ] if (options.attributions) attr = options.attributions var server = options.server || 'https://wxs.ign.fr/geoportail/wmts' var gppKey = options.gppKey || options.key || 'choisirgeoportail' var wmts_options = { url: ol.source.Geoportail.getServiceURL(server, gppKey), layer: layer, matrixSet: 'PM', format: options.format ? options.format : 'image/jpeg', projection: 'EPSG:3857', tileGrid: tg, style: options.style ? options.style : 'normal', attributions: attr, crossOrigin: (typeof options.crossOrigin == 'undefined') ? 'anonymous' : options.crossOrigin, wrapX: !(options.wrapX === false) } super(wmts_options) this._server = server this._gppKey = gppKey // Load url using basic authentification if (options.authentication) { this.setTileLoadFunction(ol.source.Geoportail.tileLoadFunctionWithAuthentication(options.authentication, this.getFormat())) } } /** Get a tile load function to load tiles with basic authentication * @param {string} authentication as btoa("login:pwd") * @param {string} format mime type * @return {function} tile load function to load tiles with basic authentication */ static tileLoadFunctionWithAuthentication(authentication, format) { if (!authentication) return undefined return function (tile, src) { var xhr = new XMLHttpRequest() xhr.open("GET", src) xhr.setRequestHeader("Authorization", "Basic " + authentication) xhr.responseType = "arraybuffer" xhr.onload = function () { var arrayBufferView = new Uint8Array(this.response) var blob = new Blob([arrayBufferView], { type: format }) var urlCreator = window.URL || window.webkitURL var imageUrl = urlCreator.createObjectURL(blob) tile.getImage().src = imageUrl } xhr.onerror = function () { tile.getImage().src = "" } xhr.send() } } /** Get service URL according to server url or standard url */ serviceURL() { return ol.source.Geoportail.getServiceURL(this._server, this._gppKey) } /** * Return the associated API key of the Map. * @function * @return the API key. * @api stable */ getGPPKey() { return this._gppKey } /** * Set the associated API key to the Map. * @param {String} key the API key. * @param {String} authentication as btoa("login:pwd") * @api stable */ setGPPKey(key, authentication) { this._gppKey = key var serviceURL = this.serviceURL() this.setTileUrlFunction(function () { var url = ol.source.Geoportail.prototype.getTileUrlFunction().apply(this, arguments) if (url) { var args = url.split("?") return serviceURL + "?" + args[1] } else return url }) // Load url using basic authentification if (authentication) { this.setTileLoadFunction(ol.source.Geoportail.tileLoadFunctionWithAuthentication(authentication, this.getFormat())) } } /** Return the GetFeatureInfo URL for the passed coordinate, resolution, and * projection. Return `undefined` if the GetFeatureInfo URL cannot be * constructed. * @param {ol.Coordinate} coord * @param {Number} resolution * @param {ol.proj.Projection} projection default the source projection * @param {Object} options * @param {string} options.INFO_FORMAT response format text/plain, text/html, application/json, default text/plain * @return {String|undefined} GetFeatureInfo URL. */ getFeatureInfoUrl(coord, resolution, projection, options) { options = options || {} if (!projection) projection = this.getProjection() var tileCoord = this.tileGrid.getTileCoordForCoordAndResolution(coord, resolution) var ratio = 1 var url = this.getTileUrlFunction()(tileCoord, ratio, projection) if (!url) return url var tileResolution = this.tileGrid.getResolution(tileCoord[0]) var tileExtent = this.tileGrid.getTileCoordExtent(tileCoord) var i = Math.floor((coord[0] - tileExtent[0]) / (tileResolution / ratio)) var j = Math.floor((tileExtent[3] - coord[1]) / (tileResolution / ratio)) return url.replace(/Request=GetTile/i, 'Request=getFeatureInfo') + '&INFOFORMAT=' + (options.INFO_FORMAT || 'text/plain') + '&I=' + i + '&J=' + j } /** Get feature info * @param {ol.Coordinate} coord * @param {Number} resolution * @param {ol.proj.Projection} projection default the source projection * @param {Object} options * @param {string} options.INFO_FORMAT response format text/plain, text/html, application/json, default text/plain * @param {function} options.callback a function that take the response as parameter * @param {function} options.error function called when an error occurred */ getFeatureInfo(coord, resolution, options) { var url = this.getFeatureInfoUrl(coord, resolution, null, options) ol.ext.Ajax.get({ url: url, dataType: options.format || 'text/plain', options: { encode: false }, success: function (resp) { if (options.callback) options.callback(resp) }, error: options.error || function () { } }) } } /** Standard IGN-GEOPORTAIL attribution */ ol.source.Geoportail.defaultAttribution = 'Géoportail © IGN-France'; /** Get service URL according to server url or standard url */ ol.source.Geoportail.getServiceURL = function(server, gppKey) { if (server) { return server.replace(/^(https?:\/\/[^/]*)(.*)$/, "$1/" + gppKey + "$2") } else { return (window.geoportailConfig ? window.geoportailConfig.url : "https://wxs.ign.fr/") + gppKey + "/geoportail/wmts" } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A source for grid binning * @constructor * @extends {ol.source.Vector} * @param {Object} options ol.source.VectorOptions + grid option * @param {ol.source.Vector} options.source Source * @param {number} [options.size] size of the grid in meter, default 200m * @param {function} [options.geometryFunction] Function that takes an ol.Feature as argument and returns an ol.geom.Point as feature's center. * @param {function} [options.flatAttributes] Function takes a bin and the features it contains and aggragate the features in the bin attributes when saving */ ol.source.GridBin = class olsourceGridBin extends ol.source.BinBase { constructor(options) { options = options || {}; super(options); this.set('gridProjection', options.gridProjection || 'EPSG:4326'); this.setSize('size', options.size || 1); this.reset(); } /** Set grid projection * @param {ol.ProjectionLike} proj */ setGridProjection(proj) { this.set('gridProjection', proj); this.reset(); } /** Set grid size * @param {number} size */ setSize(size) { this.set('size', size); this.reset(); } /** Get the grid geometry at the coord * @param {ol.Coordinate} coord * @returns {ol.geom.Polygon} * @api */ getGridGeomAt(coord) { coord = ol.proj.transform(coord, this.getProjection() || 'EPSG:3857', this.get('gridProjection')); var size = this.get('size'); var x = size * Math.floor(coord[0] / size); var y = size * Math.floor(coord[1] / size); var geom = new ol.geom.Polygon([[[x, y], [x + size, y], [x + size, y + size], [x, y + size], [x, y]]]); return geom.transform(this.get('gridProjection'), this.getProjection() || 'EPSG:3857'); } } /* Copyright (c) 2017-2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A source for hexagonal binning * @constructor * @extends {ol.source.Vector} * @param {Object} options ol.source.VectorOptions + ol.HexGridOptions * @param {ol.source.Vector} options.source Source * @param {number} [options.size] size of the hexagon in map units, default 80000 * @param {ol.coordinate} [options.origin] origin of the grid, default [0,0] * @param {HexagonLayout} [options.layout] grid layout, default pointy * @param {function} [options.geometryFunction] Function that takes an ol.Feature as argument and returns an ol.geom.Point as feature's center. * @param {function} [options.flatAttributes] Function takes a bin and the features it contains and aggragate the features in the bin attributes when saving */ ol.source.HexBin = class olsourceHexBin extends ol.source.BinBase { constructor(options) { options = options || {}; super(options); /** The HexGrid * @type {ol.HexGrid} */ this._hexgrid = new ol.HexGrid(options); // Handle existing features this.reset(); } /** Get the hexagon geometry at the coord * @param {ol.Coordinate} coord * @returns {ol.geom.Polygon} * @api */ getGridGeomAt(coord) { var h = this._hexgrid.coord2hex(coord); return new ol.geom.Polygon([this._hexgrid.getHexagon(h)]); } /** Set the inner HexGrid size. * @param {number} newSize * @param {boolean} noreset If true, reset will not be called (It need to be called through) */ setSize(newSize, noreset) { this._hexgrid.setSize(newSize); if (!noreset) { this.reset(); } } /** Get the inner HexGrid size. * @return {number} */ getSize() { return this._hexgrid.getSize(); } /** Set the inner HexGrid layout. * @param {HexagonLayout} newLayout * @param {boolean} noreset If true, reset will not be called (It need to be called through) */ setLayout(newLayout, noreset) { this._hexgrid.setLayout(newLayout); if (!noreset) { this.reset(); } } /** Get the inner HexGrid layout. * @return {HexagonLayout} */ getLayout() { return this._hexgrid.getLayout(); } /** Set the inner HexGrid origin. * @param {ol.Coordinate} newLayout * @param {boolean} noreset If true, reset will not be called (It need to be called through) */ setOrigin(newLayout, noreset) { this._hexgrid.setOrigin(newLayout); if (!noreset) { this.reset(); } } /** Get the inner HexGrid origin. * @return {ol.Coordinate} */ getOrigin() { return this._hexgrid.getOrigin(); } /** * Get hexagons without circular dependencies (vs. getFeatures) * @return {Array} */ getHexFeatures() { return super.getGridFeatures(); } } /* Copyright (c) 2021 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) */ /** Inverse distance weighting interpolated source - Shepard's method * @see https://en.wikipedia.org/wiki/Inverse_distance_weighting * @constructor * @extends {ol.source.ImageCanvas} * @fire drawstart * @fire drawend * @param {*} [options] * @param {ol.source.vector} options.source a source to interpolate * @param {function} [options.getColor] a function that takes a value and returns a color (as an Array [r,g,b,a]) * @param {boolean} [options.useWorker=false] use worker to calculate the distance map (may cause flickering on small data sets). Source will fire drawstart, drawend while calculating * @param {Object} [options.lib] Functions that will be made available to operations run in a worker * @param {number} [options.scale=4] scale factor, use large factor to enhance performances (but minor accuracy) * @param {string|function} options.weight The feature attribute to use for the weight or a function that returns a weight from a feature. Weight values should range from 0 to 100. Default use the weight attribute of the feature. */ ol.source.IDW = class olsourceIDW extends ol.source.ImageCanvas { constructor(options) { options = options || {}; // Draw image on canvas options.canvasFunction = function (extent, resolution, pixelRatio, size) { return this.calculateImage(extent, resolution, pixelRatio, size); }; super(options); this._source = options.source; this._canvas = document.createElement('CANVAS'); this._source.on(['addfeature', 'removefeature', 'clear', 'removefeature'], function () { this.changed(); }.bind(this)); if (typeof(options.getColor) === 'function') this.getColor = options.getColor if (options.useWorker) { var lib = { hue2rgb: this.hue2rgb, getColor: this.getColor } for (var f in options.useWorker) { lib[f] = options.useWorker[f]; } this.worker = new ol.ext.Worker(this.computeImage, { onMessage: this.onImageData.bind(this), lib: lib }); } this._position = { extent: [], resolution: 0 }; this.set('scale', options.scale || 4); this._weight = typeof (options.weight) === 'function' ? options.weight : function (f) { return f.get(options.weight || 'weight'); }; } /** Get the source */ getSource() { return this._source; } /** Apply the value to the map RGB. Overwrite this function to set your own colors. * @param {number} v value * @param {Uint8ClampedArray} data RGBA array * @param {number} i index in the RGBA array * @api * / setData(v, data, i) { // Get color var color = this.getColor(v); // Convert to RGB data[i] = color[0]; data[i + 1] = color[1]; data[i + 2] = color[2]; data[i + 3] = color[3]; } /** Get image value at coord (RGBA) * @param {l.coordinate} coord * @return {Uint8ClampedArray} */ getValue(coord) { if (!this._canvas) return null; var pt = this.transform(coord); var v = this._canvas.getContext('2d').getImageData(Math.round(pt[0]), Math.round(pt[1]), 1, 1).data; return (v); } /** Convert hue to rgb factor * @param {number} h * @return {number} * @private */ hue2rgb(h) { h = (h + 6) % 6; if (h < 1) return Math.round(h * 255); if (h < 3) return 255; if (h < 4) return Math.round((4 - h) * 255); return 0; } /** Get color for a value. Return an array of RGBA values. * @param {number} v value * @returns {Array} * @api */ getColor(v) { // Get hue var h = 4 - (0.04 * v); // Convert to RGB return [ this.hue2rgb(h + 2), this.hue2rgb(h), this.hue2rgb(h - 2), 255 ]; } /** Compute image data * @param {Object} e */ computeImage(e) { var pts = e.data.pts; var width = e.data.width; var height = e.data.height; var imageData = new Uint8ClampedArray(width * height * 4); // Compute image var x, y; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { var t = 0, b = 0; for (var i = 0; i < pts.length; ++i) { var dx = x - pts[i][0]; var dy = y - pts[i][1]; var d = dx * dx + dy * dy; // Inverse distance weighting - Shepard's method if (d === 0) { b = 1; t = pts[i][2]; break; } var inv = 1 / (d * d); t += inv * pts[i][2]; b += inv; } // Set color var color = this.getColor(t / b); // Convert to RGB var pos = (y * width + x) * 4; imageData[pos] = color[0]; imageData[pos + 1] = color[1]; imageData[pos + 2] = color[2]; imageData[pos + 3] = color[3]; } } return { type: 'image', data: imageData, width: width, height: height }; } /** Calculate IDW at extent / resolution * @param {ol/extent/Extent} extent * @param {number} resolution * @param {number} pixelRatio * @param {ol/size/Size} size * @return {HTMLCanvasElement} * @private */ calculateImage(extent, resolution, pixelRatio, size) { if (!this._source) return this._canvas; if (this._updated) { this._updated = false; return this._canvas; } // Calculation canvas at small resolution var width = Math.round(size[0] / (this.get('scale') * pixelRatio)); var height = Math.round(size[1] / (this.get('scale') * pixelRatio)); // Transform coords to pixel / value var pts = []; var dw = width / (extent[2] - extent[0]); var dh = height / (extent[1] - extent[3]); var tr = this.transform = function (xy, v) { return [ (xy[0] - extent[0]) * dw, (xy[1] - extent[3]) * dh, v ]; }; // Get features / weight this._source.getFeatures().forEach(function (f) { pts.push(tr(f.getGeometry().getFirstCoordinate(), this._weight(f))); }.bind(this)); if (this.worker) { // kill old worker and star new one this.worker.postMessage({ pts: pts, width: width, height: height }, true); this.dispatchEvent({ type: 'drawstart' }); // Move the canvas position meanwhile if (this._canvas.width !== Math.round(size[0]) || this._canvas.height !== Math.round(size[1]) || this._position.resolution !== resolution || this._position.extent[0] !== extent[0] || this._position.extent[1] !== extent[1]) { this._canvas.width = Math.round(size[0]); this._canvas.height = Math.round(size[1]); } this._position.extent = extent; this._position.resolution = resolution; } else { this._canvas.width = Math.round(size[0]); this._canvas.height = Math.round(size[1]); var imageData = this.computeImage({ data: { pts: pts, width: width, height: height } }); this.onImageData(imageData); } return this._canvas; } /** Display data when ready * @private */ onImageData(imageData) { // Calculation canvas at small resolution var canvas = this._internal = document.createElement('CANVAS'); canvas.width = imageData.width; canvas.height = imageData.height; var ctx = canvas.getContext('2d'); ctx.putImageData(new ImageData(imageData.data, imageData.width, imageData.height), 0, 0); // Draw full resolution canvas this._canvas.getContext('2d').drawImage(canvas, 0, 0, this._canvas.width, this._canvas.height); // Force redraw if (this.worker) { this.dispatchEvent({ type: 'drawend' }); this._updated = true; this.changed(); } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A source for INSEE grid * @constructor * @extends {ol.source.Vector} * @param {Object} options ol.source.VectorOptions + grid option * @param {ol.source.Vector} options.source Source * @param {number} [options.size] size of the grid in meter, default 200m * @param {function} [options.geometryFunction] Function that takes an ol.Feature as argument and returns an ol.geom.Point as feature's center. * @param {function} [options.flatAttributes] Function takes a bin and the features it contains and aggragate the features in the bin attributes when saving */ ol.source.InseeBin = class olsourceInseeBin extends ol.source.BinBase { constructor(options) { options = options || {}; super(options); this._grid = new ol.InseeGrid({ size: options.size }); this.reset(); } /** Set grid size * @param {number} size */ setSize(size) { if (this.getSize() !== size) { this._grid.set('size', size); this.reset(); } } /** Get grid size * @return {number} size */ getSize() { return this._grid.get('size'); } /** Get the grid geometry at the coord * @param {ol.Coordinate} coord * @returns {ol.geom.Polygon} * @api */ getGridGeomAt(coord) { return this._grid.getGridAtCoordinate(coord, this.getProjection()); } /** Get grid extent * @param {ol.ProjectionLike} proj * @return {ol.Extent} */ getGridExtent(proj) { return this._grid.getExtent(proj); } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). @classdesc ol.source.Mapillary is a source that load Mapillary's geotagged photos in a vector layer. Inherits from: */ /** Mapillary test (not tested) * @constructor ol.source.Mapillary * @extends {ol.source.Vector} * @param {olx.source.Mapillary=} options */ ol.source.Mapillary = class olsourceMapillary extends ol.source.Vector { constructor(opt_options) { var options = opt_options || {}; options.loader = function(extent, resolution, projection) { return this._loaderFn(extent, resolution, projection); } /** Default attribution */ if (!options.attributions) options.attributions = ["© Mapillary"]; // Bbox strategy : reload at each move if (!options.strategy) options.strategy = ol.loadingstrategy.bbox; // Init parent super(options); /** Max resolution to load features */ this._maxResolution = options.maxResolution || 100; /** Query limit */ this._limit = options.limit || 100; // Client ID // this.set("clientId", options.clientId); } /** Decode wiki attributes and choose to add feature to the layer * @param {feature} the feature * @param {attributes} wiki attributes * @return {boolean} true: add the feature to the layer * @API stable */ readFeature( /*feature, attributes*/) { return true; } /** Loader function used to load features. * @private */ _loaderFn(extent, resolution, projection) { if (resolution > this._maxResolution) return; var bbox = ol.proj.transformExtent(extent, projection, "EPSG:4326"); // Commons API: for more info @see https://www.mapillary.com/developer var date = Date.now() - 6 * 30 * 24 * 60 * 60 * 1000; var url = "https://a.mapillary.com/v2/search/im?client_id=" + this.get('clientId') + "&max_lat=" + bbox[3] + "&max_lon=" + bbox[2] + "&min_lat=" + bbox[1] + "&min_lon=" + bbox[0] + "&limit=" + (this._limit - 1) + "&start_time=" + date; // Ajax request to get the tile ol.ext.Ajax.get( { url: url, dataType: 'jsonp', success: function (data) { console.log(data); /* var features = []; var att, pt, feature, lastfeature = null; if (data.query && data.query.pages) return; for ( var i in data.query.pages) { att = data.query.pages[i]; if (att.coordinates && att.coordinates.length ) { pt = [att.coordinates[0].lon, att.coordinates[0].lat]; } else { var meta = att.imageinfo[0].metadata; if (!meta) { //console.log(att); continue; } pt = []; for (var k=0; k} sources Input sources or layers. For vector data, use an VectorImage layer. * @param {number} radius default 4 * @param {number} intensity default 25 */ ol.source.OilPainting = class olsourceOilPainting extends ol.source.Raster { constructor(options) { options.operation = ol.source.OilPainting._operation options.operationType = 'image'; super(options); this.set('radius', options.radius || 4); this.set('intensity', options.intensity || 25); this.on('beforeoperations', function (event) { var w = Math.round((event.extent[2] - event.extent[0]) / event.resolution); var h = Math.round((event.extent[3] - event.extent[1]) / event.resolution); event.data.image = new ImageData(w, h); event.data.radius = Number(this.get('radius')) || 1; event.data.intensity = Number(this.get('intensity')); }.bind(this)); } /** Set value and force change */ set(key, val) { if (val) { switch (key) { case 'intensity': case 'radius': { val = Number(val); if (val < 1) val = 1; this.changed(); break; } } } return super.set(key, val); } } /** * @private */ ol.source.OilPainting._operation = function(pixels, data) { var width = pixels[0].width, height = pixels[0].height, imgData = pixels[0], pixData = imgData.data, pixelIntensityCount = []; var destImageData = data.image, destPixData = destImageData.data, intensityLUT = [], rgbLUT = []; for (var y = 0; y < height; y++) { intensityLUT[y] = []; rgbLUT[y] = []; for (var x = 0; x < width; x++) { var idx = (y * width + x) * 4, r = pixData[idx], g = pixData[idx + 1], b = pixData[idx + 2], avg = (r + g + b) / 3; intensityLUT[y][x] = Math.round((avg * data.intensity) / 255); rgbLUT[y][x] = { r: r, g: g, b: b }; } } var radius = data.radius; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { pixelIntensityCount = []; // Find intensities of nearest pixels within radius. for (var yy = -radius; yy <= radius; yy++) { for (var xx = -radius; xx <= radius; xx++) { if (y + yy > 0 && y + yy < height && x + xx > 0 && x + xx < width) { var intensityVal = intensityLUT[y + yy][x + xx]; if (!pixelIntensityCount[intensityVal]) { pixelIntensityCount[intensityVal] = { val: 1, r: rgbLUT[y + yy][x + xx].r, g: rgbLUT[y + yy][x + xx].g, b: rgbLUT[y + yy][x + xx].b }; } else { pixelIntensityCount[intensityVal].val++; pixelIntensityCount[intensityVal].r += rgbLUT[y + yy][x + xx].r; pixelIntensityCount[intensityVal].g += rgbLUT[y + yy][x + xx].g; pixelIntensityCount[intensityVal].b += rgbLUT[y + yy][x + xx].b; } } } } pixelIntensityCount.sort(function (a, b) { return b.val - a.val; }); var curMax = pixelIntensityCount[0].val, dIdx = (y * width + x) * 4; destPixData[dIdx] = ~~(pixelIntensityCount[0].r / curMax); destPixData[dIdx + 1] = ~~(pixelIntensityCount[0].g / curMax); destPixData[dIdx + 2] = ~~(pixelIntensityCount[0].b / curMax); destPixData[dIdx + 3] = 255; } } return destImageData; } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * OSM layer using the Ovepass API * @constructor ol.source.Overpass * @extends {ol.source.Vector} * @param {any} options * @param {string} options.url service url, default: https://overpass-api.de/api/interpreter * @param {Array} options.filter an array of tag filters, ie. ["key", "key=value", "key~value", ...] * @param {boolean} options.node get nodes, default: true * @param {boolean} options.way get ways, default: true * @param {boolean} options.rel get relations, default: false * @param {number} options.maxResolution maximum resolution to load features * @param {string|ol.Attribution|Array} options.attributions source attribution, default OSM attribution * @param {ol.loadingstrategy} options.strategy loading strategy, default ol.loadingstrategy.bbox */ ol.source.Overpass = class olsourceOverpass extends ol.source.Vector { constructor(options) { options = options || {} options.loader = function(extent, resolution, projection) { return this._loaderFn(extent, resolution, projection) } /** Default attribution */ if (!options.attributions) { options.attributions = ol.source.OSM.ATTRIBUTION } // Bbox strategy : reload at each move if (!options.strategy) options.strategy = ol.loadingstrategy.bbox super(options) /** Ovepass API Url */ this._url = options.url || 'https://overpass-api.de/api/interpreter' /** Max resolution to load features */ this._maxResolution = options.maxResolution || 100 this._types = { node: options.node !== false, way: options.way !== false, rel: options.rel === true } this._filter = options.filter } /** Loader function used to load features. * @private */ _loaderFn(extent, resolution, projection) { if (resolution > this._maxResolution) return var self = this var bbox = ol.proj.transformExtent(extent, projection, "EPSG:4326") bbox = bbox[1] + ',' + bbox[0] + ',' + bbox[3] + ',' + bbox[2] // Overpass QL var query = '[bbox:' + bbox + '][out:xml][timeout:25];' query += '(' // Search attributes for (var t in this._types) { if (this._types[t]) { query += t for (var n = 0, filter; filter = this._filter[n]; n++) { query += '[' + filter + ']' } query += ';' } } query += ');out;>;out skel qt;' var ajax = new XMLHttpRequest() ajax.open('POST', this._url, true) ajax.onload = function () { var features = new ol.format.OSMXML().readFeatures(this.responseText, { featureProjection: projection }) var result = [] // Remove duplicated features for (var i = 0, f; f = features[i]; i++) { if (!self.hasFeature(f)) result.push(f) } self.addFeatures(result) } ajax.onerror = function () { console.log(arguments) } ajax.send('data=' + query) } /** * Search if feature is allready loaded * @param {ol.Feature} feature * @return {boolean} * @private */ hasFeature(feature) { var p = feature.getGeometry().getFirstCoordinate() var id = feature.getId() var existing = this.getFeaturesInExtent([p[0] - 0.1, p[1] - 0.1, p[0] + 0.1, p[1] + 0.1]) for (var i = 0, f; f = existing[i]; i++) { if (id === f.getId()) { return true } } return false } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A vector source to load WFS at a tile zoom level * @constructor * @fires tileloadstart * @fires tileloadend * @fires tileloaderror * @fires overload * @extends {ol.source.Vector} * @param {Object} options * @param {string} [options.version=1.1.0] WFS version to use. Can be either 1.0.0, 1.1.0 or 2.0.0. * @param {string} options.typeName WFS type name parameter * @param {number} options.tileZoom zoom to load the tiles * @param {number} options.maxFeatures maximum features returned in the WFS * @param {number} options.featureLimit maximum features in the source before refresh, default Infinity * @param {boolean} [options.pagination] experimental enable pagination, default no pagination */ ol.source.TileWFS = class olsourceTileWFS extends ol.source.Vector { constructor(options) { options = options || {} if (!options.featureLimit) options.featureLimit = Infinity // Tile loading strategy var zoom = options.tileZoom || 14 var sourceOpt = { strategy: ol.loadingstrategy.tile(ol.tilegrid.createXYZ({ minZoom: zoom, maxZoom: zoom, tileSize: 512 })) } // Loading params var format = new ol.format.GeoJSON() var url = options.url + '?service=WFS' + '&request=GetFeature' + '&version=' + (options.version || '1.1.0') + '&typename=' + (options.typeName || '') + '&outputFormat=application/json' if (options.maxFeatures) { url += '&maxFeatures=' + options.maxFeatures + '&count=' + options.maxFeatures } var loader = { loading: 0, loaded: 0 } // Loading fn sourceOpt.loader = function (extent, resolution, projection) { if (loader.loading === loader.loaded) { loader.loading = loader.loaded = 0 if (this.getFeatures().length > options.maxFeatures) { this.clear() this.refresh() } } loader.loading++ this.dispatchEvent({ type: 'tileloadstart', loading: loader.loading, loaded: loader.loaded }) this._loadTile(url, extent, projection, format, loader) } super(sourceOpt) this.set('pagination', options.pagination) } /** * */ _loadTile(url, extent, projection, format, loader) { var req = url + '&srsname=' + projection.getCode() + '&bbox=' + extent.join(',') + ',' + projection.getCode() if (this.get('pagination') && !/&startIndex/.test(url)) { req += '&startIndex=0' } ol.ext.Ajax.get({ url: req, success: function (response) { loader.loaded++ if (response.error) { this.dispatchEvent({ type: 'tileloaderror', error: response, loading: loader.loading, loaded: loader.loaded }) } else { // Load features var features = format.readFeatures(response, { featureProjection: projection }) if (features.length > 0) { this.addFeatures(features) } // Next page? var pos = response.numberReturned || 0 if (/&startIndex/.test(url)) { pos += parseInt(url.replace(/.*&startIndex=(\d*).*/, '$1')) url = url.replace(/&startIndex=(\d*)/, '') } // Still something to load ? if (pos < response.totalFeatures) { if (!this.get('pagination')) { this.dispatchEvent({ type: 'overload', total: response.totalFeatures, returned: response.numberReturned }) this.dispatchEvent({ type: 'tileloadend', loading: loader.loading, loaded: loader.loaded }) } else { url += '&startIndex=' + pos loader.loaded-- this._loadTile(url, extent, projection, format, loader) } } else { this.dispatchEvent({ type: 'tileloadend', loading: loader.loading, loaded: loader.loaded }) } } }.bind(this), error: function (e) { loader.loaded++ this.dispatchEvent({ type: 'tileloaderror', error: e, loading: loader.loading, loaded: loader.loaded }) }.bind(this) }) } } ;(function () { var clear = ol.source.Vector.prototype.clear; /** Overwrite ol/source/Vector clear to fire clearstart / clearend event */ ol.source.Vector.prototype.clear = function(opt_fast) { this.dispatchEvent({ type: 'clearstart' }); clear.call(this, opt_fast) this.dispatchEvent({ type: 'clearend' }); }; })(); /** * @classdesc 3D vector layer rendering * @constructor * @extends {pl.layer.Image} * @param {Object} options * @param {ol.layer.Vector} options.source the source to display in 3D * @param {ol.style.Style} options.styler drawing style * @param {number} options.maxResolution max resolution to render 3D * @param {number} options.defaultHeight default height if none is return by a propertie * @param {function|string|Number} options.height a height function (returns height giving a feature) or a popertie name for the height or a fixed value * @param {Array} options.center center of the view, default [.5,1] */ ol.layer.Vector3D = class ollayerVector3D extends ol.layer.Image { constructor(options) { options = options || {} var canvas = document.createElement('canvas') super({ source: new ol.source.ImageCanvas({ canvasFunction: function (extent, resolution, pixelRatio, size /*, projection*/) { canvas.width = size[0] canvas.height = size[1] return canvas } }), center: options.center || [.5, 1], defaultHeight: options.defaultHeight || 0, maxResolution: options.maxResolution || Infinity }) this._source = options.source this.height_ = this.getHfn(options.height) this.setStyle(options.style) this.on(['postcompose', 'postrender'], this.onPostcompose_.bind(this)) } /** * Set the height function for the layer * @param {function|string|Number} height a height function (returns height giving a feature) or a popertie name or a fixed value */ setHeight(height) { this.height_ = this.getHfn(height) this.changed() } /** * Set style associated with the layer * @param {ol.style.Style} s */ setStyle(s) { if (s instanceof ol.style.Style) this._style = s else this._style = new ol.style.Style() if (!this._style.getStroke()) { this._style.setStroke(new ol.style.Stroke({ width: 1, color: 'red' })) } if (!this._style.getFill()) { this._style.setFill(new ol.style.Fill({ color: 'rgba(0,0,255,0.5)' })) } if (!this._style.getText()) { this._style.setText(new ol.style.Fill({ color: 'red' }) ) } // Get the geometry if (s && s.getGeometry()) { var geom = s.getGeometry() if (typeof (geom) === 'function') { this.set('geometry', geom) } else { this.set('geometry', function () { return geom }) } } else { this.set('geometry', function (f) { return f.getGeometry() }) } } /** * Get style associated with the layer * @return {ol.style.Style} */ getStyle() { return this._style } /** Calculate 3D at potcompose * @private */ onPostcompose_(e) { var res = e.frameState.viewState.resolution if (res > this.get('maxResolution')) return this.res_ = res * 400 if (this.animate_) { var elapsed = e.frameState.time - this.animate_ if (elapsed < this.animateDuration_) { this.elapsedRatio_ = this.easing_(elapsed / this.animateDuration_) // tell OL3 to continue postcompose animation e.frameState.animate = true } else { this.animate_ = false this.height_ = this.toHeight_ } } var ratio = e.frameState.pixelRatio var ctx = e.context var m = this.matrix_ = e.frameState.coordinateToPixelTransform // Old version (matrix) if (!m) { m = e.frameState.coordinateToPixelMatrix, m[2] = m[4] m[3] = m[5] m[4] = m[12] m[5] = m[13] } this.center_ = [ ctx.canvas.width * this.get('center')[0] / ratio, ctx.canvas.height * this.get('center')[1] / ratio ] var f = this._source.getFeaturesInExtent(e.frameState.extent) ctx.save() ctx.scale(ratio, ratio) var s = this.getStyle() ctx.lineWidth = s.getStroke().getWidth() ctx.lineCap = s.getStroke().getLineCap() ctx.strokeStyle = ol.color.asString(s.getStroke().getColor()) ctx.fillStyle = ol.color.asString(s.getFill().getColor()) var builds = [] for (var i = 0; i < f.length; i++) { builds.push(this.getFeature3D_(f[i], this._getFeatureHeight(f[i]))) } this.drawFeature3D_(ctx, builds) ctx.restore() } /** Create a function that return height of a feature * @param {function|string|number} h a height function or a popertie name or a fixed value * @return {function} function(f) return height of the feature f * @private */ getHfn(h) { switch (typeof (h)) { case 'function': return h case 'string': { var dh = this.get('defaultHeight') return (function (f) { return (Number(f.get(h)) || dh) }) } case 'number': return (function ( /*f*/) { return h }) default: return (function ( /*f*/) { return 10 }) } } /** Animate rendering * @param {*} options * @param {string|function|number} options.height an attribute name or a function returning height of a feature or a fixed value * @param {number} options.duration the duration of the animatioin ms, default 1000 * @param {ol.easing} options.easing an ol easing function * @api */ animate(options) { options = options || {} this.toHeight_ = this.getHfn(options.height) this.animate_ = new Date().getTime() this.animateDuration_ = options.duration || 1000 this.easing_ = options.easing || ol.easing.easeOut // Force redraw this.changed() } /** Check if animation is on * @return {bool} */ animating() { if (this.animate_ && new Date().getTime() - this.animate_ > this.animateDuration_) { this.animate_ = false } return !!this.animate_ } /** Get height for a feature * @param {ol.Feature} f * @return {number} * @private */ _getFeatureHeight(f) { if (this.animate_) { var h1 = this.height_(f) var h2 = this.toHeight_(f) return (h1 * (1 - this.elapsedRatio_) + this.elapsedRatio_ * h2) } else return this.height_(f) } /** Get hvector for a point * @private */ hvector_(pt, h) { var p0 = [ pt[0] * this.matrix_[0] + pt[1] * this.matrix_[1] + this.matrix_[4], pt[0] * this.matrix_[2] + pt[1] * this.matrix_[3] + this.matrix_[5] ] return { p0: p0, p1: [ p0[0] + h / this.res_ * (p0[0] - this.center_[0]), p0[1] + h / this.res_ * (p0[1] - this.center_[1]) ] } } /** Get a vector 3D for a feature * @private */ getFeature3D_(f, h) { var geom = this.get('geometry')(f) var c = geom.getCoordinates() switch (geom.getType()) { case "Polygon": c = [c] // fallthrough case "MultiPolygon": var build = [] for (var i = 0; i < c.length; i++) { for (var j = 0; j < c[i].length; j++) { var b = [] for (var k = 0; k < c[i][j].length; k++) { b.push(this.hvector_(c[i][j][k], h)) } build.push(b) } } return { type: "MultiPolygon", feature: f, geom: build } case "Point": return { type: "Point", feature: f, geom: this.hvector_(c, h) } default: return {} } } /** Draw 3D feature * @private */ drawFeature3D_(ctx, build) { var i, j, b, k // Construct for (i = 0; i < build.length; i++) { switch (build[i].type) { case "MultiPolygon": { for (j = 0; j < build[i].geom.length; j++) { b = build[i].geom[j] for (k = 0; k < b.length; k++) { ctx.beginPath() ctx.moveTo(b[k].p0[0], b[k].p0[1]) ctx.lineTo(b[k].p1[0], b[k].p1[1]) ctx.stroke() } } break } case "Point": { var g = build[i].geom ctx.beginPath() ctx.moveTo(g.p0[0], g.p0[1]) ctx.lineTo(g.p1[0], g.p1[1]) ctx.stroke() break } default: break } } // Roof for (i = 0; i < build.length; i++) { switch (build[i].type) { case "MultiPolygon": { ctx.beginPath() for (j = 0; j < build[i].geom.length; j++) { b = build[i].geom[j] if (j == 0) { ctx.moveTo(b[0].p1[0], b[0].p1[1]) for (k = 1; k < b.length; k++) { ctx.lineTo(b[k].p1[0], b[k].p1[1]) } } else { ctx.moveTo(b[0].p1[0], b[0].p1[1]) for (k = b.length - 2; k >= 0; k--) { ctx.lineTo(b[k].p1[0], b[k].p1[1]) } } ctx.closePath() } ctx.fill("evenodd") ctx.stroke() break } case "Point": { b = build[i] var t = b.feature.get('label') if (t) { var p = b.geom.p1 var m = ctx.measureText(t) var h = Number(ctx.font.match(/\d+(\.\d+)?/g).join([])) ctx.fillRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10) ctx.strokeRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10) ctx.save() ctx.fillStyle = ol.color.asString(this._style.getText().getFill().getColor()) ctx.textAlign = 'center' ctx.textBaseline = 'bottom' ctx.fillText(t, p[0], p[1]) ctx.restore() } break } default: break } } } } /* 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). @classdesc ol.source.WikiCommons is a source that load Wikimedia Commons content in a vector layer. Inherits from: */ /** * @constructor ol.source.WikiCommons * @extends {ol.source.Vector} * @param {olx.source.WikiCommons=} options */ ol.source.WikiCommons = class olsourceWikiCommons extends ol.source.Vector { constructor(opt_options) { var options = opt_options || {} options.loader = function(extent, resolution, projection) { return this._loaderFn(extent, resolution, projection) } /** Default attribution */ if (!options.attributions) options.attributions = [ '© Wikimedia Commons'] // Bbox strategy : reload at each move if (!options.strategy) options.strategy = ol.loadingstrategy.bbox super(options) /** Max resolution to load features */ this._maxResolution = options.maxResolution || 100 /** Result language */ this._lang = options.lang || "fr" /** Query limit */ this._limit = options.limit || 100 } /** Decode wiki attributes and choose to add feature to the layer * @param {feature} the feature * @param {attributes} wiki attributes * @return {boolean} true: add the feature to the layer * @API stable */ readFeature(feature, attributes) { feature.set("descriptionurl", attributes.descriptionurl) feature.set("url", attributes.url) feature.set("title", attributes.title.replace(/^file:|.jpg$/ig, "")) feature.set("thumbnail", attributes.url.replace(/^(.+wikipedia\/commons)\/([a-zA-Z0-9]\/[a-zA-Z0-9]{2})\/(.+)$/, "$1/thumb/$2/$3/200px-$3")) feature.set("user", attributes.user) if (attributes.extmetadata && attributes.extmetadata.LicenseShortName) { feature.set("copy", attributes.extmetadata.LicenseShortName.value) } return true } /** Loader function used to load features. * @private */ _loaderFn(extent, resolution, projection) { if (resolution > this._maxResolution) return var self = this var bbox = ol.proj.transformExtent(extent, projection, "EPSG:4326") // Commons API: for more info @see https://commons.wikimedia.org/wiki/Commons:API/MediaWiki var url = "https://commons.wikimedia.org/w/api.php?action=query&format=json&origin=*&prop=coordinates|imageinfo" + "&generator=geosearch&iiprop=timestamp|user|url|extmetadata|metadata|size&iiextmetadatafilter=LicenseShortName" + "&ggsbbox=" + bbox[3] + "|" + bbox[0] + "|" + bbox[1] + "|" + bbox[2] + "&ggslimit=" + this._limit + "&iilimit=" + (this._limit - 1) + "&ggsnamespace=6" // Ajax request to get the tile ol.ext.Ajax.get({ url: url, success: function (data) { //console.log(data); var features = [] var att, pt, feature if (!data.query || !data.query.pages) return for (var i in data.query.pages) { att = data.query.pages[i] if (att.coordinates && att.coordinates.length) { pt = [att.coordinates[0].lon, att.coordinates[0].lat] } else { var meta = att.imageinfo[0].metadata if (!meta) { //console.log(att); continue } pt = [] var found = 0 for (var k = 0; k < meta.length; k++) { if (meta[k].name == "GPSLongitude") { pt[0] = meta[k].value found++ } if (meta[k].name == "GPSLatitude") { pt[1] = meta[k].value found++ } } if (found != 2) { //console.log(att); continue } } feature = new ol.Feature(new ol.geom.Point(ol.proj.transform(pt, "EPSG:4326", projection))) att.imageinfo[0].title = att.title if (self.readFeature(feature, att.imageinfo[0])) { features.push(feature) } } self.addFeatures(features) } }) } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (http://www.cecill.info/). ol.layer.AnimatedCluster is a vector layer that animate cluster */ /** * A vector layer for animated cluster * @constructor * @extends {ol.layer.Vector} * @param {olx.layer.AnimatedClusterOptions=} options extend olx.layer.Options * @param {Number} options.animationDuration animation duration in ms, default is 700ms * @param {ol.easingFunction} animationMethod easing method to use, default ol.easing.easeOut */ ol.layer.AnimatedCluster = class ollayerAnimatedCluster extends ol.layer.Vector { constructor(opt_options) { var options = opt_options || {} super(options) this.oldcluster = new ol.source.Vector() this.clusters = [] this.animation = { start: false } this.set('animationDuration', typeof (options.animationDuration) == 'number' ? options.animationDuration : 700) this.set('animationMethod', options.animationMethod || ol.easing.easeOut) // Save cluster before change this.getSource().on('change', this.saveCluster.bind(this)) // Animate the cluster this.on(['precompose', 'prerender'], this.animate.bind(this)) this.on(['postcompose', 'postrender'], this.postanimate.bind(this)) } /** save cluster features before change * @private */ saveCluster() { if (this.oldcluster) { this.oldcluster.clear() if (!this.get('animationDuration')) return var features = this.getSource().getFeatures() if (features.length && features[0].get('features')) { this.oldcluster.addFeatures(this.clusters) this.clusters = features.slice(0) this.sourceChanged = true } } } /** * Get the cluster that contains a feature * @private */ getClusterForFeature(f, cluster) { for (var j = 0, c; c = cluster[j]; j++) { var features = c.get('features') if (features && features.length) { for (var k = 0, f2; f2 = features[k]; k++) { if (f === f2) { return c } } } } return false } /** * Stop animation * @private */ stopAnimation() { this.animation.start = false this.animation.cA = [] this.animation.cB = [] } /** * animate the cluster * @private */ animate(e) { var duration = this.get('animationDuration') if (!duration) return var resolution = e.frameState.viewState.resolution // var ratio = e.frameState.pixelRatio; var i, c0, a = this.animation var time = e.frameState.time // Start a new animation, if change resolution and source has changed if (a.resolution != resolution && this.sourceChanged) { var extent = e.frameState.extent if (a.resolution < resolution) { extent = ol.extent.buffer(extent, 100 * resolution) a.cA = this.oldcluster.getFeaturesInExtent(extent) a.cB = this.getSource().getFeaturesInExtent(extent) a.revers = false } else { extent = ol.extent.buffer(extent, 100 * resolution) a.cA = this.getSource().getFeaturesInExtent(extent) a.cB = this.oldcluster.getFeaturesInExtent(extent) a.revers = true } a.clusters = [] for (i = 0, c0; c0 = a.cA[i]; i++) { var f = c0.get('features') if (f && f.length) { var c = this.getClusterForFeature(f[0], a.cB) if (c) a.clusters.push({ f: c0, pt: c.getGeometry().getCoordinates() }) } } // Save state a.resolution = resolution this.sourceChanged = false // No cluster or too much to animate if (!a.clusters.length || a.clusters.length > 1000) { this.stopAnimation() return } // Start animation from now time = a.start = (new Date()).getTime() } // Run animation if (a.start) { var vectorContext = e.vectorContext || ol.render.getVectorContext(e) var d = (time - a.start) / duration // Animation ends if (d > 1.0) { this.stopAnimation() d = 1 } d = this.get('animationMethod')(d) // Animate var style = this.getStyle() var stylefn = (typeof (style) == 'function') ? style : style.length ? function () { return style } : function () { return [style] } // Layer opacity e.context.save() e.context.globalAlpha = this.getOpacity() for (i = 0, c; c = a.clusters[i]; i++) { var pt = c.f.getGeometry().getCoordinates() var dx = pt[0] - c.pt[0] var dy = pt[1] - c.pt[1] if (a.revers) { pt[0] = c.pt[0] + d * dx pt[1] = c.pt[1] + d * dy } else { pt[0] = pt[0] - d * dx pt[1] = pt[1] - d * dy } // Draw feature var st = stylefn(c.f, resolution, true) if (!st.length) st = [st] // If one feature: draw the feature if (c.f.get("features").length === 1 && !dx && !dy) { f = c.f.get("features")[0] } // else draw a point else { var geo = new ol.geom.Point(pt) f = new ol.Feature(geo) } for (var k = 0, s; s = st[k]; k++) { // Multi-line text if (s.getText() && /\n/.test(s.getText().getText())) { var offsetX = s.getText().getOffsetX() var offsetY = s.getText().getOffsetY() var rot = s.getText().getRotation() || 0 var fontSize = Number((s.getText().getFont() || '10px').match(/\d+/)) * 1.2 var str = s.getText().getText().split('\n') var dl, nb = str.length - 1 var s2 = s.clone() // Draw each lines str.forEach(function (t, i) { if (i == 1) { // Allready drawn s2.setImage() s2.setFill() s2.setStroke() } switch (s.getText().getTextBaseline()) { case 'alphabetic': case 'ideographic': case 'bottom': { dl = nb break } case 'hanging': case 'top': { dl = 0 break } default: { dl = nb / 2 break } } s2.getText().setOffsetX(offsetX - Math.sin(rot) * fontSize * (i - dl)) s2.getText().setOffsetY(offsetY + Math.cos(rot) * fontSize * (i - dl)) s2.getText().setText(t) vectorContext.drawFeature(f, ol.ext.getVectorContextStyle(e, s2)) }) } else { vectorContext.drawFeature(f, ol.ext.getVectorContextStyle(e, s)) } /* OLD VERSION OL < 4.3 // Retina device var ratio = e.frameState.pixelRatio; var sc; // OL < v4.3 : setImageStyle doesn't check retina var imgs = ol.Map.prototype.getFeaturesAtPixel ? false : s.getImage(); if (imgs) { sc = imgs.getScale(); imgs.setScale(sc*ratio); } // OL3 > v3.14 if (vectorContext.setStyle) { // If one feature: draw the feature if (c.f.get("features").length===1 && !dx && !dy) { vectorContext.drawFeature(c.f.get("features")[0], s); } // else draw a point else { vectorContext.setStyle(s); vectorContext.drawGeometry(geo); } } // older version else { vectorContext.setImageStyle(imgs); vectorContext.setTextStyle(s.getText()); vectorContext.drawPointGeometry(geo); } if (imgs) imgs.setScale(sc); */ } } e.context.restore() // tell ol to continue postcompose animation e.frameState.animate = true // Prevent layer drawing (clip with null rect) e.context.save() e.context.beginPath() e.context.rect(0, 0, 0, 0) e.context.clip() this.clip_ = true } return } /** * remove clipping after the layer is drawn * @private */ postanimate(e) { if (this.clip_) { e.context.restore() this.clip_ = false } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * @classdesc * Image layer to use with a GeoImage source and return the extent calcaulted with this source. * @extends {ol.layer.Image} * @param {Object=} options Layer Image options. * @api */ ol.layer.GeoImage = class ollayerGeoImage extends ol.layer.Image { constructor(options) { super(options); } /** * Return the {@link module:ol/extent~Extent extent} of the source associated with the layer. * @return {ol.Extent} The layer extent. * @observable * @api */ getExtent() { return this.getSource().getExtent(); } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ // /** IGN's Geoportail WMTS layer definition * @constructor * @extends {ol.layer.Tile} * @param {olx.layer.WMTSOptions=} options WMTS options if not defined default are used * @param {string} options.layer Geoportail layer name * @param {string} options.gppKey Geoportail API key, default use layer registered key * @param {ol.projectionLike} [options.projection=EPSG:3857] projection for the extent, default EPSG:3857 * @param {olx.source.WMTSOptions=} tileoptions WMTS options if not defined default are used */ ol.layer.Geoportail = class ollayerGeoportail extends ol.layer.Tile { constructor(layer, options, tileoptions) { options = options || {} tileoptions = tileoptions || {} // use function(options, tileoption) when layer is set in options if (typeof (layer) !== 'string') { tileoptions = options || {} options = layer layer = options.layer } var maxZoom = options.maxZoom // A source is defined if (options.source) { layer = options.source.getLayer() options.gppKey = options.source.getGPPKey() } var capabilities = window.geoportailConfig ? window.geoportailConfig.capabilities[options.gppKey || options.key] || window.geoportailConfig.capabilities["default"] || ol.layer.Geoportail.capabilities : ol.layer.Geoportail.capabilities capabilities = capabilities[layer] if (!capabilities) capabilities = ol.layer.Geoportail.capabilities[layer] if (!capabilities) { capabilities = { title: layer, originators: [] } console.error("ol.layer.Geoportail: no layer definition for \"" + layer + "\"\nTry to use ol/layer/Geoportail~loadCapabilities() to get it.") // throw new Error("ol.layer.Geoportail: no layer definition for \""+layer+"\""); } // tile options & default params for (var i in capabilities) { if (typeof tileoptions[i] == "undefined") tileoptions[i] = capabilities[i] } if (!tileoptions.gppKey && !tileoptions.key) tileoptions.gppKey = options.gppKey || options.key if (!options.source) options.source = new ol.source.Geoportail(layer, tileoptions) if (!options.title) options.title = capabilities.title if (!options.name) options.name = layer options.layer = layer if (!options.queryable) options.queryable = capabilities.queryable if (!options.desc) options.desc = capabilities.desc if (!options.extent && capabilities.bbox) { if (capabilities.bbox[0] > -170 && capabilities.bbox[2] < 170) { options.extent = ol.proj.transformExtent(capabilities.bbox, 'EPSG:4326', options.projection || 'EPSG:3857') } } options.maxZoom = maxZoom // calculate layer max resolution if (!options.maxResolution && tileoptions.minZoom) { options.source.getTileGrid().minZoom -= (tileoptions.minZoom > 1 ? 2 : 1) options.maxResolution = options.source.getTileGrid().getResolution(options.source.getTileGrid().minZoom) options.source.getTileGrid().minZoom = tileoptions.minZoom } super(options) this._originators = capabilities.originators // BUG GPP: Attributions constraints are not set properly :( /** / // Set attribution according to the originators var counter = 0; // Get default attribution var getAttrib = function(title, o) { if (this.get('attributionMode')==='logo') { if (!title) return ol.source.Geoportail.prototype.attribution; else return ''; } else { if (!title) return ol.source.Geoportail.prototype.attribution; else return '© '+title+'' } }.bind(this); var currentZ, currentCenter = []; var setAttribution = function(e) { var a, o, i; counter--; if (!counter) { var z = e.frameState.viewState.zoom; console.log(e) if (z===currentZ && e.frameState.viewState.center[0]===currentCenter[0] && e.frameState.viewState.center[1]===currentCenter[1]){ return; } currentZ = z; currentCenter = e.frameState.viewState.center; var ex = e.frameState.extent; ex = ol.proj.transformExtent (ex, e.frameState.viewState.projection, 'EPSG:4326'); if (this._originators) { var attrib = this.getSource().getAttributions(); // ol v5 if (typeof(attrib)==='function') attrib = attrib(); attrib.splice(0, attrib.length); var maxZoom = 0; for (a in this._originators) { o = this._originators[a]; for (i=0; i maxZoom && ol.extent.intersects(ex, o.constraint[i].bbox)) { maxZoom = o.constraint[i].maxZoom; } } } if (maxZoom < z) z = maxZoom; if (this.getSource().getTileGrid() && z < this.getSource().getTileGrid().getMinZoom()) { z = this.getSource().getTileGrid().getMinZoom(); } for (a in this._originators) { o = this._originators[a]; if (!o.constraint.length) { attrib.push (getAttrib(a, o)); } else { for (i=0; i= o.constraint[i].minZoom && ol.extent.intersects(ex, o.constraint[i].bbox)) { attrib.push (getAttrib(a, o)); break; } } } } if (!attrib.length) attrib.push ( getAttrib() ); this.getSource().setAttributions(attrib); } } }.bind(this); this.on('precompose', function(e) { counter++; setTimeout(function () { setAttribution(e) }, 500); }); /**/ } /** Register new layer capability * @param {string} layer layer name * @param {*} capability */ static register(layer, capability) { ol.layer.Geoportail.capabilities[layer] = capability } /** Check if a layer registered with a key? * @param {string} layer layer name * @returns {boolean} */ static isRegistered(layer) { return ol.layer.Geoportail.capabilities[layer] && ol.layer.Geoportail.capabilities[layer].key } /** Load capabilities from the service * @param {string} gppKey the API key to get capabilities for * @return {*} Promise-like response */ static loadCapabilities(gppKey, all) { var onSuccess = function () { } var onError = function () { } var onFinally = function () { } this.getCapabilities(gppKey, all).then(function (c) { ol.layer.Geoportail.capabilities = c onSuccess(c) }).catch(function (e) { onError(e) }).finally(function (c) { onFinally(c) }) var response = { then: function (callback) { if (typeof (callback) === 'function') onSuccess = callback return response }, catch: function (callback) { if (typeof (callback) === 'function') onError = callback return response }, finally: function (callback) { if (typeof (callback) === 'function') onFinally = callback return response } } return response } /** Get Key capabilities * @param {string} gppKey the API key to get capabilities for * @return {*} Promise-like response */ static getCapabilities(gppKey) { var capabilities = {} var onSuccess = function () { } var onError = function () { } var onFinally = function () { } var geopresolutions = [156543.03390625, 78271.516953125, 39135.7584765625, 19567.87923828125, 9783.939619140625, 4891.9698095703125, 2445.9849047851562, 1222.9924523925781, 611.4962261962891, 305.74811309814453, 152.87405654907226, 76.43702827453613, 38.218514137268066, 19.109257068634033, 9.554628534317017, 4.777314267158508, 2.388657133579254, 1.194328566789627, 0.5971642833948135, 0.29858214169740677, 0.14929107084870338] // Transform resolution to zoom function getZoom(res) { res = Number(res) * 0.000281 for (var r = 0; r < geopresolutions.length; r++) if (res > geopresolutions[r]) return r } // Merge constraints function mergeConstraints(ori) { for (var i = ori.constraint.length - 1; i > 0; i--) { for (var j = 0; j < i; j++) { var bok = true for (var k = 0; k < 4; k++) { if (ori.constraint[i].bbox[k] != ori.constraint[j].bbox[k]) { bok = false break } } if (!bok) continue if (ori.constraint[i].maxZoom == ori.constraint[j].minZoom || ori.constraint[j].maxZoom == ori.constraint[i].minZoom || ori.constraint[i].maxZoom + 1 == ori.constraint[j].minZoom || ori.constraint[j].maxZoom + 1 == ori.constraint[i].minZoom || ori.constraint[i].minZoom - 1 == ori.constraint[j].maxZoom || ori.constraint[j].minZoom - 1 == ori.constraint[i].maxZoom) { ori.constraint[j].maxZoom = Math.max(ori.constraint[i].maxZoom, ori.constraint[j].maxZoom) ori.constraint[j].minZoom = Math.min(ori.constraint[i].minZoom, ori.constraint[j].minZoom) ori.constraint.splice(i, 1) break } } } } // Get capabilities ol.ext.Ajax.get({ url: 'https://wxs.ign.fr/' + gppKey + '/autoconf/', dataType: 'TEXT', error: function (e) { onError(e) onFinally({}) }, success: function (resp) { var parser = new DOMParser() var config = parser.parseFromString(resp, "text/xml") var layers = config.getElementsByTagName('Layer') for (var i = 0, l; l = layers[i]; i++) { // WMTS ? if (!/WMTS/.test(l.getElementsByTagName('Server')[0].attributes['service'].value)) continue // if (!all && !/geoportail\/wmts/.test(l.find("OnlineResource").attr("href"))) continue; var service = { key: gppKey, server: l.getElementsByTagName('gpp:Key')[0].innerHTML.replace(gppKey + "/", ""), layer: l.getElementsByTagName('Name')[0].innerHTML, title: l.getElementsByTagName('Title')[0].innerHTML, format: l.getElementsByTagName('Format')[0] ? l.getElementsByTagName('Format')[0].innerHTML : 'image.jpeg', style: l.getElementsByTagName('Style')[0].getElementsByTagName('Name')[0].innerHTML, queryable: (l.attributes.queryable.value === '1'), tilematrix: 'PM', minZoom: getZoom(l.getElementsByTagName('sld:MaxScaleDenominator')[0].innerHTML), maxZoom: getZoom(l.getElementsByTagName('sld:MinScaleDenominator')[0].innerHTML), bbox: JSON.parse('[' + l.getElementsByTagName('gpp:BoundingBox')[0].innerHTML + ']'), desc: l.getElementsByTagName('Abstract')[0].innerHTML.replace(/^$/, '$1') } service.originators = {} var origin = l.getElementsByTagName('gpp:Originator') for (var k = 0, o; o = origin[k]; k++) { var ori = service.originators[o.attributes['name'].value] = { href: o.getElementsByTagName('gpp:URL')[0].innerHTML, attribution: o.getElementsByTagName('gpp:Attribution')[0].innerHTML, logo: o.getElementsByTagName('gpp:Logo')[0].innerHTML, minZoom: 20, maxZoom: 0, constraint: [] } // Scale contraints var constraint = o.getElementsByTagName('gpp:Constraint') for (var j = 0, c; c = constraint[j]; j++) { var zmax = getZoom(c.getElementsByTagName('sld:MinScaleDenominator')[0].innerHTML) var zmin = getZoom(c.getElementsByTagName('sld:MaxScaleDenominator')[0].innerHTML) if (zmin > ori.maxZoom) ori.maxZoom = zmin if (zmin < ori.minZoom) ori.minZoom = zmin if (zmax > ori.maxZoom) ori.maxZoom = zmax if (zmax < ori.minZoom) ori.minZoom = zmax ori.constraint.push({ minZoom: zmin, maxZoom: zmax, bbox: JSON.parse('[' + c.getElementsByTagName('gpp:BoundingBox')[0].innerHTML + ']') }) } // Merge constraints mergeConstraints(ori) } capabilities[service.layer] = service } onSuccess(capabilities) onFinally(capabilities) } }) // Promise like response var response = { then: function (callback) { if (typeof (callback) === 'function') onSuccess = callback return response }, catch: function (callback) { if (typeof (callback) === 'function') onError = callback return response }, finally: function (callback) { if (typeof (callback) === 'function') onFinally = callback return response }, } return response } } /** Default capabilities for main layers */ ol.layer.Geoportail.capabilities = { // choisirgeoportail "GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2": { "key":"cartes", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2","title":"Plan IGN v2","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":19,"bbox":[-175,-85,175,85],"desc":"Cartographie multi-échelles sur le territoire national, issue des bases de données vecteur de l’IGN, mis à jour régulièrement et réalisée selon un processus entièrement automatisé. Version actuellement en beta test","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":19,"constraint":[{"minZoom":0,"maxZoom":19,"bbox":[-175,-85,175,85]}]}}}, "CADASTRALPARCELS.PARCELLAIRE_EXPRESS": { "key":"parcellaire", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"CADASTRALPARCELS.PARCELLAIRE_EXPRESS","title":"PCI vecteur","format":"image/png","style":"PCI vecteur","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":19,"bbox":[-63.37252,-21.475586,55.925865,51.31212],"desc":"Plan cadastral informatisé vecteur de la DGFIP.","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":19,"constraint":[{"minZoom":0,"maxZoom":19,"bbox":[-63.37252,-21.475586,55.925865,51.31212]}]}}}, "ORTHOIMAGERY.ORTHOPHOTOS": { "key":"ortho", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"ORTHOIMAGERY.ORTHOPHOTOS","title":"Photographies aériennes","format":"image/jpeg","style":"normal","queryable":true,"tilematrix":"PM","minZoom":0,"bbox":[-178.18713,-22.767689,167.94624,51.11242],"desc":"Photographies aériennes","originators":{"CRCORSE":{"href":"http://www.corse.fr//","attribution":"CRCORSE","logo":"https://wxs.ign.fr/static/logos/CRCORSE/CRCORSE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[8.428783,41.338627,9.688606,43.08541]}]},"SIGLR":{"href":"http://www.siglr.org//","attribution":"SIGLR","logo":"https://wxs.ign.fr/static/logos/SIGLR/SIGLR.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.6784439,42.316307,4.8729386,44.978218]}]},"BOURGOGNE-FRANCHE-COMTE":{"href":"https://www.bourgognefranchecomte.fr/","attribution":"Auvergne","logo":"https://wxs.ign.fr/static/logos/BOURGOGNE-FRANCHE-COMTE/BOURGOGNE-FRANCHE-COMTE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.837849,46.131435,7.1713247,48.408287]}]},"FEDER_AUVERGNE":{"href":"http://www.europe-en-auvergne.eu/","attribution":"Auvergne","logo":"https://wxs.ign.fr/static/logos/FEDER_AUVERGNE/FEDER_AUVERGNE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.0398402,44.60505,3.38408,45.49146]}]},"FEDER_PAYSDELALOIRE":{"href":"https://www.europe.paysdelaloire.fr/","attribution":"Pays-de-la-Loire","logo":"https://wxs.ign.fr/static/logos/FEDER_PAYSDELALOIRE/FEDER_PAYSDELALOIRE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-2.457367,46.19304,0.951426,48.57609]}]},"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":13,"maxZoom":20,"constraint":[{"minZoom":19,"maxZoom":19,"bbox":[-63.160706,-21.401262,55.84643,51.11242]},{"bbox":[0.035491213,43.221077,6.0235267,49.696926]},{"minZoom":20,"maxZoom":20,"bbox":[0.035491213,43.221077,6.0235267,49.696926]},{"minZoom":13,"maxZoom":18,"bbox":[-178.18713,-21.401329,55.85611,51.11242]}]},"E-MEGALIS":{"href":"http://www.e-megalisbretagne.org//","attribution":"Syndicat mixte de coopération territoriale (e-Megalis)","logo":"https://wxs.ign.fr/static/logos/E-MEGALIS/E-MEGALIS.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-3.7059498,47.971947,-1.8486879,48.99035]}]},"FEDER2":{"href":"http://www.europe-en-france.gouv.fr/","attribution":"Fonds européen de développement économique et régional","logo":"https://wxs.ign.fr/static/logos/FEDER2/FEDER2.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.3577043,48.824635,4.269964,50.37648]}]},"PREFECTURE_GUADELOUPE":{"href":"www.guadeloupe.pref.gouv.fr/","attribution":"guadeloupe","logo":"https://wxs.ign.fr/static/logos/PREFECTURE_GUADELOUPE/PREFECTURE_GUADELOUPE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-61.82342,14.371942,-60.787838,16.521578]}]},"OCCITANIE":{"href":"https://www.laregion.fr/","attribution":"La Région Occitanie; Pyrénées - Méditerranée","logo":"https://wxs.ign.fr/static/logos/OCCITANIE/OCCITANIE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.2086434,48.805965,2.4859917,48.915382]}]},"RGD_SAVOIE":{"href":"http://www.rgd.fr","attribution":"Régie de Gestion de Données des Pays de Savoie (RGD 73-74)","logo":"https://wxs.ign.fr/static/logos/RGD_SAVOIE/RGD_SAVOIE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":19,"maxZoom":19,"bbox":[5.7759595,45.65335,7.0887337,46.438328]},{"minZoom":13,"maxZoom":18,"bbox":[5.5923314,45.017353,7.2323394,46.438328]}]},"CG45":{"href":"http://www.loiret.com","attribution":"Le conseil général du Loiret","logo":"https://wxs.ign.fr/static/logos/CG45/CG45.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.4883244,47.471867,3.1349874,48.354233]}]},"CRAIG":{"href":"http://www.craig.fr","attribution":"Centre Régional Auvergnat de l'Information Géographique (CRAIG)","logo":"https://wxs.ign.fr/static/logos/CRAIG/CRAIG.gif","minZoom":13,"maxZoom":20,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.0398402,44.60505,6.4295278,46.8038]},{"minZoom":20,"maxZoom":20,"bbox":[2.2243388,44.76621,2.7314367,45.11295]}]},"e-Megalis":{"href":"http://www.e-megalisbretagne.org//","attribution":"Syndicat mixte de coopération territoriale (e-Megalis)","logo":"https://wxs.ign.fr/static/logos/e-Megalis/e-Megalis.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-5.1937118,47.23789,-0.98568505,48.980812]}]},"PPIGE":{"href":"http://www.ppige-npdc.fr/","attribution":"PPIGE","logo":"https://wxs.ign.fr/static/logos/PPIGE/PPIGE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.5212119,49.957302,4.2673664,51.090965]}]},"CG06":{"href":"http://www.cg06.fr","attribution":"Département Alpes Maritimes (06) en partenariat avec : Groupement Orthophoto 06 (NCA, Ville de Cannes, CARF, CASA,CG06, CA de Grasse) ","logo":"https://wxs.ign.fr/static/logos/CG06/CG06.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[6.6093955,43.44647,7.7436337,44.377018]}]},"MEGALIS-BRETAGNE":{"href":"https://www.megalisbretagne.org/","attribution":"Syndicat mixte Mégalis Bretagne","logo":"https://wxs.ign.fr/static/logos/MEGALIS-BRETAGNE/MEGALIS-BRETAGNE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-5.2086344,47.591938,-3.3396015,48.808697]}]},"FEDER":{"href":"http://www.europe-en-france.gouv.fr/","attribution":"Fonds européen de développement économique et régional","logo":"https://wxs.ign.fr/static/logos/FEDER/FEDER.gif","minZoom":0,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-1.9662633,42.316307,8.25674,50.18387]},{"minZoom":0,"maxZoom":12,"bbox":[-2.400665,41.333557,9.560094,50.366302]}]},"LANGUEDOC-ROUSSILLON":{"href":"https://www.laregion.fr/","attribution":"Région Occitanie","logo":"https://wxs.ign.fr/static/logos/LANGUEDOC-ROUSSILLON/LANGUEDOC-ROUSSILLON.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.6784439,42.63972,4.208843,43.979004]}]},"GRAND_EST":{"href":"https://www.grandest.fr/","attribution":"Hauts-de-France","logo":"https://wxs.ign.fr/static/logos/GRAND_EST/GRAND_EST.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[5.362788,47.390827,7.6924667,49.58011]}]},"CNES_AUVERGNE":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_AUVERGNE/CNES_AUVERGNE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.2656832,45.279934,4.0227704,46.8038]}]},"HAUTS_DE_FRANCE":{"href":"https://www.hautsdefrance.fr/","attribution":"Hauts-de-France","logo":"https://wxs.ign.fr/static/logos/HAUTS_DE_FRANCE/HAUTS_DE_FRANCE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.0740242,48.81521,4.3390365,51.11242]}]},"MPM":{"href":"http://www.marseille-provence.com/","attribution":"Marseille Provence Métropole","logo":"https://wxs.ign.fr/static/logos/MPM/MPM.gif","minZoom":20,"maxZoom":20,"constraint":[{"minZoom":20,"maxZoom":20,"bbox":[5.076959,43.153347,5.7168245,43.454994]}]},"DITTT":{"href":"http://www.dittt.gouv.nc/portal/page/portal/dittt/","attribution":"Direction des Infrastructures, de la Topographie et des Transports Terrestres","logo":"https://wxs.ign.fr/static/logos/DITTT/DITTT.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[163.47784,-22.767689,167.94624,-19.434975]}]},"CNES_978":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_978/CNES_978.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-63.160706,18.04345,-62.962185,18.133898]}]},"CNES_ALSACE":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_ALSACE/CNES_ALSACE.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[6.8086324,47.39981,7.668318,48.32695]}]},"CNES_974":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_974/CNES_974.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[55.205757,-21.401262,55.84643,-20.862825]}]},"CNES_975":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_975/CNES_975.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-56.410988,46.734093,-56.10308,47.149963]}]},"CNES_976":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_976/CNES_976.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[44.916977,-13.089187,45.30442,-12.564543]}]},"CNES_977":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_977/CNES_977.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-62.952805,17.862621,-62.78276,17.98024]}]},"CNES":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES/CNES.gif","minZoom":13,"maxZoom":16,"constraint":[{"minZoom":13,"maxZoom":16,"bbox":[-55.01953,1.845384,-50.88867,6.053161]}]},"ASTRIUM":{"href":"http://www.geo-airbusds.com/","attribution":"Airbus Defence and Space","logo":"https://wxs.ign.fr/static/logos/ASTRIUM/ASTRIUM.gif","minZoom":13,"maxZoom":16,"constraint":[{"minZoom":13,"maxZoom":16,"bbox":[-55.01953,1.845384,-50.88867,6.053161]}]},"CNES_971":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_971/CNES_971.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-61.82342,15.819616,-60.99497,16.521578]}]},"CNES_972":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_972/CNES_972.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-61.247208,14.371855,-60.778458,14.899901]}]}}}, // Deprecated "GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD": {"server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD","title":"Carte IGN","format":"image/jpeg","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":18,"bbox":[-179.62723,-84.5047,179.74588,85.47958],"desc":"Cartographie topographique multi-échelles du territoire français issue des bases de données vecteur de l’IGN - emprise nationale, visible du 1/200 au 1/130000000","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":18,"constraint":[{"minZoom":5,"maxZoom":5,"bbox":[-179.57285,-83.84196,178.4975,85.36646]},{"minZoom":0,"maxZoom":2,"bbox":[-175.99709,-84.42859,175.99709,84.2865]},{"minZoom":3,"maxZoom":3,"bbox":[-176.23093,-84.5047,179.08267,84.89126]},{"minZoom":4,"maxZoom":4,"bbox":[-179.62723,-84.0159,-179.21112,85.47958]},{"minZoom":6,"maxZoom":8,"bbox":[-179.49689,-84.02368,179.74588,85.30035]},{"minZoom":15,"maxZoom":18,"bbox":[-5.6663494,41.209736,10.819784,51.175068]},{"minZoom":14,"maxZoom":14,"bbox":[-5.713191,40.852314,11.429714,51.44377]},{"minZoom":13,"maxZoom":13,"bbox":[-63.37252,13.428586,11.429714,51.44377]},{"minZoom":11,"maxZoom":12,"bbox":[-63.37252,13.428586,11.496459,51.444122]},{"minZoom":9,"maxZoom":9,"bbox":[-64.81273,13.428586,11.496459,51.444016]},{"minZoom":10,"maxZoom":10,"bbox":[-63.37252,13.428586,11.496459,51.444016]}]}}}, // Need API key "GEOGRAPHICALGRIDSYSTEMS.MAPS": {"server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS","title":"Cartes IGN","format":"image/jpeg","style":"normal","queryable":true,"tilematrix":"PM","minZoom":0,"maxZoom":18,"bbox":[-180,-75,180,80],"desc":"Cartes IGN","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":18,"constraint":[{"minZoom":7,"maxZoom":7,"bbox":[-178.20573,-68.138855,144.84375,51.909786]},{"minZoom":8,"maxZoom":8,"bbox":[-178.20573,-68.138855,168.24327,51.909786]},{"minZoom":13,"maxZoom":13,"bbox":[-178.20573,-67.101425,168.24327,51.44377]},{"minZoom":14,"maxZoom":14,"bbox":[-178.20573,-67.101425,168.23909,51.44377]},{"minZoom":11,"maxZoom":12,"bbox":[-178.20573,-67.101425,168.24327,51.444122]},{"minZoom":9,"maxZoom":10,"bbox":[-178.20573,-68.138855,168.24327,51.444016]},{"minZoom":15,"maxZoom":15,"bbox":[-178.20573,-46.502903,168.23909,51.175068]},{"minZoom":16,"maxZoom":16,"bbox":[-178.20573,-46.502903,168.29811,51.175068]},{"minZoom":0,"maxZoom":6,"bbox":[-180,-60,180,80]},{"minZoom":18,"maxZoom":18,"bbox":[-5.6663494,41.209736,10.819784,51.175068]},{"minZoom":17,"maxZoom":17,"bbox":[-179.5,-75,179.5,75]}]},"DITTT":{"href":"http://www.dittt.gouv.nc/portal/page/portal/dittt/","attribution":"Direction des Infrastructures, de la Topographie et des Transports Terrestres","logo":"https://wxs.ign.fr/static/logos/DITTT/DITTT.gif","minZoom":8,"maxZoom":16,"constraint":[{"minZoom":8,"maxZoom":10,"bbox":[163.47784,-22.972307,168.24327,-19.402702]},{"minZoom":11,"maxZoom":13,"bbox":[163.47784,-22.972307,168.24327,-19.494438]},{"minZoom":14,"maxZoom":15,"bbox":[163.47784,-22.764496,168.23909,-19.493542]},{"minZoom":16,"maxZoom":16,"bbox":[163.47784,-22.809465,168.29811,-19.403923]}]}}}, // Other layers "ADMINEXPRESS-COG-CARTO.LATEST": {"key": "administratif", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"ADMINEXPRESS-COG-CARTO.LATEST","title":"ADMINEXPRESS COG CARTO","format":"image/png","style":"normal","queryable":true,"tilematrix":"PM","minZoom":6,"maxZoom":16,"bbox":[-63.37252,-21.475586,55.925865,51.31212],"desc":"Limites administratives Express COG code officiel géographique 2021","originators":{"IGN":{"href":"https://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":6,"maxZoom":16,"constraint":[{"minZoom":6,"maxZoom":16,"bbox":[-63.37252,-21.475586,55.925865,51.31212]}]}}}, "GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN": {"key":"altimetrie","server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN","title":"Carte des pentes","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":17,"bbox":[-63.161392,-21.544624,56.001812,51.099052],"desc":"Carte des zones ayant une valeur de pente supérieure à 30°-35°-40°-45° d'après la BD ALTI au pas de 5m","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":17,"constraint":[{"minZoom":0,"maxZoom":17,"bbox":[-5.1504726,41.32521,9.570543,51.099052]}]}}}, "ELEVATION.SLOPES": {"key":"altimetrie","server":"https://wxs.ign.fr/geoportail/wmts","layer":"ELEVATION.SLOPES","title":"Altitude","format":"image/jpeg","style":"normal","queryable":true,"tilematrix":"PM","minZoom":6,"maxZoom":14,"bbox":[-178.20589,-22.595179,167.43176,50.93085],"desc":"La couche altitude se compose d'un MNT (Modèle Numérique de Terrain) affiché en teintes hypsométriques et issu de la BD ALTI®.","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":6,"maxZoom":14,"constraint":[{"minZoom":6,"maxZoom":14,"bbox":[55.205746,-21.392344,55.846554,-20.86271]}]}}}, "GEOGRAPHICALGRIDSYSTEMS.MAPS.BDUNI.J1": { "key":"cartes", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS.BDUNI.J1","title":"Plan IGN j+1","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":18,"bbox":[-179.5,-75,179.5,75],"desc":"Plan IGN j+1","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":18,"constraint":[{"minZoom":0,"maxZoom":18,"bbox":[-179,-80,179,80]}]}}}, "TRANSPORTNETWORKS.ROADS": { "key": "topographie", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"TRANSPORTNETWORKS.ROADS","title":"Routes","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":6,"maxZoom":18,"bbox":[-63.969162,-21.49687,55.964417,71.584076],"desc":"Affichage du réseau routier français et européen.","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":6,"maxZoom":18,"constraint":[{"minZoom":15,"maxZoom":18,"bbox":[-63.37252,-21.475586,55.925865,51.31212]},{"minZoom":6,"maxZoom":14,"bbox":[-63.969162,-21.49687,55.964417,71.584076]}]}}}, }; /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Return a preview image of the source. * @param {ol.Coordinate|undefined} lonlat The center of the preview. * @param {number} resolution of the preview. * @return {String} the preview url * @api */ ol.source.Source.prototype.getPreview = function(/*lonlat, resolution*/) { return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAk6QAAJOkBUCTn+AAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANeSURBVHic7ZpPiE1RHMc/780MBhkik79JSUlIUbOxI+wkI2yRhYSUlJLNpJF/xcpiJBmZGBZsNM1CkmhKITGkGbH0/BuPmXnP4rxbb/TOn3fvOffeec6nfqvb/b7f93fveeec37ng8Xg8Ho/nf6Uu4d+fDswFssCvhHOJhaXAMeApMAQUyyIPPAdOAiuTStAVy4EHjDWsix5gdRLJ2mY34ulWYz6IEeA4kIk9awtkgTOEM/5vdAKT4k0/Ou3YMR/ELcbRm9AKFLBbgCJwNE4TYZkJfMG++SIwDCyLz0o4bI17WdyJz0r1TAZ+oDcxCBwAFgIzEIuhvcBbg3sLwOK4DFXLFvQGniCGSSUagS4DjUPOHESkA3XiOWCORqMR6Nfo9DjI3QqPUSd+ylBnv0Zn0GrWFvmIOvGNhjqrNDp/EAutyFgRKUM2tgO+Gur81FxvAKYZaimxXYBvmuuLDHWWaK4X0RfJCNsF6NdcbzXU2a65PohYFKWOc+jn8PUajbWIXaBKp9NB7lZYh34OzwFbFfd/NtDYYSth27urLGIm0M31AL3APWAAmIooymaDnPIl/Vz4NN1yHrd7gcvxWQnHAuA3bsyPop8hUsE13BSgK04TUViBeFo2zedJ8S6wElexW4D2eNOPTjNi6WvD/DtEr8E6tk6GGoAmxFY2iFHE9NZiQf8gogiB9gTEH23izAZuE77vHyU+ANucO1QwD3hD/MbLowAcdm20EmkwXx4n3NodS9rMB2HabYpEWs0HcRqHp0fNwAvJD+eBTZr7p6BvmQVxUaEzEbiruNfJekH15L8jtrEm7JJolEcOmKXRqQOuKDQuY7HZY8s8iNfzkSLxIuI43FTrkkLnOlBfRW4VsWk+oAX5weknxFAxJQNckGgVgZuIRVoomoGXEmGTMa+iQ6K7M4SW7k24QYgiuDQPYinbhugiF4H3RGtzZYCzyIvQXfpNI1ybLyeLpf5+iTbkRbiP2EcocTHm4+YI8iI8RFHwWjAfsA95Q+YZFU6wasl8wB7kReijtNbIILa0vcg/PRlGfPQwHmlCviDqAzaA+OREtzqr1ejOIDorxlNEjTGUBV4nnUWCvAJxGDlA8q9j3DEArAn2zvXAfOwfl6eVAmJrPpJ0Ih6Px+PxeJLjLwPul3vj5d0eAAAAAElFTkSuQmCC"; }; /** * Return the tile image of the source. * @param {ol.Coordinate|undefined} lonlat The center of the preview. * @param {number} resolution of the preview. * @return {String} the preview url * @api */ ol.source.Tile.prototype.getPreview = function(lonlat, resolution) { if (!lonlat) lonlat = [21020, 6355964]; if (!resolution) resolution = 150; var coord = this.getTileGrid().getTileCoordForCoordAndResolution(lonlat, resolution); var fn = this.getTileUrlFunction(); return fn.call(this, coord, this.getProjection()); }; /** * Return the tile image of the source. * @param {ol.Coordinate|undefined} lonlat The center of the preview. * @param {number} resolution of the preview. * @return {String} the preview url * @api */ ol.source.TileWMS.prototype.getPreview = function(lonlat, resolution) { if (!lonlat) lonlat = [21020, 6355964]; if (!resolution) resolution = 150; var fn = this.getTileUrlFunction(); if (fn) { var tileGrid = this.getTileGrid() || this.getTileGridForProjection(this.getProjection()); var coord = tileGrid.getTileCoordForCoordAndResolution(lonlat, resolution); return fn.call(this, coord, 1, this.getProjection()); } // Use getfeature info instead var url = this.getGetFeatureInfoUrl ? this.getGetFeatureInfoUrl(lonlat, resolution, this.getProjection() || 'EPSG:3857', {}) : this.getFeatureInfoUrl(lonlat, resolution, this.getProjection() || 'EPSG:3857', {}); url = url.replace(/getfeatureinfo/i,"GetMap"); return url; }; /** * Return a preview for the layer. * @param {ol.Coordinate|undefined} lonlat The center of the preview. * @param {number} resolution of the preview. * @return {Array} list of preview url * @api */ ol.layer.Base.prototype.getPreview = function(lonlat, resolution, projection) { if (this.get("preview")) return [ this.get("preview") ]; if (!resolution) resolution = 150; // Get middle resolution if (resolution < this.getMinResolution() || resolution > this.getMaxResolution()) { var rmin = this.getMinResolution(), rmax = this.getMaxResolution(); if (rmax>100000) rmax = 156543; // min zoom : world if (rmin<0.15) rmin = 0.15; // max zoom resolution = rmax; while (rmax>rmin) { rmin *= 2; rmax /= 2; resolution = rmin; } } var e = this.getExtent(); if (!lonlat) lonlat = [21020, 6355964]; // Default lonlat if (e && !ol.extent.containsCoordinate(e,lonlat)) lonlat = [ (e[0]+e[2])/2, (e[1]+e[3])/2 ]; if (projection) lonlat = ol.proj.transform (lonlat, projection, this.getSource().getProjection()); if (this.getSource && this.getSource()) { try { return [ this.getSource().getPreview(lonlat, resolution) ]; } catch(e) { /* nothing to do */ } } return []; }; /** * Return a preview for the layer. * @param {_ol_coordinate_|undefined} lonlat The center of the preview. * @param {number} resolution of the preview. * @return {Array} list of preview url * @api */ ol.layer.Group.prototype.getPreview = function(lonlat, resolution) { if (this.get("preview")) return [ this.get("preview") ]; var t = []; if (this.getLayers) { var l = this.getLayers().getArray(); for (var i=0; i this.get('maxResolution')) return this.res_ = res * 400 if (this.animate_) { var elapsed = e.frameState.time - this.animate_ if (elapsed < this.animateDuration_) { this.elapsedRatio_ = this.easing_(elapsed / this.animateDuration_) // tell OL3 to continue postcompose animation e.frameState.animate = true } else { this.animate_ = false this.height_ = this.toHeight_ } } var ratio = e.frameState.pixelRatio var ctx = e.context var m = this.matrix_ = e.frameState.coordinateToPixelTransform // Old version (matrix) if (!m) { m = e.frameState.coordinateToPixelMatrix, m[2] = m[4] m[3] = m[5] m[4] = m[12] m[5] = m[13] } this.center_ = [ctx.canvas.width / 2 / ratio, ctx.canvas.height / ratio] var f = this.layer_.getSource().getFeaturesInExtent(e.frameState.extent) ctx.save() ctx.scale(ratio, ratio) var s = this.getStyle() ctx.lineWidth = s.getStroke().getWidth() ctx.strokeStyle = ol.color.asString(s.getStroke().getColor()) ctx.fillStyle = ol.color.asString(s.getFill().getColor()) var builds = [] for (var i = 0; i < f.length; i++) { var h = this.getFeatureHeight(f[i]) if (h) builds.push(this.getFeature3D_(f[i], h)) } if (this.get('ghost')) this.drawGhost3D_(ctx, builds) else this.drawFeature3D_(ctx, builds) ctx.restore() } /** Set layer to render 3D */ setLayer(l) { if (this._listener) { this._listener.forEach(function (l) { ol.Observable.unByKey(l) }) } this.layer_ = l this._listener = l.on(['postcompose', 'postrender'], this.onPostcompose_.bind(this)) } /** Create a function that return height of a feature * @param {function|string|number} h a height function or a popertie name or a fixed value * @return {function} function(f) return height of the feature f */ getHfn(h) { switch (typeof (h)) { case 'function': return h case 'string': { var dh = this.get('defaultHeight') return (function (f) { return (Number(f.get(h)) || dh) }) } case 'number': return (function ( /*f*/) { return h }) default: return (function ( /*f*/) { return 10 }) } } /** Animate rendering * @param {olx.render3D.animateOptions} * @param {string|function|number} param.height an attribute name or a function returning height of a feature or a fixed value * @param {number} param.duration the duration of the animatioin ms, default 1000 * @param {ol.easing} param.easing an ol easing function * @api */ animate(options) { options = options || {} this.toHeight_ = this.getHfn(options.height) this.animate_ = new Date().getTime() this.animateDuration_ = options.duration || 1000 this.easing_ = options.easing || ol.easing.easeOut // Force redraw this.layer_.changed() } /** Check if animation is on * @return {bool} */ animating() { if (this.animate_ && new Date().getTime() - this.animate_ > this.animateDuration_) { this.animate_ = false } return !!this.animate_ } /** Get feature height * @param {ol.Feature} f */ getFeatureHeight(f) { if (this.animate_) { var h1 = this.height_(f) var h2 = this.toHeight_(f) return (h1 * (1 - this.elapsedRatio_) + this.elapsedRatio_ * h2) } else return this.height_(f) } /** Get elevation line * @private */ hvector_(pt, h) { var p0 = [ pt[0] * this.matrix_[0] + pt[1] * this.matrix_[1] + this.matrix_[4], pt[0] * this.matrix_[2] + pt[1] * this.matrix_[3] + this.matrix_[5] ] return { p0: p0, p1: [ p0[0] + h / this.res_ * (p0[0] - this.center_[0]), p0[1] + h / this.res_ * (p0[1] - this.center_[1]) ] } } /** Get drawing * @private */ getFeature3D_(f, h) { var geom = this.get('geometry')(f) var c = geom.getCoordinates() switch (geom.getType()) { case "Polygon": c = [c] // fallthrough case "MultiPolygon": var build = [] for (var i = 0; i < c.length; i++) { for (var j = 0; j < c[i].length; j++) { var b = [] for (var k = 0; k < c[i][j].length; k++) { b.push(this.hvector_(c[i][j][k], h)) } build.push(b) } } return { type: "MultiPolygon", feature: f, geom: build, height: h } case "Point": return { type: "Point", feature: f, geom: this.hvector_(c, h), height: h } default: return {} } } /** Draw a feature * @param {CanvasRenderingContext2D} ctx * @param {ol.Feature} build * @private */ drawFeature3D_(ctx, build) { var i, j, b, k // Construct for (i = 0; i < build.length; i++) { switch (build[i].type) { case "MultiPolygon": { for (j = 0; j < build[i].geom.length; j++) { b = build[i].geom[j] for (k = 0; k < b.length; k++) { ctx.beginPath() ctx.moveTo(b[k].p0[0], b[k].p0[1]) ctx.lineTo(b[k].p1[0], b[k].p1[1]) ctx.stroke() } } break } case "Point": { var g = build[i].geom ctx.beginPath() ctx.moveTo(g.p0[0], g.p0[1]) ctx.lineTo(g.p1[0], g.p1[1]) ctx.stroke() break } default: break } } // Roof for (i = 0; i < build.length; i++) { switch (build[i].type) { case "MultiPolygon": { ctx.beginPath() for (j = 0; j < build[i].geom.length; j++) { b = build[i].geom[j] if (j == 0) { ctx.moveTo(b[0].p1[0], b[0].p1[1]) for (k = 1; k < b.length; k++) { ctx.lineTo(b[k].p1[0], b[k].p1[1]) } } else { ctx.moveTo(b[0].p1[0], b[0].p1[1]) for (k = b.length - 2; k >= 0; k--) { ctx.lineTo(b[k].p1[0], b[k].p1[1]) } } ctx.closePath() } ctx.fill("evenodd") ctx.stroke() break } case "Point": { b = build[i] var t = b.feature.get('label') if (t) { var p = b.geom.p1 var f = ctx.fillStyle ctx.fillStyle = ctx.strokeStyle ctx.textAlign = 'center' ctx.textBaseline = 'bottom' ctx.fillText(t, p[0], p[1]) var m = ctx.measureText(t) var h = Number(ctx.font.match(/\d+(\.\d+)?/g).join([])) ctx.fillStyle = "rgba(255,255,255,0.5)" ctx.fillRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10) ctx.strokeRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10) ctx.fillStyle = f //console.log(build[i].feature.getProperties()) } break } default: break } } } /** * @private */ drawGhost3D_(ctx, build) { var i, j, b, k // Construct for (i = 0; i < build.length; i++) { switch (build[i].type) { case "MultiPolygon": { for (j = 0; j < build[i].geom.length; j++) { b = build[i].geom[j] for (k = 0; k < b.length - 1; k++) { ctx.beginPath() ctx.moveTo(b[k].p0[0], b[k].p0[1]) ctx.lineTo(b[k].p1[0], b[k].p1[1]) ctx.lineTo(b[k + 1].p1[0], b[k + 1].p1[1]) ctx.lineTo(b[k + 1].p0[0], b[k + 1].p0[1]) ctx.lineTo(b[k].p0[0], b[k].p0[1]) var m = [(b[k].p0[0] + b[k + 1].p0[0]) / 2, (b[k].p0[1] + b[k + 1].p0[1]) / 2] var h = [b[k].p0[1] - b[k + 1].p0[1], -b[k].p0[0] + b[k + 1].p0[0]] var c = ol.coordinate.getIntersectionPoint( [m, [m[0] + h[0], m[1] + h[1]]], [b[k].p1, b[k + 1].p1] ) var gradient = ctx.createLinearGradient( m[0], m[1], c[0], c[1] ) gradient.addColorStop(0, 'rgba(255,255,255,.2)') gradient.addColorStop(1, 'rgba(255,255,255,0)') ctx.fillStyle = gradient ctx.fill() } } break } case "Point": { var g = build[i].geom ctx.beginPath() ctx.moveTo(g.p0[0], g.p0[1]) ctx.lineTo(g.p1[0], g.p1[1]) ctx.stroke() break } default: break } } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A sketch layer used as overlay to handle drawing sketch (helper for drawing tools) * @constructor * @extends {ol/layer/Vector} * @fires drawstart * @fires drawend * @fires drawabort * @param {*} options * @param {string} options.type Geometry type, default LineString * @param {ol.style.Style|Array} options.style Drawing style * @param {ol.style.Style|Array} options.sketchStyle Sketch style */ ol.layer.SketchOverlay = class ollayerSketchOverlay extends ol.layer.Vector { constructor(options) { options = options || {} var style = options.style || ol.style.Style.defaultStyle(true) var sketchStyle = options.sketchStyle if (!sketchStyle) { sketchStyle = ol.style.Style.defaultStyle() sketchStyle = [ new ol.style.Style({ image: new ol.style.RegularShape({ points: 4, radius: 10, radius2: 0, stroke: new ol.style.Stroke({ color: [255, 255, 255, .5], width: 3 }) }) }), sketchStyle[0].clone() ] sketchStyle[1].setImage(new ol.style.RegularShape({ points: 4, radius: 10, radius2: 0, stroke: new ol.style.Stroke({ color: [0, 153, 255, 1], width: 1.25 }) })) } super({ name: 'sketch', source: new ol.source.Vector({ useSpatialIndex: false }), style: function (f) { return (f.get('sketch') ? sketchStyle : style) }, updateWhileAnimating: true, updateWhileInteracting: true }) this._geom = [] // Sketch features this.getSource().addFeatures([ new ol.Feature({ sketch: true, geometry: new ol.geom.Point([]) }), new ol.Feature({ sketch: true, geometry: new ol.geom.LineString([]) }), new ol.Feature(), new ol.Feature(new ol.geom.Point([])) ]) this.setGeometryType(options.type) } /** Set geometry type * @param {string} type Geometry type * @return {string} the current type */ setGeometryType(type) { var t = /^Point$|^LineString$|^Polygon$|^Circle$/.test(type) ? type : 'LineString' if (t !== this._type) { this.abortDrawing() this._type = t } return this._type } /** Get geometry type * @return {string} Geometry type */ getGeometryType() { return this._type } /** Add a new Point to the sketch * @param {ol.coordinate} coord * @return {boolean} true if point has been added, false if same coord */ addPoint(coord) { if (this._lastCoord !== this._position) { if (!this._geom.length) { this.startDrawing() } this._geom.push(coord) this._lastCoord = coord this._position = coord this.drawSketch() if (this.getGeometryType() === 'Point') { this.finishDrawing() } if (this.getGeometryType() === 'Circle' && this._geom.length >= 2) { this.finishDrawing() } return true } return false } /** Remove the last Point from the sketch */ removeLastPoint() { this._geom.pop() this._lastCoord = this._geom[this._geom.length - 1] this.drawSketch() } /** Strat a new drawing * @param {*} options * @param {string} type Geometry type, default the current type * @param {Array} coordinates a list of coordinates to extend * @param {ol.Feature} feature a feature to extend (LineString or Polygon only) * @param {boolean} atstart extent coordinates or feature at start, default false (extend at end) */ startDrawing(options) { options = options || {} this._geom = [] if (options.type) this.setGeometryType(options.type) this.drawSketch() if (!this._drawing) { this.dispatchEvent({ type: 'drawstart', feature: this.getFeature() }) } this._drawing = true } /** Finish drawing * @return {ol.Feature} the drawed feature */ finishDrawing(valid) { var f = this.getSource().getFeatures()[2].clone() var isvalid = !!f switch (this.getGeometryType()) { case 'Circle': case 'LineString': { isvalid = this._geom.length > 1 break } case 'Polygon': { isvalid = this._geom.length > 2 break } } if (valid && !isvalid) return false this._geom = [] this._lastCoord = null this.drawSketch() if (this._drawing) { this.dispatchEvent({ type: 'drawend', valid: isvalid, feature: f }) } this._drawing = false return f } /** Abort drawing */ abortDrawing() { if (this._drawing) { this.dispatchEvent({ type: 'drawabort', feature: this.getFeature() }) } this._drawing = false this._geom = [] this._lastCoord = null this.drawSketch() } /** Set current position * @param {ol.coordinate} coord */ setPosition(coord) { this._position = coord this.drawLink() } /** Get current position * @return {ol.coordinate} */ getPosition() { return this._position } /** Draw/refresh link */ drawLink() { var features = this.getSource().getFeatures() if (this._position) { if (this._lastCoord && this._lastCoord === this._position) { features[0].getGeometry().setCoordinates([]) } else { features[0].getGeometry().setCoordinates(this._position) } if (this._geom.length) { if (this.getGeometryType() === 'Circle') { features[1].setGeometry(new ol.geom.Circle(this._geom[0], ol.coordinate.dist2d(this._geom[0], this._position))) } else if (this.getGeometryType() === 'Polygon') { features[1].setGeometry(new ol.geom.LineString([this._lastCoord, this._position, this._geom[0]])) } else { features[1].setGeometry(new ol.geom.LineString([this._lastCoord, this._position])) } } else { features[1].setGeometry(new ol.geom.LineString([])) } } else { features[0].getGeometry().setCoordinates([]) features[1].setGeometry(new ol.geom.LineString([])) } } /** Get current feature */ getFeature() { return this.getSource().getFeatures()[2] } /** Draw/refresh sketch */ drawSketch() { this.drawLink() var features = this.getSource().getFeatures() if (!this._geom.length) { features[2].setGeometry(null) features[3].setGeometry(new ol.geom.Point([])) } else { if (!this._lastCoord) this._lastCoord = this._geom[this._geom.length - 1] features[3].getGeometry().setCoordinates(this._lastCoord) switch (this._type) { case 'Point': { features[2].setGeometry(new ol.geom.Point(this._lastCoord)) break } case 'Circle': { if (!features[2].getGeometry()) { features[2].setGeometry(new ol.geom.Circle(this._geom[0], ol.coordinate.dist2d(this._geom[0], this._geom[this._geom.length - 1]))) } else { features[2].getGeometry().setRadius(ol.coordinate.dist2d(this._geom[0], this._geom[this._geom.length - 1])) } break } case 'LineString': { if (!features[2].getGeometry()) { features[2].setGeometry(new ol.geom.LineString(this._geom)) } else { features[2].getGeometry().setCoordinates(this._geom) } break } case 'Polygon': { this._geom.push(this._geom[0]) if (!features[2].getGeometry()) { features[2].setGeometry(new ol.geom.Polygon([this._geom])) } else { features[2].getGeometry().setCoordinates([this._geom]) } this._geom.pop() break } default: { console.error('[ol/layer/SketchOverlay~drawSketch] geometry type not supported (' + this._type + ')') break } } } } } /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A map with a perspective * @constructor * @extends {ol.Map} * @fires change:perspective * @param {olx.MapOptions=} options * @param {ol.events.condition} tiltCondition , default altKeyOnly */ ol.PerspectiveMap = class olPerspectiveMap extends ol.Map { constructor(options) { // Map div var divMap = options.target instanceof Element ? options.target : document.getElementById(options.target); if (window.getComputedStyle(divMap).position !== 'absolute') { divMap.style.position = 'relative'; } divMap.style.overflow = 'hidden'; // Create map inside var map = ol.ext.element.create('DIV', { className: 'ol-perspective-map', parent: divMap }); var opts = {}; Object.assign(opts, options); opts.target = map; // enhance pixel ratio //opts.pixelRatio = 2; super(opts); this._tiltCondition = options.tiltCondition || ol.events.condition.altKeyOnly; } /** Get pixel ratio for the map */ getPixelRatio() { return window.devicePixelRatio; } /** Set perspective angle * @param {number} angle the perspective angle 0 (vertical) - 30 (max), default 0 * @param {*} options * @param {number} options.duration The duration of the animation in milliseconds, default 500 * @param {function} options.easing The easing function used during the animation, defaults to ol.easing.inAndOut). */ setPerspective(angle, options) { options = options || {}; // max angle if (angle > 30) angle = 30; else if (angle < 0) angle = 0; var fromAngle = this._angle || 0; var toAngle = Math.round(angle * 10) / 10; var style = this.getTarget().querySelector('.ol-layers').style; cancelAnimationFrame(this._animatedPerspective); requestAnimationFrame(function (t) { this._animatePerpective(t, t, style, fromAngle, toAngle, options.duration, options.easing || ol.easing.inAndOut); }.bind(this)); } /** Animate the perspective * @param {number} t0 starting timestamp * @param {number} t current timestamp * @param {CSSStyleDeclaration} style style to modify * @param {number} fromAngle starting angle * @param {number} toAngle ending angle * @param {number} duration The duration of the animation in milliseconds, default 500 * @param {function} easing The easing function used during the animation, defaults to ol.easing.inAndOut). * @private */ _animatePerpective(t0, t, style, fromAngle, toAngle, duration, easing) { var dt, end; if (duration === 0) { dt = 1; end = true; } else { dt = (t - t0) / (duration || 500); end = (dt >= 1); } dt = easing(dt); var angle; if (end) { angle = this._angle = toAngle; } else { angle = this._angle = fromAngle + (toAngle - fromAngle) * dt; } var fac = angle / 30; // apply transform to the style style.transform = 'translateY(-' + (17 * fac) + '%) perspective(200px) rotateX(' + angle + 'deg) scaleY(' + (1 - fac / 2) + ')'; this.getMatrix3D(true); this.render(); if (!end) { requestAnimationFrame(function (t) { this._animatePerpective(t0, t, style, fromAngle, toAngle, duration || 500, easing || ol.easing.inAndOut); }.bind(this)); } // Dispatch event this.dispatchEvent({ type: 'change:perspective', angle: angle, animating: !end }); } /** Convert to pixel coord according to the perspective * @param {MapBrowserEvent} mapBrowserEvent The event to handle. */ handleMapBrowserEvent(e) { e.pixel = [ e.originalEvent.offsetX / this.getPixelRatio(), e.originalEvent.offsetY / this.getPixelRatio() ]; e.coordinate = this.getCoordinateFromPixel(e.pixel); ol.Map.prototype.handleMapBrowserEvent.call(this, e); // Change perspective on tilt condition if (this._tiltCondition(e)) { switch (e.type) { case 'pointerdown': { this._dragging = e.originalEvent.offsetY; break; } case 'pointerup': { this._dragging = false; break; } case 'pointerdrag': { if (this._dragging !== false) { var angle = e.originalEvent.offsetY > this._dragging ? .5 : -.5; if (angle) { this.setPerspective((this._angle || 0) + angle, { duration: 0 }); } this._dragging = e.originalEvent.offsetY; } break; } } } else { this._dragging = false; } } /** Get map full teansform matrix3D * @return {Array>} */ getMatrix3D(compute) { if (compute) { var ele = this.getTarget().querySelector('.ol-layers'); // Get transform matrix3D from CSS var tx = ol.matrix3D.getTransform(ele); // Get the CSS transform origin from the transformed parent - default is '50% 50%' var txOrigin = ol.matrix3D.getTransformOrigin(ele); // Compute the full transform that is applied to the transformed parent (-origin * tx * origin) this._matrixTransform = ol.matrix3D.computeTransformMatrix(tx, txOrigin); } if (!this._matrixTransform) this._matrixTransform = ol.matrix3D.identity(); return this._matrixTransform; } /** Get pixel at screen from coordinate. * The default getPixelFromCoordinate get pixel in the perspective. * @param {ol.coordinate} coord * @param {ol.pixel} */ getPixelScreenFromCoordinate(coord) { // Get pixel in the transform system var px = this.getPixelFromCoordinate(coord); // Get transform matrix3D from CSS var fullTx = this.getMatrix3D(); // Transform the point using full transform var pixel = ol.matrix3D.transformVertex(fullTx, px); // Perform the homogeneous divide to apply perspective to the points (divide x,y,z by the w component). pixel = ol.matrix3D.projectVertex(pixel); return [pixel[0], pixel[1]]; } /** Not working... * */ getPixelFromPixelScreen(px) { // Get transform matrix3D from CSS var fullTx = ol.matrix3D.inverse(this.getMatrix3D()); // Transform the point using full transform var pixel = ol.matrix3D.transformVertex(fullTx, px); // Perform the homogeneous divide to apply perspective to the points (divide x,y,z by the w component). pixel = ol.matrix3D.projectVertex(pixel); return [pixel[0], pixel[1]]; } } /* HACK: Overwrited Overlay function to handle overlay positing in a perspective map */ ;(function() { var _updatePixelPosition = ol.Overlay.prototype.updatePixelPosition; /** Update pixel projection in a perspective map (apply projection to the position) * @private */ ol.Overlay.prototype.updatePixelPosition = function () { var map = this.getMap(); if (map && map instanceof ol.PerspectiveMap) { var position = this.getPosition(); if (!map || !map.isRendered() || !position) { this.setVisible(false); return; } // Get pixel at screen var pixel = map.getPixelScreenFromCoordinate(position); var mapSize = map.getSize(); pixel[0] -= mapSize[0]/4 pixel[1] -= mapSize[1]/4 /* for ol v6.2.x // Offset according positioning var pos = this.getPositioning(); if (/bottom/.test(pos)) { pixel[1] += mapSize[1]/4 } else { pixel[1] -= mapSize[1]/4 } if (/right/.test(pos)) { pixel[0] += mapSize[0]/4 } else { pixel[0] -= mapSize[0]/4 } */ // Update this.updateRenderedPosition(pixel , mapSize); } else { _updatePixelPosition.call(this); } }; /**/ })(); /* Copyright (c) 2020 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Abstract base class; normally only used for creating subclasses. * An object with coordinates, draw and update * @constructor * @extends {ol.Object} * @param {*} options * @param {ol.Overlay} options.overlay * @param {ol.pixel} options.coordinate the position of the particule */ ol.particule.Base = class olparticuleBase extends ol.Object { constructor(options) { options = options || {}; super(options); this.setOverlay(options.overlay); this.coordinate = options.coordinate || [0, 0]; } /** Set the particule overlay * @param {ol.Overlay} overl */ setOverlay(overlay) { this._overlay = overlay; } /** Get the particule overlay * @return {ol.Overlay} */ getOverlay() { return this._overlay; } /** Draw the particule * @param { CanvasRenderingContext2D } ctx */ draw( /* ctx */) { } /** Update the particule * @param {number} dt timelapes since last call */ update( /* dt */) { } /** Update the particule * @param {number} dt timelapes since last call */ getRandomCoord(dt) { if (this.getOverlay().randomCoord) return this.getOverlay().randomCoord(); else return [dt, 0]; } } /* Copyright (c) 2020 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A cloud particule to display clouds over the map * @constructor * @extends {ol.particule.Base} * @param {*} options * @param {ol.Overlay} options.overlay * @param {ol.pixel} options.coordinate the position of the particule * @param {string} [options.src] bird image src */ ol.particule.Bird = class olparticuleBird extends ol.particule.Base { constructor(options) { options = options || {}; super(options); this.bird = new Image(); this.bird.addEventListener('load', function() { this.set('size', [this.bird.width || 50, this.bird.height || 50]); console.log(this.bird.width, this.bird.height) }.bind(this)) this.bird.src = options.src || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAABDCAQAAAD+S8VaAAAAAnNCSVQICFXsRgQAAAAJcEhZcwAAAvMAAALzAdLpCioAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAG90lEQVR42uWaaVRTRxTHJ4AUUIoIiqISOArIDiIhBBfccCMoR0vVUpXjTqun4Fr1tO5i3YuodaFqRMECKm4VUHEhUvWgBYuilgpiRVZpCARI3r8fWA4hYY9AXu77+ObNzO/O8u787xDSyeYSTzSICpu+DjV0ogrze84PBneByuJv3JpbBlx6MEBbJfG/8S2RAACFXXtU0gERN1BjCc9UEN/e7I2w1gFPinv3UDkHbFiGOqOwJVjlHMALRT3LLJ7trGIOuHwFUsY7q2IOuJ0u7YB//pswWFn6/vnUcbOCAn7ctfnUrsijl85dv5pw786fd9OTsvg5/JykN3fTb6ZcTDgVvefIkqXmVvKr0NN/IUQDO7C1qwJrOwyftIZ7cmIiN21eZlB+SOUtFKNl9kF0hb9ujmyVM73FMmWv3m+2J4zxw74NDN5/5vT1qzeT7j3n5/Bz7mcmPk24cy32Ai8i9Pj2nwIX+jo4kc8UMMqeXr5bfC6N/2tUHrdsCQ4gAR/QNhNRJ8+6GklXH7xStlxW+ViLxrpjqBswJ/z4rYyCFrQnwJPCxGe/x53i+fO+XOth2xpsvQm+PkfGP3YuYIo1oInTyIJiLDFtoZfUP+AXeaW2rZHXKZ8xJ35NeU+1odVSbIIBbEQeb70Tffd6ckmj0QbDy9/zOufdILE6SN0TBkVafnn0ka/NatrrditDXpmYKw36pREwPyr+Y0V72n0CsxoedTDFrMJJyRMDZJYIx8+yYICQKbDJtcjtL9IGAcEMKN7efIy+snnTYv/tR8Ry3+eWRUYFzavRB9SWL7icXKWAVrPRr96wEqjBTjg5bop03GGi77XF85FdqVZNIQ1konOsEvx35yOCN1xMFimszjNSDqh+ektGfVG3xjyTzaqkX3uDTiaCdh0ZA/qSgWXWWfb7CYMQQsiUUANK1j8hoJf1lSFUg0u+z1xCiFuMUYWsAy7QCj9ZzhIgIDCkpi4nhBCGsafNGx2peXCQRvhlcGrEAQSOhYQQQtyTG74YCglN8CswrVF8goEVhBBCrMzdozi33OOHJmvUvQqghQtKMEUu+GDB0Cj2Q/vsUdJn0JH8+oXG4rWS46djSD0ePcr2lUuafbZlIbN0UAnngpyA0I3FumeZxxQYVlZ/ooWleKm0+FHQbTDuWnAp5F6cbNfskcDtcg9J9aMGNUxDIiglgy+CPxhypj4Ddu/cfFpxOrIqrv7QAsH4V2nwYxoEvwQEOpRlAeeG07hWnopH7FMHgTr6VmhAA1xEQNjF4bMxQwpcj2I9duVZLiVtTb7YT7T2I30JccyqrrA7ZuESRF0SvhQ/QKfByDu/VZAs5O6rXS9U6onZ+A2CLgQvwWn0l5n4TAFnjOKksR5En6i73q6/q3IRhvwugB8LBylwi6IhixxX9Wd/CoWQwTrJTuaEOSwzENcKDR7Yj4xOg4+Hq3SEXzX8fIfcObAZPizV+bGxqLZhMyxBWgdP+xi4ScGbCNnhhrodqxnrso65pLidNxMQENihqoPgS3AY5rU7krh35eCPbon2c4hap2nnxob2GQQE+zpAM4qFb53EoUWxE3t93jXyBwyXcG1KD+8/IXwBAmFYg26Vx37oHjnIlnQlGzbJvMCX+lQrPgT6dat9yAcT/S6aSOIs2rjjxLaQ9SsX83gv8uShiNuAn4mR9fZ5dizpphRpREvj1YvOhiU84OdmoghFyKH47y/GHohtLf45ITvVuLyfyKLI5RlntyJSXx2+P+gaejt5O7FNCSEkcFHTuAmPom6/qqxJqFRee33wHGc6rVLjXtym8C8nTTcnDNMh/n5BfnN8mFY18jWdbPlceeBViEsPi16xxFSL7ncjukVelTvxUzsxjOlAUzsULv8/GfdEJa7G7D7YWLCcUzbNkfb42zaXNaG2h4XTHH/n9x+bjIHKqeAdNMZf55fbrKBYLNq+lqb433lkFrUk5hNKdu6mIf5XA1KetzibR+09TLcfonrMtVYlNKk9h2gV//FCW3tCFmMXT0nOe83bxpklbdDJqrD+BC1mwUzTtOw2Sl/UFjpsh8ci2pHirFgxV8nxV/oJxO2RwR6+HNFbmfkZ15PaqwQe/VmJ+R18Aql37XTAsQ9EefUBW6NeEk34IaWN8HkIQk+Jva0SzwGXP6p1XDeEoqB1qx/L0B3dKY+VSr0JDurDFNaK2ZoYg5142sx1m3LEYxUsq+Vv8ejVSv8bdJ/UXySds9eDB4JwEnFIRS6KUIi/8RJxCEEARte74GBR6DycFpGgtZNFPkHrHgOx61miSaPDEOtEn8qWwvepZMc5Mel3ItZmHbbM12wSXV/snMHZQ6eRlzEzI9d9rnftskwERhXVNxF7ik1Krd87pbLCbWYR9Y7v0f/htaJHbsoDhwAAAABJRU5ErkJggg=='; this.set('size', [this.bird.width || 50, this.bird.height || 50]); } /** Draw the particule * @param {CanvasRenderingContext2D } ctx */ draw(ctx) { //ctx.drawImage(this.bird, this.coordinate[0], this.coordinate[1]); var angle = this.getOverlay().get('angle'); ctx.save(); ctx.translate(this.coordinate[0], this.coordinate[1]); ctx.rotate(angle + Math.PI / 2); ctx.scale(.5, .5); ctx.drawImage(this.bird, -this.bird.width / 2, -this.bird.height / 2); ctx.restore(); } /** Update the particule * @param {number} dt timelapes since last call */ update(dt) { var speed = this.getOverlay().get('speed') * dt / this.getOverlay()._fps; var angle = this.getOverlay().get('angle'); this.coordinate[0] += speed * Math.cos(angle); this.coordinate[1] += speed * Math.sin(angle); } } /* Copyright (c) 2020 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A cloud particule to display clouds over the map * @constructor * @extends {ol.particule.Base} * @param {*} options * @param {ol.Overlay} options.overlay * @param {ol.pixel} options.coordinate the position of the particule */ ol.particule.Cloud = class olparticuleCloud extends ol.particule.Base { constructor(options) { options = options || {}; super(options); this.set('size', [100, 100]); var canvas = document.createElement('CANVAS'); canvas.width = 200; canvas.height = 200; var ctx = canvas.getContext('2d'); var grd = this.gradient = ctx.createRadialGradient(50, 50, 0, 50, 50, 50); grd.addColorStop(0, 'rgba(255,255,255,.2'); grd.addColorStop(1, 'rgba(255,255,255,0'); // Create cloud image this.image = canvas; for (var k = 0; k < 7; k++) { ctx.save(); var x = Math.random() * 100; var y = Math.random() * 100; ctx.translate(x, y); ctx.fillStyle = grd; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.restore(); } } /** Draw the particule * @param {CanvasRenderingContext2D } ctx */ draw(ctx) { ctx.save(); ctx.translate(this.coordinate[0], this.coordinate[1]); ctx.drawImage(this.image, -this.image.width / 2, -this.image.width / 2); ctx.restore(); } /** Update the particule * @param {number} dt timelapes since last call */ update(dt) { var speed = this.getOverlay().get('speed') * dt / this.getOverlay()._fps; var angle = this.getOverlay().get('angle'); this.coordinate[0] += speed * Math.cos(angle); this.coordinate[1] += speed * Math.sin(angle); } } /* Copyright (c) 2020 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Rain particules to display clouds over the map * @constructor * @extends {ol.particule.Base} * @param {*} options * @param {ol.Overlay} options.overlay * @param {ol.pixel} options.coordinate the position of the particule */ ol.particule.Rain = class olparticuleRain extends ol.particule.Base { constructor(options) { options = options || {}; super(options); this.z = Math.floor(Math.random() * 5) + 1; var canvas = document.createElement('CANVAS'); canvas.width = 50; canvas.height = 50; var ctx = canvas.getContext('2d'); this.gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 25); this.gradient.addColorStop(0, 'rgba(0,0,80,0)'); this.gradient.addColorStop(1, 'rgba(0,0,80,.3)'); } /** Draw the particule * @param {CanvasRenderingContext2D } ctx */ draw(ctx) { ctx.save(); var angle = this.getOverlay().get('angle'); ctx.beginPath(); var x1 = Math.cos(angle) * 10 * (1 + this.z / 2); var y1 = Math.sin(angle) * 10 * (1 + this.z / 2); ctx.lineWidth = Math.round(this.z / 2); ctx.strokeStyle = this.gradient; ctx.translate(this.coordinate[0], this.coordinate[1]); ctx.moveTo(0, 0); ctx.lineTo(x1, y1); ctx.stroke(); ctx.restore(); } /** Update the particule * @param {number} dt timelapes since last call */ update(dt) { var dl = this.getOverlay().get('speed') * dt / this.getOverlay()._fps * this.z; var angle = this.getOverlay().get('angle'); this.coordinate[0] += dl * Math.cos(angle); this.coordinate[1] += dl * Math.sin(angle); } } /* Copyright (c) 2020 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Raindrop particules to display clouds over the map * @constructor * @extends {ol.particule.Base} * @param {*} options * @param {ol.Overlay} options.overlay * @param {ol.pixel} options.coordinate the position of the particule */ ol.particule.RainDrop =class olparticuleRainDrop extends ol.particule.Base { constructor(options) { options = options || {}; super(options); this.size = 0; // Drops var canvas = document.createElement('CANVAS'); canvas.width = 100; canvas.height = 100; var ctx = canvas.getContext('2d'); var grd = ctx.createRadialGradient(50, 50, 0, 50, 50, 50); grd.addColorStop(0, 'rgba(128,128,192,.8'); grd.addColorStop(1, 'rgba(128,128,192,0'); this.image = canvas; ctx.fillStyle = grd; ctx.fillRect(0, 0, canvas.width, canvas.height); } /** Draw the particule * @param {CanvasRenderingContext2D } ctx */ draw(ctx) { if (this.size > 0) { ctx.save(); ctx.translate(this.coordinate[0], this.coordinate[1]); ctx.globalAlpha = this.size / 50; ctx.scale(1 - this.size / 50, 1 - this.size / 50); ctx.drawImage(this.image, -50, -50); ctx.restore(); } } /** Update the particule * @param {number} dt timelapes since last call */ update(dt) { if (this.size > 0 || Math.random() < .01) { if (this.size <= 0) { this.size = 50; this.coordinates = this.getRandomCoord(); } this.size = this.size - Math.round(dt / 20); } } } /* Copyright (c) 2020 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Rain particules to display clouds over the map * @constructor * @extends {ol.particule.Base} * @param {*} options * @param {ol.Overlay} options.overlay * @param {ol.pixel} options.coordinate the position of the particule */ ol.particule.Snow = class olparticuleSnow extends ol.particule.Base { constructor(options) { options = options || {}; super(options); this.z = (Math.floor(Math.random() * 5) + 1) / 5; this.angle = Math.random() * Math.PI; // Snow fakes var canvas = document.createElement('CANVAS'); canvas.width = 20; canvas.height = 20; var ctx = canvas.getContext('2d'); var grd = ctx.createRadialGradient(10, 10, 0, 10, 10, 10); grd.addColorStop(0, "rgba(255, 255, 255,1)"); // white grd.addColorStop(.8, "rgba(210, 236, 242,.8)"); // bluish grd.addColorStop(1, "rgba(237, 247, 249,0)"); // lighter bluish this.image = canvas; ctx.fillStyle = grd; ctx.fillRect(0, 0, canvas.width, canvas.height); } /** Draw the particule * @param {CanvasRenderingContext2D } ctx */ draw(ctx) { ctx.save(); ctx.translate(this.coordinate[0], this.coordinate[1]); ctx.globalAlpha = .4 + this.z / 2; ctx.scale(this.z, this.z); ctx.drawImage(this.image, -10, -10); ctx.restore(); } /** Update the particule * @param {number} dt timelapes since last call */ update(dt) { var speed = this.getOverlay().get('speed') * dt / this.getOverlay()._fps * this.z * 5; var angle = this.getOverlay().get('angle'); this.angle = this.angle + dt / this.getOverlay()._fps / 100; this.coordinate[0] += Math.sin(this.angle + this.z) * 2 + speed * Math.cos(angle); this.coordinate[1] += Math.cos(this.angle) + speed * Math.sin(angle); } } /* 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). */ /** * @classdesc * A popup element to be displayed over the map and attached to a single map * location. The popup are customized using CSS. * * @example var popup = new ol.Overlay.Popup(); map.addOverlay(popup); popup.show(coordinate, "Hello!"); popup.hide(); * * @constructor * @extends {ol.Overlay} * @fires show * @fires hide * @param {} options Extend Overlay options * @param {String} [options.popupClass] the a class of the overlay to style the popup. * @param {boolean} [options.anim Animate=false] the popup the popup, default false. * @param {bool} [options.closeBox=false] 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 var the popup choose its positioning to stay on the map. * @param {boolean} [options.minibar=false] add a mini vertical bar * @api stable */ ol.Overlay.Popup = class olOverlayPopup extends ol.Overlay { constructor(options) { options = options || {}; // Popup div var element = document.createElement("div"); //element.classList.add('ol-overlaycontainer-stopevent'); options.element = element; super(options); if (typeof (options.offsetBox) === 'number') { this.offsetBox = [options.offsetBox, options.offsetBox, options.offsetBox, options.offsetBox]; } else { this.offsetBox = options.offsetBox; } // Closebox this.closeBox = options.closeBox; this.onclose = options.onclose; this.onshow = options.onshow; ol.ext.element.create('BUTTON', { className: 'closeBox' + (options.closeBox ? ' hasclosebox' : ''), type: 'button', click: function () { this.hide(); }.bind(this), parent: element }); // Anchor div if (options.anchor !== false) { ol.ext.element.create('DIV', { className: 'anchor', parent: element }); } // Content this.content = ol.ext.element.create('DIV', { html: options.html || '', className: "ol-popup-content", parent: element }); if (options.minibar) { ol.ext.element.scrollDiv(this.content, { vertical: true, mousewheel: true, minibar: true }); } // Stop event if (options.stopEvent) { element.addEventListener("mousedown", function (e) { e.stopPropagation(); }); element.addEventListener("touchstart", function (e) { e.stopPropagation(); }); } this._elt = element; // call setPositioning first in constructor so getClassPositioning is called only once this.setPositioning(options.positioning || 'auto'); this.setPopupClass(options.popupClass || options.className || 'default'); if (options.anim) this.addPopupClass('anim'); // Show popup on timeout (for animation purposes) if (options.position) { setTimeout(function () { this.show(options.position); }.bind(this)); } } /** * Get CSS class of the popup according to its positioning. * @private */ getClassPositioning() { var c = ""; var pos = this.getPositioning(); if (/bottom/.test(pos)) c += "ol-popup-bottom "; if (/top/.test(pos)) c += "ol-popup-top "; if (/left/.test(pos)) c += "ol-popup-left "; if (/right/.test(pos)) c += "ol-popup-right "; if (/^center/.test(pos)) c += "ol-popup-middle "; if (/center$/.test(pos)) c += "ol-popup-center "; return c; } /** * Set a close box to the popup. * @param {bool} b * @api stable */ setClosebox(b) { this.closeBox = b; if (b) this.element.classList.add("hasclosebox"); else this.element.classList.remove("hasclosebox"); } /** * Set the CSS class of the popup. * @param {string} c class name. * @api stable */ setPopupClass(c) { var classes = ["ol-popup"]; if (this.getVisible()) classes.push('visible'); this.element.className = ""; var classesPositioning = this.getClassPositioning().split(' ') .filter(function (className) { return className.length > 0; }); if (c) { c.split(' ').filter(function (className) { return className.length > 0; }) .forEach(function (className) { classes.push(className); }); } else { classes.push("default"); } classesPositioning.forEach(function (className) { classes.push(className); }); if (this.closeBox) { classes.push("hasclosebox"); } this.element.classList.add.apply(this.element.classList, classes); } /** * Add a CSS class to the popup. * @param {string} c class name. * @api stable */ addPopupClass(c) { this.element.classList.add(c); } /** * Remove a CSS class to the popup. * @param {string} c class name. * @api stable */ removePopupClass(c) { this.element.classList.remove(c); } /** * Set positionning of the popup * @param {ol.OverlayPositioning | string | undefined} pos an ol.OverlayPositioning * or 'auto' to var the popup choose the best position * @api stable */ setPositioning(pos) { if (pos === undefined) return; if (/auto/.test(pos)) { this.autoPositioning = pos.split('-'); if (this.autoPositioning.length == 1) this.autoPositioning[1] = "auto"; } else { this.autoPositioning = false; } pos = pos.replace(/auto/g, "center"); if (pos == "center") pos = "bottom-center"; this.setPositioning_(pos); } /** @private * @param {ol.OverlayPositioning | string | undefined} pos */ setPositioning_(pos) { if (this.element) { super.setPositioning(pos); this.element.classList.remove("ol-popup-top", "ol-popup-bottom", "ol-popup-left", "ol-popup-right", "ol-popup-center", "ol-popup-middle"); var classes = this.getClassPositioning().split(' ') .filter(function (className) { return className.length > 0; }); this.element.classList.add.apply(this.element.classList, classes); } } /** Check if popup is visible * @return {boolean} */ getVisible() { return this.element.classList.contains("visible"); } /** * Set the position and the content of the popup. * @param {ol.Coordinate|string} coordinate the coordinate of the popup or the HTML content. * @param {string|undefined} html the HTML content (undefined = previous content). * @example var popup = new ol.Overlay.Popup(); // Show popup popup.show([166000, 5992000], "Hello world!"); // Move popup at coord with the same info popup.show([167000, 5990000]); // set new info popup.show("New informations"); * @api stable */ show(coordinate, html) { if (!html && typeof (coordinate) == 'string') { html = coordinate; coordinate = null; } if (coordinate === true) { coordinate = this.getPosition(); } var self = this; var map = this.getMap(); if (!map) return; if (html && html !== this.prevHTML) { // Prevent flickering effect this.prevHTML = html; this.content.innerHTML = ''; if (html instanceof Element) { this.content.appendChild(html); } else { ol.ext.element.create('DIV', { html: html, parent: this.content }); } // Refresh when loaded (img) Array.prototype.slice.call(this.content.querySelectorAll('img')) .forEach(function (image) { image.addEventListener('load', function () { try { map.renderSync(); } catch (e) { /* ok */ } self.content.dispatchEvent(new Event('scroll')); }); }); } if (coordinate) { // Auto positionning if (this.autoPositioning) { var p = map.getPixelFromCoordinate(coordinate); var s = map.getSize(); var pos = []; if (this.autoPositioning[0] == 'auto') { pos[0] = (p[1] < s[1] / 3) ? "top" : "bottom"; } else pos[0] = this.autoPositioning[0]; pos[1] = (p[0] < 2 * s[0] / 3) ? "left" : "right"; this.setPositioning_(pos[0] + "-" + pos[1]); if (this.offsetBox) { this.setOffset([this.offsetBox[pos[1] == "left" ? 2 : 0], this.offsetBox[pos[0] == "top" ? 3 : 1]]); } } else { if (this.offsetBox) { this.setOffset(this.offsetBox); } } // Show this.setPosition(coordinate); // Set visible class (wait to compute the size/position first) this.element.parentElement.style.display = ''; if (typeof (this.onshow) == 'function') this.onshow(); this.dispatchEvent({ type: 'show' }); this._tout = setTimeout(function () { self.element.classList.add('visible'); }, 0); } } /** * Hide the popup * @api stable */ hide() { if (this.getPosition() == undefined) return; if (typeof (this.onclose) == 'function') this.onclose(); this.setPosition(undefined); if (this._tout) clearTimeout(this._tout); this.element.classList.remove("visible"); this.dispatchEvent({ type: 'hide' }); } } /* Copyright (c) 2020 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** An overlay to play animations on top of the map * The overlay define a set of particules animated on top of the map. * Particules are objects with coordinates. * They are dawn in a canvas using the draw particule method. * The update particule method updates the particule position according to the timelapse * * @constructor * @extends {ol.Overlay} * @param {*} options * @param {String} options.className class of the Overlay * @param {number} option.density particule density, default .5 * @param {number} option.speed particule speed, default 4 * @param {number} option.angle particule angle in radian, default PI/4 * @param {boolean} options.animate start animation, default true * @param {number} options.fps frame per second, default 25 */ ol.Overlay.AnimatedCanvas = class olOverlayAnimatedCanvas extends ol.Overlay { constructor(options) { options = options || {}; var canvas = ol.ext.element.create('CANVAS', { className: ((options.className || '') + ' ol-animated-overlay').trim() }); super({ element: canvas, stopEvent: false }); this._canvas = canvas; this._ctx = this._canvas.getContext('2d'); this._listener = []; this._time = 0; this._particuleClass = options.particule || ol.particule.Base; if (options.createParticule) this._createParticule = options.createParticule; // 25fps this._fps = 1000 / (options.fps || 25); // Default particules properties var p = this._createParticule(); this._psize = p.get('size') || [50, 50]; this.set('density', options.density || .5); this.set('speed', options.speed || 4); this.set('angle', typeof (options.angle) === 'number' ? options.angle : Math.PI / 4); if (options.animate !== false) this.setAnimation(true); // Prevent animation when window on background document.addEventListener("visibilitychange", function () { this._pause = true; }.bind(this)); } /** Set the visibility * @param {boolean} b */ setVisible(b) { this.element.style.display = b ? 'block' : 'none'; if (b) this.setAnimation(this.get('animation')); } /** Get the visibility * @return {boolean} b */ getVisible() { return this.element.style.display != 'none'; } /** No update for this overlay */ updatePixelPosition() { } /** * Set the map instance the overlay is associated with * @param {ol.Map} map The map instance. */ setMap(map) { if (this.getMap()) { this.getMap().getViewport().querySelector('.ol-overlaycontainer').removeChild(this._canvas); } this._listener.forEach(function (l) { ol.Observable.unByKey(l); }); this._listener = []; super.setMap(map); if (map) { var size = map.getSize(); this._canvas.width = size[0]; this._canvas.height = size[1]; this.draw(); this._listener.push(map.on('change:size', function () { var size = map.getSize(); if (this._canvas.width !== size[0] || this._canvas.height !== size[1]) { this._canvas.width = size[0]; this._canvas.height = size[1]; this.draw(); } }.bind(this))); } } /** Create particules or return exiting ones */ getParticules() { var w = this._psize[0]; var h = this._psize[1]; var d = (this.get('density') * this._canvas.width * this._canvas.height / w / h) << 0; if (!this._particules) this._particules = []; if (d > this._particules.length) { for (var i = this._particules.length; i < d; i++) { this._particules.push(this._createParticule(this, this.randomCoord())); } } else { this._particules.length = d; } return this._particules; } /** Create a particule * @private */ _createParticule(overlay, coordinate) { return new this._particuleClass({ overlay: overlay, coordinate: coordinate }); } /** Get random coordinates on canvas */ randomCoord() { return [ Math.random() * (this._canvas.width + this._psize[0]) - this._psize[0] / 2, Math.random() * (this._canvas.height + this._psize[1]) - this._psize[1] / 2 ]; } /** Draw canvas overlay (draw each particules) * @param {number} dt timelapes since last call */ draw(dt) { var ctx = this._ctx; this.clear(); ctx.beginPath(); this.getParticules().forEach(function (p) { if (dt) { p.update(dt); this.testExit(p); } p.draw(this._ctx); }.bind(this)); } /** Test if particule exit the canvas and add it on other side * @param {*} p the point to test * @param {ol.size} size size of the overlap */ testExit(p) { var size = this._psize; if (p.coordinate[0] < -size[0]) { p.coordinate[0] = this._canvas.width + size[0]; p.coordinate[1] = Math.random() * (this._canvas.height + size[1]) - size[1] / 2; } else if (p.coordinate[0] > this._canvas.width + size[0]) { p.coordinate[0] = -size[0]; p.coordinate[1] = Math.random() * (this._canvas.height + size[1]) - size[1] / 2; } else if (p.coordinate[1] < -size[1]) { p.coordinate[0] = Math.random() * (this._canvas.width + size[0]) - size[0] / 2; p.coordinate[1] = this._canvas.height + size[1]; } else if (p.coordinate[1] > this._canvas.height + size[1]) { p.coordinate[0] = Math.random() * (this._canvas.width + size[0]) - size[0] / 2; p.coordinate[1] = -size[1]; } } /** Clear canvas */ clear() { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } /** Get overlay canvas * @return {CanvasElement} */ getCanvas() { return this._canvas; } /** Set canvas animation * @param {boolean} anim, default true * @api */ setAnimation(anim) { anim = (anim !== false); this.set('animation', anim); if (anim) { this._pause = true; requestAnimationFrame(this._animate.bind(this)); } else { this.dispatchEvent({ type: 'animation:stop', time: this._time }); } } /** * @private */ _animate(time) { if (this.getVisible() && this.get('animation')) { if (this._pause) { // reset time requestAnimationFrame(function (time) { this._time = time; requestAnimationFrame(this._animate.bind(this)); }.bind(this)); } else { // Test fps if (time - this._time > this._fps) { this.draw(time - this._time); this._time = time; } requestAnimationFrame(this._animate.bind(this)); } } this._pause = false; } } /* 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). */ /** * @classdesc * An overlay fixed on the map. * Use setPosition(coord, true) to force the overlay position otherwise the position will be ignored. * * @example var popup = new ol.Overlay.Fixed(); map.addOverlay(popup); popup.setposition(position, true); * * @constructor * @extends {ol.Overlay} * @param {} options Extend Overlay options * @api stable */ ol.Overlay.Fixed = class olOverlayFixed extends ol.Overlay { constructor(options) { super(options); } /** Prevent modifying position and use a force argument to force positionning. * @param {ol.coordinate} position * @param {boolean} force true to change the position, default false */ setPosition(position, force) { if (this.getMap() && position) { this._pixel = this.getMap().getPixelFromCoordinate(position); } super.setPosition(position); if (force) { super.updatePixelPosition(); } } /** Update position according the pixel position */ updatePixelPosition() { if (this.getMap() && this._pixel && this.getPosition()) { var pixel = this.getMap().getPixelFromCoordinate(this.getPosition()); if (Math.round(pixel[0] * 1000) !== Math.round(this._pixel[0] * 1000) || Math.round(pixel[0] * 1000) !== Math.round(this._pixel[0] * 1000)) { this.setPosition(this.getMap().getCoordinateFromPixel(this._pixel)); } } } } /* 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). */ /** * 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 var the popup choose its positioning to stay on the map. * @api stable */ 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) // Canvas for drawing inks var canvas = document.createElement('canvas') 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().width() 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 (var i in pointerEvents) { if (pointerEvents2[i]) { pointerEvents[i] = pointerEvents2[i] } } pointerEvents[e.pointerId] = e pixelPosition = this._pixel 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 } if (!this._pixel) { this._pixel = map.getPixelFromCoordinate(position) var mapSize = map.getSize() this.updateRenderedPosition(this._pixel, mapSize) } else { this.setVisible(true) } } /** 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 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.updateRenderedPosition(this._pixel, mapSize) this._overlay.changed() } } /** Set pixel position * @returns {ol.pixel} */ getPixelPosition() { return this._pixel } /** * 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 } } /* 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). */ /** * @classdesc * The Magnify overlay add a "magnifying glass" effect to an OL3 map that displays * a portion of the map in a different zoom (and actually display different content). * * @constructor * @extends {ol.Overlay} * @param {olx.OverlayOptions} options Overlay options * @api stable */ ol.Overlay.Magnify = class olOverlayMagnify extends ol.Overlay { constructor(options) { var elt = document.createElement("div") elt.className = "ol-magnify" super({ positioning: options.positioning || "center-center", element: elt, stopEvent: false }) this._elt = elt // Create magnify map this.mgmap_ = new ol.Map({ controls: new ol.Collection(), interactions: new ol.Collection(), target: options.target || this._elt, view: new ol.View({ projection: options.projection }), layers: options.layers }) this.mgview_ = this.mgmap_.getView() this.external_ = options.target ? true : false this.set("zoomOffset", options.zoomOffset || 1) this.set("active", true) this.on("propertychange", this.setView_.bind(this)) } /** * Set the map instance the overlay is associated with. * @param {ol.Map} map The map instance. */ setMap(map) { if (this.getMap()) { this.getMap().getViewport().removeEventListener("mousemove", this.onMouseMove_) } if (this._listener) ol.Observable.unByKey(this._listener) this._listener = null super.setMap(map) map.getViewport().addEventListener("mousemove", this.onMouseMove_.bind(this)) this._listener = map.getView().on('propertychange', this.setView_.bind(this)) this.setView_() } /** Get the magnifier map * @return {_ol_Map_} */ getMagMap() { return this.mgmap_ } /** Magnify is active * @return {boolean} */ getActive() { return this.get("active") } /** Activate or deactivate * @param {boolean} active */ setActive(active) { return this.set("active", active) } /** Mouse move * @private */ onMouseMove_(e) { if (!this.get("active")) { this.setPosition() } else { var px = this.getMap().getEventCoordinate(e) if (!this.external_) this.setPosition(px) this.mgview_.setCenter(px) if (!this._elt.querySelector('canvas') || this._elt.querySelector('canvas').style.display == "none"){ this.mgmap_.updateSize() } } } /** View has changed * @private */ setView_(e) { if (!this.get("active")) { this.setPosition() return } if (!e) { // refresh all this.setView_({ key: 'rotation' }) this.setView_({ key: 'resolution' }) return } // Set the view params switch (e.key) { case 'rotation': this.mgview_.setRotation(this.getMap().getView().getRotation()) break case 'zoomOffset': case 'resolution': { var z = Math.max(0, this.getMap().getView().getZoom() + Number(this.get("zoomOffset"))) this.mgview_.setZoom(z) break } default: break } } } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * @classdesc * A placemark element to be displayed over the map and attached to a single map * location. The placemarks are customized using CSS. * * @example var popup = new ol.Overlay.Placemark(); map.addOverlay(popup); popup.show(coordinate); popup.hide(); * * @constructor * @extends {ol.Overlay} * @param {} options Extend ol/Overlay/Popup options * @param {String} options.color placemark color * @param {String} options.backgroundColor placemark color * @param {String} options.contentColor placemark color * @param {Number} options.radius placemark radius in pixel * @param {String} options.popupClass the a class of the overlay to style the popup. * @param {function|undefined} options.onclose: callback function when popup is closed * @param {function|undefined} options.onshow callback function when popup is shown * @api stable */ ol.Overlay.Placemark = class olOverlayPlacemark extends ol.Overlay.Popup { constructor(options) { options = options || {}; options.popupClass = (options.popupClass || '') + ' placemark anim'; options.positioning = 'bottom-center', super(options); this.setPositioning = function () { }; if (options.color) this.element.style.color = options.color; if (options.backgroundColor) this.element.style.backgroundColor = options.backgroundColor; if (options.contentColor) this.setContentColor(options.contentColor); if (options.size) this.setRadius(options.size); } /** * Set the position and the content of the placemark (hide it before to enable animation). * @param {ol.Coordinate|string} coordinate the coordinate of the popup or the HTML content. * @param {string|undefined} html the HTML content (undefined = previous content). */ show(coordinate, html) { if (coordinate === true) { coordinate = this.getPosition(); } this.hide(); super.show(coordinate, html); } /** * Set the placemark color. * @param {string} color */ setColor(color) { this.element.style.color = color; } /** * Set the placemark background color. * @param {string} color */ setBackgroundColor(color) { this._elt.style.backgroundColor = color; } /** * Set the placemark content color. * @param {string} color */ setContentColor(color) { var c = this.element.getElementsByClassName('ol-popup-content')[0]; if (c) c.style.color = color; } /** * Set the placemark class. * @param {string} name */ setClassName(name) { var oldclass = this.element.className; this.element.className = 'ol-popup placemark ol-popup-bottom ol-popup-center ' + (/visible/.test(oldclass) ? 'visible ' : '') + (/anim/.test(oldclass) ? 'anim ' : '') + name; } /** * Set the placemark radius. * @param {number} size size in pixel */ setRadius(size) { this.element.style.fontSize = size + 'px'; } } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Template attributes for popup * @typedef {Object} TemplateAttributes * @property {string} title * @property {function} format a function that takes an attribute and a feature and returns the formated attribute * @property {string} before string to instert before the attribute (prefix) * @property {string} after string to instert after the attribute (sudfix) * @property {boolean|function} visible boolean or a function (feature, value) that decides the visibility of a attribute entry */ /** Template * @typedef {Object} Template * @property {string|function} title title of the popup, attribute name or a function that takes a feature and returns the title * @property {Object.} attributes a list of template attributes */ /** * A popup element to be displayed on a feature. * * @constructor * @extends {ol.Overlay.Popup} * @fires show * @fires hide * @fires select * @param {} options Extend Popup options * @param {String} options.popupClass the a class of the overlay to style the popup. * @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.positionning * the 'auto' positioning var the popup choose its positioning to stay on the map. * @param {Template|function} [options.template] A template with a list of properties to use in the popup or a function that takes a feature and returns a Template, default use all feature properties * @param {ol.interaction.Select} options.select a select interaction to get features from * @param {boolean} options.keepSelection keep original selection, otherwise set selection to the current popup feature and add a counter to change current feature, default false * @param {boolean} options.canFix Enable popup to be fixed, default false * @param {boolean} options.showImage display image url as image, default false * @param {boolean} options.maxChar max char to display in a cell, default 200 * @api stable */ ol.Overlay.PopupFeature = class olOverlayPopupFeature extends ol.Overlay.Popup { constructor(options) { options = options || {}; super(options); this.setTemplate(options.template); this.set('canFix', options.canFix); this.set('showImage', options.showImage); this.set('maxChar', options.maxChar || 200); this.set('keepSelection', options.keepSelection); // Bind with a select interaction if (options.select && (typeof options.select.on === 'function')) { this._select = options.select; options.select.on('select', function (e) { if (!this._noselect) { if (e.selected[0]) { this.show(e.mapBrowserEvent.coordinate, options.select.getFeatures().getArray(), e.selected[0]); } else { this.hide(); } } }.bind(this)); } } /** Set the template * @param {Template} [template] A template with a list of properties to use in the popup, default use all features properties */ setTemplate(template) { if (!template) { template = function (f) { var prop = f.getProperties(); delete prop[f.getGeometryName()]; return { attributes: Object.keys(prop) }; }; } this._template = template; this._attributeObject(this._template); } /** * @private */ _attributeObject(temp) { if (temp && temp.attributes instanceof Array) { var att = {}; temp.attributes.forEach(function (a) { att[a] = true; }); temp.attributes = att; } return temp.attributes; } /** Show the popup on the map * @param {ol.coordinate|undefined} coordinate Position of the popup * @param {ol.Feature|Array} features The features on the popup * @param {ol.Feature} current The current feature if keepSelection = true, otherwise get the first feature */ show(coordinate, features, current) { if (coordinate instanceof ol.Feature || (coordinate instanceof Array && coordinate[0] instanceof ol.Feature)) { features = coordinate; coordinate = null; } if (!(features instanceof Array)) features = [features]; this._features = features.slice(); if (!this._count) this._count = 1; // Calculate html upon feaures attributes this._count = 1; var f = this.get('keepSelection') ? current || features[0] : features[0]; var html = this._getHtml(f); if (html) { if (!this.element.classList.contains('ol-fixed')) this.hide(); if (!coordinate || features[0].getGeometry().getType() === 'Point') { coordinate = features[0].getGeometry().getFirstCoordinate(); } super.show(coordinate, html); } else { this.hide(); } } /** * @private */ _getHtml(feature) { if (!feature) return ''; var html = ol.ext.element.create('DIV', { className: 'ol-popupfeature' }); if (this.get('canFix')) { ol.ext.element.create('I', { className: 'ol-fix', parent: html }) .addEventListener('click', function () { this.element.classList.toggle('ol-fixed'); }.bind(this)); } var template = this._template; // calculate template if (typeof (template) === 'function') { template = template(feature, this._count, this._features.length); } else if (!template || !template.attributes) { template = template || {}; template.attributes = {}; for (var i in feature.getProperties()) if (i != 'geometry') { template.attributes[i] = i; } } // Display title if (template.title) { var title; if (typeof template.title === 'function') { title = template.title(feature); } else { title = feature.get(template.title); } ol.ext.element.create('H1', { html: title, parent: html }); } // Display properties in a table if (template.attributes) { var tr, table = ol.ext.element.create('TABLE', { parent: html }); var atts = this._attributeObject(template); var featureAtts = feature.getProperties(); for (var att in atts) { if (featureAtts.hasOwnProperty(att)) { var a = atts[att]; var content, val = featureAtts[att]; // Get calculated value if (a && typeof (a.format) === 'function') { val = a.format(val, feature); } // Is entry visible? var visible = true; if (a && typeof (a.visible) === 'boolean') { visible = a.visible; } else if (a && typeof (a.visible) === 'function') { visible = a.visible(feature, val); } if (visible) { tr = ol.ext.element.create('TR', { parent: table }); ol.ext.element.create('TD', { html: a ? a.title || att : att, parent: tr }); // Show image or content if (this.get('showImage') && /(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png)/.test(val)) { content = ol.ext.element.create('IMG', { src: val }); } else { if (a) { content = (a.before || '') + val + (a.after || ''); } else { content = ''; } var maxc = this.get('maxChar') || 200; if (typeof (content) === 'string' && content.length > maxc) { content = content.substr(0, maxc) + '[...]'; } } // Add value ol.ext.element.create('TD', { html: content, parent: tr }); } } } } // Zoom button ol.ext.element.create('BUTTON', { className: 'ol-zoombt', parent: html }) .addEventListener('click', function () { if (feature.getGeometry().getType() === 'Point') { this.getMap().getView().animate({ center: feature.getGeometry().getFirstCoordinate(), zoom: Math.max(this.getMap().getView().getZoom(), 18) }); } else { var ext = feature.getGeometry().getExtent(); this.getMap().getView().fit(ext, { duration: 1000 }); } }.bind(this)); // Counter if (!this.get('keepSelection') && this._features.length > 1) { var div = ol.ext.element.create('DIV', { className: 'ol-count', parent: html }); ol.ext.element.create('DIV', { className: 'ol-prev', parent: div, click: function () { this._count--; if (this._count < 1) this._count = this._features.length; html = this._getHtml(this._features[this._count - 1]); setTimeout(function () { ol.Overlay.Popup.prototype.show.call(this, this.getPosition(), html); }.bind(this), 350); }.bind(this) }); ol.ext.element.create('TEXT', { html: this._count + '/' + this._features.length, parent: div }); ol.ext.element.create('DIV', { className: 'ol-next', parent: div, click: function () { this._count++; if (this._count > this._features.length) this._count = 1; html = this._getHtml(this._features[this._count - 1]); setTimeout(function () { ol.Overlay.Popup.prototype.show.call(this, this.getPosition(), html); }.bind(this), 350); }.bind(this) }); } // Use select interaction if (this._select && !this.get('keepSelection')) { this._noselect = true; this._select.getFeatures().clear(); this._select.getFeatures().push(feature); this._noselect = false; } this.dispatchEvent({ type: 'select', feature: feature, index: this._count }); return html; } /** Fix the popup * @param {boolean} fix */ setFix(fix) { if (fix) this.element.classList.add('ol-fixed'); else this.element.classList.remove('ol-fixed'); } /** Is a popup fixed * @return {boolean} */ getFix() { return this.element.classList.contains('ol-fixed'); } } /** Get a function to use as format to get local string for an attribute * if the attribute is a number: Number.toLocaleString() * if the attribute is a date: Date.toLocaleString() * otherwise the attibute itself * @param {string} locales string with a BCP 47 language tag, or an array of such strings * @param {*} options Number or Date toLocaleString options * @return {function} a function that takes an attribute and return the formated attribute */ ol.Overlay.PopupFeature.localString = function (locales , options) { return function (a) { if (a && a.toLocaleString) { return a.toLocaleString(locales , options); } else { // Try to get a date from a string var date = new Date(a); if (isNaN(date)) return a; else return date.toLocaleString(locales , options); } }; }; /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** A tooltip element to be displayed over the map and attached on the cursor position. * @constructor * @extends {ol.Overlay.Popup} * @param {} options Extend Popup options * @param {String} options.popupClass the a class of the overlay to style the popup. * @param {number} options.maximumFractionDigits maximum digits to display on measure, default 2 * @param {function} options.formatLength a function that takes a number and returns the formated value, default length in meter * @param {function} options.formatArea a function that takes a number and returns the formated value, default length in square-meter * @param {function} options.getHTML a function that takes a feature and the info string and return a formated info to display in the tooltip, default display feature measure & info * @param {Number|Array} options.offsetBox an offset box * @param {ol.OverlayPositioning | string | undefined} options.positionning * the 'auto' positioning var the popup choose its positioning to stay on the map. * @api stable */ ol.Overlay.Tooltip = class olOverlayTooltip extends ol.Overlay.Popup { constructor(options) { options = options || {}; options.popupClass = options.popupClass || options.className || 'tooltips black'; options.positioning = options.positioning || 'center-left'; options.stopEvent = !!(options.stopEvent); super(options); this.set('maximumFractionDigits', options.maximumFractionDigits || 2); if (typeof (options.formatLength) === 'function') this.formatLength = options.formatLength; if (typeof (options.formatArea) === 'function') this.formatArea = options.formatArea; if (typeof (options.getHTML) === 'function') this.getHTML = options.getHTML; this._interaction = new ol.interaction.Interaction({ handleEvent: function (e) { if (e.type === 'pointermove' || e.type === 'click') { var info = this.getHTML(this._feature, this.get('info')); if (info) { this.show(e.coordinate, info); } else this.hide(); this._coord = e.coordinate; } return true; }.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) { if (this.getMap()) this.getMap().removeInteraction(this._interaction); super.setMap(map); if (this.getMap()) this.getMap().addInteraction(this._interaction); } /** Get the information to show in the tooltip * The area/length will be added if a feature is attached. * @param {ol.Feature|undefined} feature the feature * @param {string} info the info string * @api */ getHTML(feature, info) { if (this.get('measure')) return this.get('measure') + (info ? '
              ' + info : ''); else return info || ''; } /** Set the Tooltip info * If information is not null it will be set with a delay, * thus watever the information is inserted, the significant information will be set. * ie. ttip.setInformation('ok'); ttip.setInformation(null); will set 'ok' * ttip.set('info','ok'); ttip.set('info', null); will set null * @param {string} what The information to display in the tooltip, default remove information */ setInfo(what) { if (!what) { this.set('info', ''); this.hide(); } else setTimeout(function () { this.set('info', what); this.show(this._coord, this.get('info')); }.bind(this)); } /** Remove the current featue attached to the tip * Similar to setFeature() with no argument */ removeFeature() { this.setFeature(); } /** Format area to display in the popup. * Can be overwritten to display measure in a different unit (default: square-metter). * @param {number} area area in m2 * @return {string} the formated area * @api */ formatArea(area) { if (area > Math.pow(10, -1 * this.get('maximumFractionDigits'))) { if (area > 10000) { return (area / 1000000).toLocaleString(undefined, { maximumFractionDigits: this.get('maximumFractionDigits)') }) + ' km²'; } else { return area.toLocaleString(undefined, { maximumFractionDigits: this.get('maximumFractionDigits') }) + ' m²'; } } else { return ''; } } /** Format area to display in the popup * Can be overwritten to display measure in different unit (default: meter). * @param {number} length length in m * @return {string} the formated length * @api */ formatLength(length) { if (length > Math.pow(10, -1 * this.get('maximumFractionDigits'))) { if (length > 100) { return (length / 1000).toLocaleString(undefined, { maximumFractionDigits: this.get('maximumFractionDigits') }) + ' km'; } else { return length.toLocaleString(undefined, { maximumFractionDigits: this.get('maximumFractionDigits') }) + ' m'; } } else { return ''; } } /** Set a feature associated with the tooltips, measure info on the feature will be added in the tooltip * @param {ol.Feature|ol.Event} feature an ol.Feature or an event (object) with a feature property */ setFeature(feature) { // Handle event with a feature as property. if (feature && feature.feature) feature = feature.feature; // The feature this._feature = feature; if (this._listener) { this._listener.forEach(function (l) { ol.Observable.unByKey(l); }); } this._listener = []; this.set('measure', ''); if (feature) { this._listener.push(feature.getGeometry().on('change', function (e) { var geom = e.target; var measure; if (geom.getArea) { measure = this.formatArea(ol.sphere.getArea(geom, { projection: this.getMap().getView().getProjection() })); } else if (geom.getLength) { measure = this.formatLength(ol.sphere.getLength(geom, { projection: this.getMap().getView().getProjection() })); } this.set('measure', measure); }.bind(this))); } } } /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (http://www.cecill.info/). ol.coordinate.convexHull compute a convex hull using Andrew's Monotone Chain Algorithm. @see https://en.wikipedia.org/wiki/Convex_hull_algorithms */ ol.coordinate.convexHull; (function(){ /** Tests if a point is left or right of line (a,b). * @param {ol.coordinate} a point on the line * @param {ol.coordinate} b point on the line * @param {ol.coordinate} o * @return {bool} true if (a,b,o) turns clockwise */ var clockwise = function (a, b, o) { return ((a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) <= 0); }; /** Compute a convex hull using Andrew's Monotone Chain Algorithm * @param {Array} points an array of 2D points * @return {Array} the convex hull vertices */ ol.coordinate.convexHull = function (points) { // Sort by increasing x and then y coordinate var i; points.sort(function(a, b) { return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]; }); // Compute the lower hull var lower = []; for (i = 0; i < points.length; i++) { while (lower.length >= 2 && clockwise(lower[lower.length - 2], lower[lower.length - 1], points[i])) { lower.pop(); } lower.push(points[i]); } // Compute the upper hull var upper = []; for (i = points.length - 1; i >= 0; i--) { while (upper.length >= 2 && clockwise(upper[upper.length - 2], upper[upper.length - 1], points[i])) { upper.pop(); } upper.push(points[i]); } upper.pop(); lower.pop(); return lower.concat(upper); }; /* Get coordinates of a geometry */ var getCoordinates = function (geom) { var i, p var h = []; switch (geom.getType()) { case "Point":h.push(geom.getCoordinates()); break; case "LineString": case "LinearRing": case "MultiPoint":h = geom.getCoordinates(); break; case "MultiLineString": p = geom.getLineStrings(); for (i = 0; i < p.length; i++) h.concat(getCoordinates(p[i])); break; case "Polygon": h = getCoordinates(geom.getLinearRing(0)); break; case "MultiPolygon": p = geom.getPolygons(); for (i = 0; i < p.length; i++) h.concat(getCoordinates(p[i])); break; case "GeometryCollection": p = geom.getGeometries(); for (i = 0; i < p.length; i++) h.concat(getCoordinates(p[i])); break; default:break; } return h; }; /** Compute a convex hull on a geometry using Andrew's Monotone Chain Algorithm * @return {Array} the convex hull vertices */ ol.geom.Geometry.prototype.convexHull = function() { return ol.coordinate.convexHull(getCoordinates(this)); }; })(); /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (http://www.cecill.info/). */ /** Convert coordinate to French DFCI grid * @param {ol/coordinate} coord * @param {number} level [0-3] * @param {ol/proj/Projection} projection of the coord, default EPSG:27572 * @return {String} the DFCI index */ ol.coordinate.toDFCI = function (coord, level, projection) { if (!level && level !==0) level = 3; if (projection) { if (!ol.proj.get('EPSG:27572')) { // Add Lambert IIe proj if (!proj4.defs["EPSG:27572"]) proj4.defs("EPSG:27572","+proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +a=6378249.2 +b=6356515 +towgs84=-168,-60,320,0,0,0,0 +pm=paris +units=m +no_defs"); ol.proj.proj4.register(proj4); } coord = ol.proj.transform(coord, projection, 'EPSG:27572'); } var x = coord[0]; var y = coord[1]; var s = ''; // Level 0 var step = 100000; s += String.fromCharCode(65 + Math.floor((x<800000?x:x+200000)/step)) + String.fromCharCode(65 + Math.floor((y<2300000?y:y+200000)/step) - 1500000/step); if (level === 0) return s; // Level 1 var step1 = 100000/5; s += 2*Math.floor((x%step)/step1); s += 2*Math.floor((y%step)/step1); if (level === 1) return s; // Level 2 var step2 = step1 / 10; var x0 = Math.floor((x%step1)/step2); s += String.fromCharCode(65 + (x0<8 ? x0 : x0+2)); s += Math.floor((y%step1)/step2); if (level === 2) return s; // Level 3 var x3 = Math.floor((x%step2)/500); var y3 = Math.floor((y%step2)/500); if (x3<1) { if (y3>1) s += '.1'; else s += '.4'; } else if (x3>2) { if (y3>1) s += '.2'; else s += '.3'; } else if (y3>2) { if (x3<2) s += '.1'; else s += '.2'; } else if (y3<1) { if (x3<2) s += '.4'; else s += '.3'; } else { s += '.5'; } return s; }; /** Get coordinate from French DFCI index * @param {String} index the DFCI index * @param {ol/proj/Projection} projection result projection, default EPSG:27572 * @return {ol/coordinate} coord */ ol.coordinate.fromDFCI = function (index, projection) { var coord; // Level 0 var step = 100000; var x = index.charCodeAt(0) - 65; x = (x<8 ? x : x-2)*step; var y = index.charCodeAt(1) - 65; y = (y<8 ? y : y-2)*step + 1500000; if (index.length===2) { coord = [x+step/2, y+step/2]; } else { // Level 1 step /= 5; x += Number(index.charAt(2))/2*step; y += Number(index.charAt(3))/2*step; if (index.length===4) { coord = [x+step/2, y+step/2]; } else { // Level 2 step /= 10; var x0 = index.charCodeAt(4) - 65; x += (x0<8 ? x0 : x0-2)*step; y += Number(index.charAt(5))*step; if (index.length === 6) { coord = [x+step/2, y+step/2]; } else { // Level 3 switch (index.charAt(7)) { case '1': coord = [x+step/4, y+3*step/4]; break; case '2': coord = [x+3*step/4, y+3*step/4]; break; case '3': coord = [x+3*step/4, y+step/4]; break; case '4': coord = [x+step/4, y+step/4]; break; default: coord = [x+step/2, y+step/2]; break; } } } } // Convert ? if (projection) { if (!ol.proj.get('EPSG:27572')) { // Add Lambert IIe proj if (!proj4.defs["EPSG:27572"]) proj4.defs("EPSG:27572","+proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +a=6378249.2 +b=6356515 +towgs84=-168,-60,320,0,0,0,0 +pm=paris +units=m +no_defs"); ol.proj.proj4.register(proj4); } coord = ol.proj.transform(coord, 'EPSG:27572', projection); } return coord; }; /** The string is a valid DFCI index * @param {string} index DFCI index * @return {boolean} */ ol.coordinate.validDFCI = function (index) { if (index.length<2 || index.length>8) return false; if (/[^A-H|^K-N]/.test(index.substr(0,1))) return false; if (/[^B-H|^K-N]/.test(index.substr(1,1))) return false; if (index.length>2) { if (index.length<4) return false; if (/[^0,^2,^4,^6,^8]/.test(index.substr(2,1))) return false; if (/[^0,^2,^4,^6,^8]/.test(index.substr(3,1))) return false; } if (index.length>4) { if (index.length<6) return false; if (/[^A-H|^K-L]/.test(index.substr(4,1))) return false; if (/[^0-9]/.test(index.substr(5,1))) return false; } if (index.length>6) { if (index.length<8) return false; if (index.substr(6,1)!=='.') return false; if (/[^1-5]/.test(index.substr(7,1))) return false; } return true; } /** Coordinate is valid for DFCI * @param {ol/coordinate} coord * @param {ol/proj/Projection} projection result projection, default EPSG:27572 * @return {boolean} */ ol.coordinate.validDFCICoord = function (coord, projection) { if (projection) { if (!ol.proj.get('EPSG:27572')) { // Add Lambert IIe proj if (!proj4.defs["EPSG:27572"]) proj4.defs("EPSG:27572","+proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +a=6378249.2 +b=6356515 +towgs84=-168,-60,320,0,0,0,0 +pm=paris +units=m +no_defs"); ol.proj.proj4.register(proj4); } coord = ol.proj.transform(coord, projection, 'EPSG:27572'); } // Test extent if (0 > coord[0] || coord[0] > 1200000 ) return false; if (1600000 > coord[1] || coord[1] > 2700000 ) return false; return true; }; /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (http://www.cecill.info/). */ /* Define namespace */ ol.graph = {}; /** * @classdesc * Compute the shortest paths between nodes in a graph source * The source must only contains LinesString. * * It uses a A* optimisation. * You can overwrite methods to customize the result. * @see https://en.wikipedia.org/wiki/Dijkstras_algorithm * @constructor * @fires calculating * @fires start * @fires finish * @fires pause * @param {any} options * @param {ol/source/Vector} options.source the source for the edges * @param {integer} [options.maxIteration=20000] maximum iterations before a pause event is fired, default 20000 * @param {integer} [options.stepIteration=2000] number of iterations before a calculating event is fired, default 2000 * @param {number} [options.epsilon=1E-6] geometric precision (min distance beetween 2 points), default 1E-6 */ ol.graph.Dijkstra = class olgraphDijskra extends ol.Object { constructor(options) { options = options || {}; super(); this.source = options.source; this.nodes = new ol.source.Vector(); // Maximum iterations this.maxIteration = options.maxIteration || 20000; this.stepIteration = options.stepIteration || 2000; // A* optimisation this.astar = true; this.candidat = []; this.set('epsilon', options.epsilon || 1E-6); } /** Get the weighting of the edge, for example a speed factor * The function returns a value beetween ]0,1] * - 1 = no weighting * - 0.5 = goes twice more faster on this road * * If no feature is provided you must return the lower weighting you're using * @param {ol/Feature} feature * @return {number} a number beetween 0-1 * @api */ weight( /* feature */) { return 1; } /** Get the edge direction * - 0 : the road is blocked * - 1 : direct way * - -1 : revers way * - 2 : both way * @param {ol/Feature} feature * @return {Number} 0: blocked, 1: direct way, -1: revers way, 2:both way * @api */ direction( /* feature */) { return 2; } /** Calculate the length of an edge * @param {ol/Feature|ol/geom/LineString} geom * @return {number} * @api */ getLength(geom) { if (geom.getGeometry) geom = geom.getGeometry(); return geom.getLength(); } /** Get the nodes source concerned in the calculation * @return {ol/source/Vector} */ getNodeSource() { return this.nodes; } /** Get all features at a coordinate * @param {ol/coordinate} coord * @return {Array
                } */ getEdges(coord) { var extent = ol.extent.buffer(ol.extent.boundingExtent([coord]), this.get('epsilon')); var result = []; this.source.forEachFeatureIntersectingExtent(extent, function (f) { result.push(f); }); return result; } /** Get a node at a coordinate * @param {ol/coordinate} coord * @return {ol/Feature} the node */ getNode(coord) { var extent = ol.extent.buffer(ol.extent.boundingExtent([coord]), this.get('epsilon')); var result = []; this.nodes.forEachFeatureIntersectingExtent(extent, function (f) { result.push(f); }); return result[0]; } /** Add a node * @param {ol/coorindate} p * @param {number} wdist the distance to reach this node * @param {ol/Feature} from the feature used to come to this node * @param {ol/Feature} prev the previous node * @return {ol/Feature} the node * @private */ addNode(p, wdist, dist, from, prev) { // Final condition if (this.wdist && wdist > this.wdist) return false; // Look for existing point var node = this.getNode(p); // Optimisation ? var dtotal = wdist + this.getLength(new ol.geom.LineString([this.end, p])) * this.weight(); if (this.astar && this.wdist && dtotal > this.wdist) return false; if (node) { // Allready there if (node !== this.arrival && node.get('wdist') <= wdist) return node; // New candidat node.set('dist', dist); node.set('wdist', wdist); node.set('dtotal', dtotal); node.set('from', from); node.set('prev', prev); if (node === this.arrival) { this.wdist = wdist; } this.candidat.push(node); } else { // New candidat node = new ol.Feature({ geometry: new ol.geom.Point(p), from: from, prev: prev, dist: dist || 0, wdist: wdist, dtotal: dtotal, }); if (wdist < 0) { node.set('wdist', false); } else this.candidat.push(node); // Add it in the node source this.nodes.addFeature(node); } return node; } /** Get the closest coordinate of a node in the graph source (an edge extremity) * @param {ol/coordinate} p * @return {ol/coordinate} * @private */ closestCoordinate(p) { var e = this.source.getClosestFeatureToCoordinate(p); var p0 = e.getGeometry().getFirstCoordinate(); var p1 = e.getGeometry().getLastCoordinate(); if (ol.coordinate.dist2d(p, p0) < ol.coordinate.dist2d(p, p1)) return p0; else return p1; } /** Calculate a path beetween 2 points * @param {ol/coordinate} start * @param {ol/coordinate} end * @return {boolean|Array
                  } false if don't start (still running) or start and end nodes */ path(start, end) { if (this.running) return false; // Starting nodes start = this.closestCoordinate(start); this.end = this.closestCoordinate(end); if (start[0] === this.end[0] && start[1] === this.end[1]) { this.dispatchEvent({ type: 'finish', route: [], wDistance: -1, distance: this.wdist }); return false; } // Initialize var self = this; this.nodes.clear(); this.candidat = []; this.wdist = 0; this.running = true; // Starting point this.addNode(start, 0); // Arrival this.arrival = this.addNode(this.end, -1); // Start this.nb = 0; this.dispatchEvent({ type: 'start' }); setTimeout(function () { self._resume(); }); return [start, this.end]; } /** Restart after pause */ resume() { if (this.running) return; if (this.candidat.length) { this.running = true; this.nb = 0; this._resume(); } } /** Pause */ pause() { if (!this.running) return; this.nb = -1; } /** Get the current 'best way'. * This may be used to animate while calculating. * @return {Array
                    } */ getBestWay() { var node, max = -1; for (var i = 0, n; n = this.candidat[i]; i++) { if (n.get('wdist') > max) { node = n; max = n.get('wdist'); } } // Calculate route to this node return this.getRoute(node); } /** Go on searching new candidats * @private */ _resume() { if (!this.running) return; while (this.candidat.length) { // Sort by wdist this.candidat.sort(function (a, b) { return (a.get('dtotal') < b.get('dtotal') ? 1 : a.get('dtotal') === b.get('dtotal') ? 0 : -1); }); // First candidate var node = this.candidat.pop(); var p = node.getGeometry().getCoordinates(); // Find connected edges var edges = this.getEdges(p); for (var i = 0, e; e = edges[i]; i++) { if (node.get('from') !== e) { var dist = this.getLength(e); if (dist < 0) { console.log('distance < 0!'); // continue; } var wdist = node.get('wdist') + dist * this.weight(e); dist = node.get('dist') + dist; var pt1 = e.getGeometry().getFirstCoordinate(); var pt2 = e.getGeometry().getLastCoordinate(); var sens = this.direction(e); if (sens !== 0) { if (p[0] === pt1[0] && p[1] === pt1[1] && sens !== -1) { this.addNode(pt2, wdist, dist, e, node); } if (p[0] === pt2[0] && p[0] === pt2[0] && sens !== 1) { this.addNode(pt1, wdist, dist, e, node); } } } // Test overflow or pause if (this.nb === -1 || this.nb++ > this.maxIteration) { this.running = false; this.dispatchEvent({ type: 'pause', overflow: (this.nb !== -1) }); return; } // Take time to do something if (!(this.nb % this.stepIteration)) { var self = this; window.setTimeout(function () { self._resume(); }, 5); this.dispatchEvent({ type: 'calculating' }); return; } } } // Finish! this.nodes.clear(); this.running = false; this.dispatchEvent({ type: 'finish', route: this.getRoute(this.arrival), wDistance: this.wdist, distance: this.arrival.get('dist') }); } /** Get the route to a node * @param {ol/Feature} node * @return {Array
                      } * @private */ getRoute(node) { var route = []; while (node) { route.unshift(node.get('from')); node = node.get('prev'); } route.shift(); return route; } } // Typo error for compatibility purposes (to be removed) ol.graph.Dijskra = ol.graph.Dijkstra /** French Geoportail alti coding * @param {ol.geom.Geometry} geom * @param {Object} options * @param {ol/proj~ProjectionLike} [options.projection='EPSG:3857'] geometry projection, default 'EPSG:3857' * @param {string} [options.apiKey='essentiels'] Geoportail API key * @param {number} [options.sampling=0] number of resulting point, max 5000, if none keep input points or use samplingDist * @param {number} [options.samplingDist=0] distance for sampling the line or use sampling if lesser * @param {string} options.success a function that takes the resulting XYZ geometry * @param {string} options.error */ ol.geom.GPAltiCode = function(geom, options) { options = options || {}; var typeGeom = geom.getType(); if (typeGeom !== 'Point' && typeGeom !== 'LineString') { console.warn('[GPAltiCode] '+typeGeom+' not supported...') return; } var proj = options.projection || 'EPSG:3857'; var sampling = options.sampling || 0; if (options.samplingDist) { var d = geom.getLength(); sampling = Math.max(sampling, Math.round(d / options.samplingDist)); } if (sampling > 5000) sampling = 5000; if (sampling < 2) sampling = 0; geom = geom.clone().transform(proj, 'EPSG:4326'); var g, lon = [], lat = []; switch (typeGeom) { case 'Point': { g = [geom.getCoordinates()]; break; } case 'LineString': { g = geom.getCoordinates(); break; } default: return; } if (sampling <= g.length) sampling = 0; g.forEach(function(p) { lon.push(Math.round(p[0]*1000000)/1000000); lat.push(Math.round(p[1]*1000000)/1000000); }); // Get elevation var param = 'lon='+lon.join('|')+'&lat='+lat.join('|'); if (sampling) param += '&sampling='+sampling; ol.ext.Ajax.get({ url: 'https://wxs.ign.fr/'+(options.apiKey || 'essentiels')+'/alti/rest/'+(lon.length>1 ? 'elevationLine' : 'elevation')+'.json?'+param, success: function(res) { var pts = []; res.elevations.forEach(function(e, i) { if (sampling) { pts.push([e.lon, e.lat, e.z]); } else { pts.push([g[i][0], g[i][1], e.z]); } }); if (typeGeom==='Point') pts = pts[0]; var result = ol.geom.createFromType(typeGeom, pts); result.transform('EPSG:4326', proj); if (typeof(options.success) === 'function') options.success(result); }, error: function(e) { if (typeof(options.error) === 'function') options.error(e); } }); } /** Calculate elevation on coordinates or on a set of coordinates * @param {ol.coordinate|Array} coord coordinate or an array of coordinates * @param {Object} options * @param {ol/proj~ProjectionLike} [options.projection='EPSG:3857'] geometry projection, default 'EPSG:3857' * @param {string} [options.apiKey='essentiels'] Geoportail API key * @param {number} [options.sampling=0] number of resulting point, max 5000, if none keep input points or use samplingDist * @param {number} [options.samplingDist=0] distance for sampling the line or use sampling if lesser * @param {string} options.success a function that takes the resulting XYZ coordinates * @param {string} options.error */ ol.coordinate.GPAltiCode = function(coord, options) { options = options || {}; var unique = !coord[0].length; var g = unique ? new ol.geom.Point(coord) : new ol.geom.LineString(coord); ol.geom.GPAltiCode(g, { projection: options.projection, apiKey: options.apiKey, sampling: options.sampling, samplingDist: options.samplingDist, success: function(g) { if (typeof(options.success) === 'function') { options.success(g.getCoordinates()) } }, error: options.error }) } /* 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). Usefull function to handle geometric operations */ /** Distance beetween 2 points * Usefull geometric functions * @param {ol.Coordinate} p1 first point * @param {ol.Coordinate} p2 second point * @return {number} distance */ ol.coordinate.dist2d = function(p1, p2) { var dx = p1[0]-p2[0]; var dy = p1[1]-p2[1]; return Math.sqrt(dx*dx+dy*dy); } /** 2 points are equal * Usefull geometric functions * @param {ol.Coordinate} p1 first point * @param {ol.Coordinate} p2 second point * @return {boolean} */ ol.coordinate.equal = function(p1, p2) { return (p1[0]==p2[0] && p1[1]==p2[1]); } /** Get center coordinate of a feature * @param {ol.Feature} f * @return {ol.coordinate} the center */ ol.coordinate.getFeatureCenter = function(f) { return ol.coordinate.getGeomCenter (f.getGeometry()); }; /** Get center coordinate of a geometry * @param {ol.geom.Geometry} geom * @return {ol.Coordinate} the center */ ol.coordinate.getGeomCenter = function(geom) { switch (geom.getType()) { case 'Point': return geom.getCoordinates(); case "MultiPolygon": geom = geom.getPolygon(0); // fallthrough case "Polygon": return geom.getInteriorPoint().getCoordinates(); default: return geom.getClosestPoint(ol.extent.getCenter(geom.getExtent())); } }; /** Offset a polyline * @param {Array} coords * @param {number} offset * @return {Array} resulting coord * @see http://stackoverflow.com/a/11970006/796832 * @see https://drive.google.com/viewerng/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxqa2dhZGdldHN0b3JlfGd4OjQ4MzI5M2Y0MjNmNzI2MjY */ ol.coordinate.offsetCoords = function (coords, offset) { var path = []; var N = coords.length-1; var max = N; var mi, mi1, li, li1, ri, ri1, si, si1, Xi1, Yi1; var p0, p1, p2; var isClosed = ol.coordinate.equal(coords[0],coords[N]); if (!isClosed) { p0 = coords[0]; p1 = coords[1]; p2 = [ p0[0] + (p1[1] - p0[1]) / ol.coordinate.dist2d(p0,p1) *offset, p0[1] - (p1[0] - p0[0]) / ol.coordinate.dist2d(p0,p1) *offset ]; path.push(p2); coords.push(coords[N]) N++; max--; } for (var i = 0; i < max; i++) { p0 = coords[i]; p1 = coords[(i+1) % N]; p2 = coords[(i+2) % N]; mi = (p1[1] - p0[1])/(p1[0] - p0[0]); mi1 = (p2[1] - p1[1])/(p2[0] - p1[0]); // Prevent alignements if (Math.abs(mi-mi1) > 1e-10) { li = Math.sqrt((p1[0] - p0[0])*(p1[0] - p0[0])+(p1[1] - p0[1])*(p1[1] - p0[1])); li1 = Math.sqrt((p2[0] - p1[0])*(p2[0] - p1[0])+(p2[1] - p1[1])*(p2[1] - p1[1])); ri = p0[0] + offset*(p1[1] - p0[1])/li; ri1 = p1[0] + offset*(p2[1] - p1[1])/li1; si = p0[1] - offset*(p1[0] - p0[0])/li; si1 = p1[1] - offset*(p2[0] - p1[0])/li1; Xi1 = (mi1*ri1-mi*ri+si-si1) / (mi1-mi); Yi1 = (mi*mi1*(ri1-ri)+mi1*si-mi*si1) / (mi1-mi); // Correction for vertical lines if(p1[0] - p0[0] == 0) { Xi1 = p1[0] + offset*(p1[1] - p0[1])/Math.abs(p1[1] - p0[1]); Yi1 = mi1*Xi1 - mi1*ri1 + si1; } if (p2[0] - p1[0] == 0 ) { Xi1 = p2[0] + offset*(p2[1] - p1[1])/Math.abs(p2[1] - p1[1]); Yi1 = mi*Xi1 - mi*ri + si; } path.push([Xi1, Yi1]); } } if (isClosed) { path.push(path[0]); } else { coords.pop(); p0 = coords[coords.length-1]; p1 = coords[coords.length-2]; p2 = [ p0[0] - (p1[1] - p0[1]) / ol.coordinate.dist2d(p0,p1) *offset, p0[1] + (p1[0] - p0[0]) / ol.coordinate.dist2d(p0,p1) *offset ]; path.push(p2); } return path; } /** Find the segment a point belongs to * @param {ol.Coordinate} pt * @param {Array} coords * @return {} the index (-1 if not found) and the segment */ ol.coordinate.findSegment = function (pt, coords) { for (var i=0; i} geom * @param {number} y the y to split * @param {number} n contour index * @return {Array>} */ ol.coordinate.splitH = function (geom, y, n) { var x, abs; var list = []; for (var i=0; iy || geom[i][1]>=y && geom[i+1][1]} d1 * @param {Arrar} d2 */ ol.coordinate.getIntersectionPoint = function (d1, d2) { var d1x = d1[1][0] - d1[0][0]; var d1y = d1[1][1] - d1[0][1]; var d2x = d2[1][0] - d2[0][0]; var d2y = d2[1][1] - d2[0][1]; var det = d1x * d2y - d1y * d2x; if (det != 0) { var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det; return [d2[0][0] + k*d2x, d2[0][1] + k*d2y]; } else { return false; } }; ol.extent.intersection; (function() { // Split at x function splitX(pts, x) { var pt; for (var i=pts.length-1; i>0; i--) { if ((pts[i][0]>x && pts[i-1][0]x)) { pt = [ x, (x - pts[i][0]) / (pts[i-1][0]-pts[i][0]) * (pts[i-1][1]-pts[i][1]) + pts[i][1]]; pts.splice(i, 0, pt); } } } // Split at y function splitY(pts, y) { var pt; for (var i=pts.length-1; i>0; i--) { if ((pts[i][1]>y && pts[i-1][1]y)) { pt = [ (y - pts[i][1]) / (pts[i-1][1]-pts[i][1]) * (pts[i-1][0]-pts[i][0]) + pts[i][0], y]; pts.splice(i, 0, pt); } } } /** Fast polygon intersection with an extent (used for area calculation) * @param {ol.extent.Extent} extent * @param {ol.geom.Polygon|ol.geom.MultiPolygon} polygon * @returns {ol.geom.Polygon|ol.geom.MultiPolygon|null} return null if not a polygon geometry */ ol.extent.intersection = function(extent, polygon) { var poly = (polygon.getType() === 'Polygon'); if (!poly && polygon.getType() !== 'MultiPolygon') return null; var geom = polygon.getCoordinates(); if (poly) geom = [geom]; geom.forEach(function(g) { g.forEach(function(c) { splitX(c, extent[0]); splitX(c, extent[2]); splitY(c, extent[1]); splitY(c, extent[3]); }); }) // Snap geom to the extent geom.forEach(function(g) { g.forEach(function(c) { c.forEach(function(p) { if (p[0]extent[2]) p[0] = extent[2]; if (p[1]extent[3]) p[1] = extent[3]; }) }) }) if (poly) { return new ol.geom.Polygon(geom[0]); } else { return new ol.geom.MultiPolygon(geom); } }; })(); /** Add points along a segment * @param {ol.Coordinate} p1 * @param {ol.Coordinate} p2 * @param {number} d * @param {boolean} start include starting point, default true * @returns {Array} */ ol.coordinate.sampleAt = function(p1, p2, d, start) { var pts = []; if (start!==false) pts.push(p1); var dl = ol.coordinate.dist2d(p1,p2); if (dl) { var nb = Math.round(dl/d); if (nb>1) { var dx = (p2[0]-p1[0]) / nb; var dy = (p2[1]-p1[1]) / nb; for (var i=1; i r) { hasout = true; l.push([ c[0] + r / d * (p[0]-c[0]), c[1] + r / d * (p[1]-c[1]) ]); } else { // hasin = true; l.push(p); } }); }) }); if (!hasout) return geom; if (geom.getType() === 'Polygon') { return new ol.geom.Polygon(result[0]); } else { return new ol.geom.MultiPolygon(result); } } } } else { console.warn('[ol/geom/Circle~intersection] Unsupported geometry type: '+geom.getType()); } return geom; }; /** Split a lineString by a point or a list of points * NB: points must be on the line, use getClosestPoint() to get one * @param {ol.Coordinate | Array} pt points to split the line * @param {Number} tol distance tolerance for 2 points to be equal */ ol.geom.LineString.prototype.splitAt = function(pt, tol) { var i; if (!pt) return [this]; if (!tol) tol = 1e-10; // Test if list of points if (pt.length && pt[0].length) { var result = [this]; for (i=0; i split if (split) { ci.push(pt); c.push (new ol.geom.LineString(ci)); ci = [pt]; } } ci.push(c0[i+1]); } if (ci.length>1) c.push (new ol.geom.LineString(ci)); if (c.length) return c; else return [this]; } // import('ol-ext/geom/LineStringSplitAt') /* 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). Usefull function to handle geometric operations */ /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ /** * Calculate a MultiPolyline to fill a Polygon with a scribble effect that appears hand-made * @param {} options * @param {Number} options.interval interval beetween lines * @param {Number} options.angle hatch angle in radian, default PI/2 * @return {ol.geom.MultiLineString|null} the resulting MultiLineString geometry or null if none */ ol.geom.MultiPolygon.prototype.scribbleFill = function (options) { var scribbles = []; var poly = this.getPolygons(); var i, p, s; for (i=0; p=poly[i]; i++) { var mls = p.scribbleFill(options); if (mls) scribbles.push(mls); } if (!scribbles.length) return null; // Merge scribbles var scribble = scribbles[0]; var ls; for (i = 0; s = scribbles[i]; i++) { ls = s.getLineStrings(); for (var k = 0; k < ls.length; k++) { scribble.appendLineString(ls[k]); } } return scribble; }; /** * Calculate a MultiPolyline to fill a Polygon with a scribble effect that appears hand-made * @param {} options * @param {Number} options.interval interval beetween lines * @param {Number} options.angle hatch angle in radian, default PI/2 * @return {ol.geom.MultiLineString|null} the resulting MultiLineString geometry or null if none */ ol.geom.Polygon.prototype.scribbleFill = function (options) { var step = options.interval; var angle = options.angle || Math.PI/2; var i, k,l; // Geometry + rotate var geom = this.clone(); geom.rotate(angle, [0,0]); var coords = geom.getCoordinates(); // Merge holes var coord = coords[0]; for (i=1; i nexty) break; if (lines[k][0].pt[1] === nexty) { var d = Math.min( (lines[k][0].index - l[0].index + mod) % mod, (l[0].index - lines[k][0].index + mod) % mod ); var d2 = Math.min( (l[1].index - l[0].index + mod) % mod, (l[0].index - l[1].index + mod) % mod ); if (d} features * @param {number} [round] round features */ setFeatures(features, round) { console.time('arcs') if (round) round = Math.pow(10, round); var edges = this._calcEdges(features, round) console.timeLog('arcs') /* DEBUG * / this._edges.clear(true); var eds = [] edges.forEach(function(e) { eds.push(e.feature); }) this._edges.addFeatures(eds) /**/ console.time('chain') this._edges = this._chainEdges(edges); console.timeLog('chain') return this._edges } /** Get the simplified features * @returns {Array} */ getFeatures() { var features = []; this._edges.forEach(function(edge) { edge.get('edge').forEach(function(ed) { // Already inserted? var f = features.find(function(e) { return ed.feature === e.feature; }) // New one if (!f) { f = { feature: ed.feature, contour: {} } features.push(f) } // add contour if (!f.contour[ed.contour]) f.contour[ed.contour] = []; f.contour[ed.contour].push({ edge: edge, index: ed.index }) }) }) // Recreate objects features.forEach(function(f) { f.typeGeom = f.feature.getGeometry().getType(); f.nom = f.feature.get('nom'); var g = []; // console.log(f.contour) for (var c in f.contour) { var t = c.split('-'); t.shift(); var coordinates = g; while (t.length) { var i = parseInt(t.shift()) if (!coordinates[i]) { coordinates[i] = []; } coordinates = coordinates[i]; } // Join f.contour[c].sort(function(a,b) { return a.index - b.index; }); f.contour[c].forEach(function(contour) { var coord = contour.edge.getGeometry().getCoordinates(); if (!coordinates.length || ol.coordinate.equal(coordinates[coordinates.length-1], coord[0])) { for (var i= coordinates.length ? 1 : 0; i=0; i--) { coordinates.unshift(coord[i]); } } else { // revert for (var i=coord.length-2; i>=0; i--) { coordinates.push(coord[i]); } } // console.log(c, coordinates.length, coord.length) }) } f.geom = g; // console.log(g) f.feature.getGeometry().setCoordinates(g); }) // return features; } /** Simplify edges using Visvalingam algorithm * @param {Object} options * @param {string} options.algo */ simplifyVisvalingam(options) { this._edges.forEach(function(f) { var gtype = f.get('edge')[0].feature.getGeometry().getType(); f.setGeometry(f.get('geom').simplifyVisvalingam({ area: options.area, dist: options.dist, ratio: options.ratio, minPoints: options.minPoints, keepEnds: /Polygon/.test(gtype) ? true : options.keepEnds })) }) } /** Simplify edges using Douglas Peucker algorithm * @param {number} tolerance */ simplify(tolerance) { this._edges.forEach(function(f) { f.setGeometry(f.get('geom').simplify(tolerance)) }) } /** Calculate edges * @param {Array} features * @returns {Array} * @private */ _calcEdges(features, round) { var edges = {}; var prev, prevEdge; function createEdge(f, a, i) { var id = a.seg[0] +'-'+ a.seg[1]; // Existing edge var e = edges[id]; // Test revert if (!e) { id = a.seg[1] +'-'+ a.seg[0]; e = edges[id]; } // Add or create a new one if (e) { e.edge.push({ feature: f, contour: a.contour, index: i }) prev = ''; } else { var edge = { geometry: a.seg, edge: [{ feature: f, contour: a.contour, index: i }], prev: prev === a.contour ? prevEdge : false }; /* DEBUG * / edge.feature = new ol.Feature({ geometry: new ol.geom.LineString(a.seg), edge: edge.edge, prev: edge.prev }) /* */ prev = a.contour; // For back chain prevEdge = edge; edges[id] = edge } } // Get all edges features.forEach(function(f) { if (!/Point/.test(f.getGeometry().getType())) { var arcs = this._getArcs(f.getGeometry().getCoordinates(), [], '0', round); // Create edges for arcs prev = ''; arcs.forEach(function (a, i) { createEdge(f, a, i) }); } }.bind(this)) // Convert to Array var tedges = []; for (var i in edges) tedges.push(edges[i]) return tedges; } /** Retrieve edges of arcs * @param {*} coords * @param {*} arcs * @param {*} contour * @returns Array * @private */ _getArcs(coords, arcs, contour, round) { // New contour if (coords[0][0][0].length) { coords.forEach(function(c, i) { this._getArcs(c, arcs, contour + '-' + i, round) }.bind(this)) } else { coords.forEach(function(c, k) { var p1, p0 = c[0]; // p0 = round ? [Math.round(c[0][0] * round) / round, Math.round(c[0][1] * round) / round] : c[0]; var ct = contour + '-' + k; for (var i=1; i} */ _chainEdges(edges) { // 2 edges are connected function isConnected(edge1, edge2) { if (edge1.length === edge2.length) { var connected, e1, e2; for (var i=0; i < edge1.length; i++) { e1 = edge1[i] connected = false; for (var j=0; j < edge2.length; j++) { e2 = edge2[j]; if (e1.feature === e2.feature && e1.contour === e2.contour) { connected = true; break; } } if (!connected) return false; } return true } return false; } // Chain features back function chainBack(f) { if (f.del) return; // Previous edge var prev = f.prev; if (!prev) return; // Merge edges if (isConnected(f.edge, prev.edge)) { // Remove prev... prev.del = true; // ...and merge with current var g = prev.geometry; var g1 = f.geometry; g1.shift(); f.geometry = g.concat(g1); f.prev = prev.prev; // Chain chainBack(f); } } // Chain features back edges.forEach(chainBack) // New arcs features var result = []; edges.forEach(function(f) { if (!f.del) { result.push(new ol.Feature({ geometry: new ol.geom.LineString(f.geometry), geom: new ol.geom.LineString(f.geometry), edge: f.edge, prev: f.prev })); } }) return result; } } /** Geohash encoding/decoding and associated functions * (c) Chris Veness 2014-2019 / MIT Licence * https://github.com/chrisveness/latlon-geohash */ ol.geohash = { // (geohash-specific) Base32 map base32: '0123456789bcdefghjkmnpqrstuvwxyz' }; /** Encodes latitude/longitude to geohash, either to specified precision or to automatically * evaluated precision. * @param {ol.coordinate} lonlat Longitude, Latitude in degrees. * @param {number} [precision] Number of characters in resulting geohash. * @returns {string} Geohash of supplied latitude/longitude. */ ol.geohash.fromLonLat = function(lonlat, precision) { var lon = lonlat[0]; var lat = lonlat[1]; // infer precision? if (!precision) { // refine geohash until it matches precision of supplied lat/lon for (var p=1; p<=12; p++) { var hash = ol.geohash.fromLonLat([lon, lat], p); var posn = ol.geohash.toLonLat(hash); if (posn.lat==lat && posn.lon==lon) return hash; } precision = 12; // set to maximum } if (precision < 1 || precision > 12) precision = 12; var idx = 0; // index into base32 map var bit = 0; // each char holds 5 bits var evenBit = true; var geohash = ''; var latMin = -90, latMax = 90; var lonMin = -180, lonMax = 180; while (geohash.length < precision) { if (evenBit) { // bisect E-W longitude var lonMid = (lonMin + lonMax) / 2; if (lon >= lonMid) { idx = idx*2 + 1; lonMin = lonMid; } else { idx = idx*2; lonMax = lonMid; } } else { // bisect N-S latitude var latMid = (latMin + latMax) / 2; if (lat >= latMid) { idx = idx*2 + 1; latMin = latMid; } else { idx = idx*2; latMax = latMid; } } evenBit = !evenBit; if (++bit == 5) { // 5 bits gives us a character: append it and start over geohash += ol.geohash.base32.charAt(idx); bit = 0; idx = 0; } } return geohash; }; /** Decode geohash to latitude/longitude * (location is approximate centre of geohash cell, to reasonable precision). * @param {string} geohash - Geohash string to be converted to latitude/longitude. * @returns {ol.coordinate} */ ol.geohash.toLonLat = function(geohash) { var extent = ol.geohash.getExtent(geohash); // <-- the hard work // now just determine the centre of the cell... var latMin = extent[1], lonMin = extent[0]; var latMax = extent[3], lonMax = extent[2]; // cell centre var lat = (latMin + latMax)/2; var lon = (lonMin + lonMax)/2; // round to close to centre without excessive precision: ⌊2-log10(Δ°)⌋ decimal places lat = lat.toFixed(Math.floor(2-Math.log(latMax-latMin)/Math.LN10)); lon = lon.toFixed(Math.floor(2-Math.log(lonMax-lonMin)/Math.LN10)); return [Number(lon), Number(lat)]; }; /** Returns SW/NE latitude/longitude bounds of specified geohash. * @param {string} geohash Cell that bounds are required of. * @returns {ol.extent | false} */ ol.geohash.getExtent = function(geohash) { if (!geohash) return false; geohash = geohash.toLowerCase(); var evenBit = true; var latMin = -90, latMax = 90; var lonMin = -180, lonMax = 180; for (var i=0; i=0; n--) { var bitN = idx >> n & 1; if (evenBit) { // longitude var lonMid = (lonMin+lonMax) / 2; if (bitN == 1) { lonMin = lonMid; } else { lonMax = lonMid; } } else { // latitude var latMid = (latMin+latMax) / 2; if (bitN == 1) { latMin = latMid; } else { latMax = latMid; } } evenBit = !evenBit; } } return [lonMin, latMin, lonMax, latMax]; }; /** Determines adjacent cell in given direction. * @param {string} geohash Geohash cel * @param {string} direction direction as char : N/S/E/W. * @returns {string|false} */ ol.geohash.getAdjacent = function (geohash, direction) { // based on github.com/davetroy/geohash-js geohash = geohash.toLowerCase(); direction = direction.toLowerCase(); if (!geohash) return false; if ('nsew'.indexOf(direction) == -1) return false; var neighbour = { n: [ 'p0r21436x8zb9dcf5h7kjnmqesgutwvy', 'bc01fg45238967deuvhjyznpkmstqrwx' ], s: [ '14365h7k9dcfesgujnmqp0r2twvyx8zb', '238967debc01fg45kmstqrwxuvhjyznp' ], e: [ 'bc01fg45238967deuvhjyznpkmstqrwx', 'p0r21436x8zb9dcf5h7kjnmqesgutwvy' ], w: [ '238967debc01fg45kmstqrwxuvhjyznp', '14365h7k9dcfesgujnmqp0r2twvyx8zb' ], }; var border = { n: [ 'prxz', 'bcfguvyz' ], s: [ '028b', '0145hjnp' ], e: [ 'bcfguvyz', 'prxz' ], w: [ '0145hjnp', '028b' ], }; var lastCh = geohash.slice(-1); // last character of hash var parent = geohash.slice(0, -1); // hash without last character var type = geohash.length % 2; // check for edge-cases which don't share common prefix if (border[direction][type].indexOf(lastCh) != -1 && parent != '') { parent = ol.geohash.getAdjacent(parent, direction); } // append letter for direction to parent return parent + ol.geohash.base32.charAt(neighbour[direction][type].indexOf(lastCh)); } /** Returns all 8 adjacent cells to specified geohash. * @param {string} geohash Geohash neighbours are required of. * @returns {{n,ne,e,se,s,sw,w,nw: string}} */ ol.geohash.getNeighbours = function(geohash) { return { 'n': ol.geohash.getAdjacent(geohash, 'n'), 'ne': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 'n'), 'e'), 'e': ol.geohash.getAdjacent(geohash, 'e'), 'se': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 's'), 'e'), 's': ol.geohash.getAdjacent(geohash, 's'), 'sw': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 's'), 'w'), 'w': ol.geohash.getAdjacent(geohash, 'w'), 'nw': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 'n'), 'w'), }; } /** Compute great circle bearing of two points. * @See http://www.movable-type.co.uk/scripts/latlong.html for the original code * @param {ol.coordinate} origin origin in lonlat * @param {ol.coordinate} destination destination in lonlat * @return {number} bearing angle in radian */ ol.sphere.greatCircleBearing = function(origin, destination) { var toRad = Math.PI/180; var ori = [ origin[0]*toRad, origin[1]*toRad ]; var dest = [ destination[0]*toRad, destination[1]*toRad ]; var bearing = Math.atan2( Math.sin(dest[0] - ori[0]) * Math.cos(dest[1]), Math.cos(ori[1]) * Math.sin(dest[1]) - Math.sin(ori[1]) * Math.cos(dest[1]) * Math.cos(dest[0] - ori[0]) ); return bearing; }; /** * Computes the destination point given an initial point, a distance and a bearing * @See http://www.movable-type.co.uk/scripts/latlong.html for the original code * @param {ol.coordinate} origin stating point in lonlat coords * @param {number} distance * @param {number} bearing bearing angle in radian * @param {*} options * @param {booelan} normalize normalize longitude beetween -180/180, deafulet true * @param {number|undefined} options.radius sphere radius, default 6371008.8 */ ol.sphere.computeDestinationPoint = function(origin, distance, bearing, options) { options = options || {}; var toRad = Math.PI/180; var radius = options.radius || 6371008.8; var phi1 = origin[1] * toRad; var lambda1 = origin[0] * toRad; var delta = distance / radius; var phi2 = Math.asin( Math.sin(phi1) * Math.cos(delta) + Math.cos(phi1) * Math.sin(delta) * Math.cos(bearing) ); var lambda2 = lambda1 + Math.atan2( Math.sin(bearing) * Math.sin(delta) * Math.cos(phi1), Math.cos(delta) - Math.sin(phi1) * Math.sin(phi2) ); var lon = lambda2 / toRad; // normalise to >=-180 and <=180° if (options.normalize!==false && (lon < -180 || lon > 180)) { lon = ((lon * 540) % 360) - 180; } return [ lon, phi2 / toRad ]; }; /** Calculate a track along the great circle given an origin and a destination * @param {ol.coordinate} origin origin in lonlat * @param {ol.coordinate} destination destination in lonlat * @param {number} distance distance between point along the track in meter, default 1km (1000) * @param {number|undefined} radius sphere radius, default 6371008.8 * @return {Array} */ ol.sphere.greatCircleTrack = function(origin, destination, options) { options = options || {}; var bearing = ol.sphere.greatCircleBearing(origin, destination); var dist = ol.sphere.getDistance(origin, destination, options.radius); var distance = options.distance || 1000; var d = distance; var geom = [origin]; while (d < dist) { geom.push(ol.sphere.computeDestinationPoint(origin, d, bearing, { radius: options.radius, normalize: false })); d += distance; } var pt = ol.sphere.computeDestinationPoint(origin, dist, bearing, { radius: options.radius, normalize: false }); if (Math.abs(pt[0]-destination[0]) > 1) { if (pt[0] > destination[0]) destination[0] += 360; else destination[0] -= 360; } geom.push(destination); return geom; }; /** Get map scale factor * @param {ol.Map} map * @param {number} [dpi=96] dpi, default 96 * @return {number} */ ol.sphere.getMapScale = function (map, dpi) { var view = map.getView(); var proj = view.getProjection(); var center = view.getCenter(); var px = map.getPixelFromCoordinate(center); px[1] += 1; var coord = map.getCoordinateFromPixel(px); var d = ol.sphere.getDistance( ol.proj.transform(center, proj, 'EPSG:4326'), ol.proj.transform(coord, proj, 'EPSG:4326')); d *= (dpi||96) /.0254 return d; }; /** Set map scale factor * @param {ol.Map} map * @param {number|string} scale the scale factor or a scale string as 1/xxx * @param {number} [dpi=96] dpi, default 96 * @return {number} scale factor */ ol.sphere.setMapScale = function (map, scale, dpi) { if (map && scale) { var fac = scale; if (typeof(scale)==='string') { fac = scale.split('/')[1]; if (!fac) fac = scale; fac = fac.replace(/[^\d]/g,''); fac = parseInt(fac); } if (!fac) return; // Calculate new resolution var view = map.getView(); var proj = view.getProjection(); var center = view.getCenter(); var px = map.getPixelFromCoordinate(center); px[1] += 1; var coord = map.getCoordinateFromPixel(px); var d = ol.sphere.getDistance( ol.proj.transform(center, proj, 'EPSG:4326'), ol.proj.transform(coord, proj, 'EPSG:4326')); d *= (dpi || 96) /.0254 view.setResolution(view.getResolution()*fac/d); return fac; } }; (function () { /** * Visvalingam polyline simplification algorithm, adapted from http://bost.ocks.org/mike/simplify/simplify.js * This uses the [Visvalingam–Whyatt](https://en.wikipedia.org/wiki/Visvalingam%E2%80%93Whyatt_algorithm) algorithm. * @param {Object} options * @param {number} [area] the tolerance area for simplification * @param {number} [dist] a tolerance distance for simplification * @param {number} [ratio=.8] a ratio of points to keep * @param {number} [minPoints=2] minimum number of points to keep * @param {boolean} [keepEnds] keep line ends * @return { LineString } A new, simplified version of the original geometry. * @api */ ol.geom.LineString.prototype.simplifyVisvalingam = function (options) { var points = this.getCoordinates(); if (options.minPoints && options.minPoints >= points.length) { return new ol.geom.LineString(points); } var heap = minHeap(), maxArea = 0, triangle, triangles = []; points = points.map(function (d) { return d.slice(0,2); }); for (var i = 1, n = points.length - 1; i < n; ++i) { triangle = points.slice(i - 1, i + 2); if (triangle[1][2] = area(triangle)) { triangles.push(triangle); heap.push(triangle); } } for (i = 0, n = triangles.length; i < n; ++i) { triangle = triangles[i]; triangle.previous = triangles[i - 1]; triangle.next = triangles[i + 1]; } while (triangle = heap.pop()) { // If the area of the current point is less than that of the previous point // to be eliminated, use the latters area instead. This ensures that the // current point cannot be eliminated without eliminating previously- // eliminated points. if (triangle[1][2] < maxArea) triangle[1][2] = maxArea; else maxArea = triangle[1][2]; if (triangle.previous) { triangle.previous.next = triangle.next; triangle.previous[2] = triangle[2]; update(triangle.previous); } else { triangle[0][2] = triangle[1][2]; } if (triangle.next) { triangle.next.previous = triangle.previous; triangle.next[0] = triangle[0]; update(triangle.next); } else { triangle[2][2] = triangle[1][2]; } } function update(triangle) { heap.remove(triangle); triangle[1][2] = area(triangle); heap.push(triangle); } // Get area to remove var w = options.area; if (options.dist) w = options.dist * options.dist / 2; // If no area if (w === undefined || options.minPoints) { // Get ordered weights var weights = points.map(function (d) { return d.length < 3 ? Infinity : d[2] += Math.random(); /* break ties */ }); weights.sort(function (a, b) { return b - a; }); if (w) { // Check min points if (weights[options.minPoints] < w) { w = weights[options.minPoints] } } else { var pointsToKeep = options.minPoints; // Calculate ratio if (!pointsToKeep) { var ratio = options.ratio || .8 pointsToKeep = Math.round(points.length * ratio); } pointsToKeep = Math.min(pointsToKeep, weights.length -1); w = weights[pointsToKeep] } } var result = points.filter(function (d) { return d[2] > w; }); if (options.keepEnds) { if (!ol.coordinate.equal(result[0], points[0])) result.unshift(points[0]); if (!ol.coordinate.equal(result[result.length-1], points[points.length-1])) result.push(points[points.length-1]); } return new ol.geom.LineString(result); }; function compare(a, b) { return a[1][2] - b[1][2]; } function area(t) { return Math.abs((t[0][0] - t[2][0]) * (t[1][1] - t[0][1]) - (t[0][0] - t[1][0]) * (t[2][1] - t[0][1])); } function minHeap() { var heap = {}, array = []; heap.push = function() { for (var i = 0, n = arguments.length; i < n; ++i) { var object = arguments[i]; up(object.index = array.push(object) - 1); } return array.length; }; heap.pop = function() { var removed = array[0], object = array.pop(); if (array.length) { array[object.index = 0] = object; down(0); } return removed; }; heap.size = function () { return array.length; }; heap.remove = function(removed) { var i = removed.index, object = array.pop(); if (i !== array.length) { array[object.index = i] = object; (compare(object, removed) < 0 ? up : down)(i); } return i; }; function up(i) { var object = array[i]; while (i > 0) { var up = ((i + 1) >> 1) - 1, parent = array[up]; if (compare(object, parent) >= 0) break; array[parent.index = i] = parent; array[object.index = i = up] = object; } } function down(i) { var object = array[i]; for (;;) { var right = (i + 1) * 2, left = right - 1, down = i, child = array[down]; if (left < array.length && compare(array[left], child) < 0) child = array[down = left]; if (right < array.length && compare(array[right], child) < 0) child = array[down = right]; if (down === i) break; array[child.index = i] = child; array[object.index = i = down] = object; } } return heap; } })(); /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Pulse an extent on postcompose * @param {ol.coordinates} point to pulse * @param {ol.pulse.options} options pulse options param * @param {ol.projectionLike|undefined} options.projection projection of coords, default no transform * @param {Number} options.duration animation duration in ms, default 2000 * @param {ol.easing} options.easing easing function, default ol.easing.upAndDown * @param {ol.style.Stroke} options.style stroke style, default 2px red */ ol.Map.prototype.animExtent = function(extent, options){ var listenerKey; options = options || {}; // Change to map's projection if (options.projection) { extent = ol.proj.transformExtent (extent, options.projection, this.getView().getProjection()); } // options var start = new Date().getTime(); var duration = options.duration || 1000; var easing = options.easing || ol.easing.upAndDown; var width = options.style ? options.style.getWidth() || 2 : 2; var color = options.style ? options.style.getColr() || 'red' : 'red'; // Animate function function animate(event) { var frameState = event.frameState; var ratio = frameState.pixelRatio; var elapsed = frameState.time - start; if (elapsed > duration) { ol.Observable.unByKey(listenerKey); } else { var elapsedRatio = elapsed / duration; var p0 = this.getPixelFromCoordinate([extent[0],extent[1]]); var p1 = this.getPixelFromCoordinate([extent[2],extent[3]]); var context = event.context; context.save(); context.scale(ratio,ratio); context.beginPath(); // var e = easing(elapsedRatio) context.globalAlpha = easing(1 - elapsedRatio); context.lineWidth = width; context.strokeStyle = color; context.rect(p0[0], p0[1], p1[0]-p0[0], p1[1]-p0[1]); context.stroke(); context.restore(); // tell OL3 to continue postcompose animation frameState.animate = true; } } // Launch animation listenerKey = this.on('postcompose', animate.bind(this)); try { this.renderSync(); } catch(e) { /* ok */ } } /** Create a cardinal spline version of this geometry. * Original https://github.com/epistemex/cardinal-spline-js * @see https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline * * @param {} options * @param {Number} options.tension a [0,1] number / can be interpreted as the "length" of the tangent, default 0.5 * @param {Number} options.resolution size of segment to split * @param {Integer} options.pointsPerSeg number of points per segment to add if no resolution is provided, default add 10 points per segment */ /** Cache cspline calculation on a geometry * @param {} options * @param {Number} options.tension a [0,1] number / can be interpreted as the "length" of the tangent, default 0.5 * @param {Number} options.resolution size of segment to split * @param {Integer} options.pointsPerSeg number of points per segment to add if no resolution is provided, default add 10 points per segment * @return {ol.geom.Geometry} */ ol.geom.Geometry.prototype.cspline = function(options){ // Calculate cspline if (this.calcCSpline_){ if (this.csplineGeometryRevision != this.getRevision() || this.csplineOption != JSON.stringify(options)) { this.csplineGeometry_ = this.calcCSpline_(options) this.csplineGeometryRevision = this.getRevision(); this.csplineOption = JSON.stringify(options); } return this.csplineGeometry_; } else { // Default do nothing return this; } }; ol.geom.GeometryCollection.prototype.calcCSpline_ = function(options) { var g=[], g0=this.getGeometries(); for (var i=0; i} line * @param {} options * @param {Number} options.tension a [0,1] number / can be interpreted as the "length" of the tangent, default 0.5 * @param {Number} options.resolution size of segment to split * @param {Integer} options.pointsPerSeg number of points per segment to add if no resolution is provided, default add 10 points per segment * @return {Array} */ ol.coordinate.cspline = function(line, options) { if (!options) options={}; var tension = typeof options.tension === "number" ? options.tension : 0.5; var length = 0; var p0 = line[0]; line.forEach(function(p) { length += ol.coordinate.dist2d(p0, p); p0 = p; }) var resolution = options.resolution || (length / line.length / (options.pointsPerSeg || 10)); var pts, res = [], // clone array x, y, // our x,y coords t1x, t2x, t1y, t2y, // tension vectors c1, c2, c3, c4, // cardinal points st, t, i; // steps based on num. of segments // clone array so we don't change the original // pts = line.slice(0); // The algorithm require a previous and next point to the actual point array. // Check if we will draw closed or open curve. // If closed, copy end points to beginning and first points to end // If open, duplicate first points to beginning, end points to end if (line.length>2 && line[0][0]==line[line.length-1][0] && line[0][1]==line[line.length-1][1]) { pts.unshift(line[line.length-2]); pts.push(line[1]); } else { pts.unshift(line[0]); pts.push(line[line.length-1]); } // ok, lets start.. function dist2d(x1, y1, x2, y2) { var dx = x2-x1; var dy = y2-y1; return Math.sqrt(dx*dx+dy*dy); } // 1. loop goes through point array // 2. loop goes through each segment between the 2 pts + 1e point before and after for (i=1; i < (pts.length - 2); i++) { var d1 = dist2d (pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1]); var numOfSegments = Math.round(d1/resolution); var d=1; if (options.normalize) { d1 = dist2d (pts[i+1][0], pts[i+1][1], pts[i-1][0], pts[i-1][1]); var d2 = dist2d (pts[i+2][0], pts[i+2][1], pts[i][0], pts[i][1]); if (d1 y_diff && x_diff > z_diff) rx = -ry - rz; else if (y_diff > z_diff) ry = -rx - rz; else rz = -rx - ry; return [rx, ry, rz]; } /** Round axial coords * @param {ol.Coordinate} h axial coordinate * @return {ol.Coordinate} rounded axial coordinate */ hex_round(h) { return this.cube2hex(this.cube_round(this.hex2cube(h))); } /** Get hexagon corners */ hex_corner(center, size, i) { return [center[0] + size * this.layout_[8 + (2 * (i % 6))], center[1] + size * this.layout_[9 + (2 * (i % 6))]]; } /** Get hexagon coordinates at a coordinate * @param {ol.Coordinate} coord * @return {Arrary} */ getHexagonAtCoord(coord) { return (this.getHexagon(this.coord2hex(coord))); } /** Get hexagon coordinates at hex * @param {ol.Coordinate} hex * @return {Arrary} */ getHexagon(hex) { var p = []; var c = this.hex2coord(hex); for (var i = 0; i <= 7; i++) { p.push(this.hex_corner(c, this.size_, i, this.layout_[8])); } return p; } /** Convert hex to coord * @param {ol.hex} hex * @return {ol.Coordinate} */ hex2coord(hex) { return [ this.origin_[0] + this.size_ * (this.layout_[0] * hex[0] + this.layout_[1] * hex[1]), this.origin_[1] + this.size_ * (this.layout_[2] * hex[0] + this.layout_[3] * hex[1]) ]; } /** Convert coord to hex * @param {ol.Coordinate} coord * @return {ol.hex} */ coord2hex(coord) { var c = [(coord[0] - this.origin_[0]) / this.size_, (coord[1] - this.origin_[1]) / this.size_]; var q = this.layout_[4] * c[0] + this.layout_[5] * c[1]; var r = this.layout_[6] * c[0] + this.layout_[7] * c[1]; return this.hex_round([q, r]); } /** Calculate distance between to hexagon (number of cube) * @param {ol.Coordinate} a first cube coord * @param {ol.Coordinate} a second cube coord * @return {number} distance */ cube_distance(a, b) { return (Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1]), Math.abs(a[2] - b[2]))); } /** Line interpolation (for floats) * @private */ lerp(a, b, t) { return a + (b - a) * t; } /** Line interpolation (for hexes) * @private */ cube_lerp(a, b, t) { return [ this.lerp(a[0] + 1e-6, b[0], t), this.lerp(a[1] + 1e-6, b[1], t), this.lerp(a[2] + 1e-6, b[2], t) ]; } /** Calculate line between to hexagon * @param {ol.Coordinate} a first cube coord * @param {ol.Coordinate} b second cube coord * @return {Array} array of cube coordinates */ cube_line(a, b) { var d = this.cube_distance(a, b); if (!d) return [a]; var results = []; for (var i = 0; i <= d; i++) { results.push(this.cube_round(this.cube_lerp(a, b, i / d))); } return results; } /** Get the neighbors for an hexagon * @param {ol.Coordinate} h axial coord * @param {number} direction * @return { ol.Coordinate | Array } neighbor || array of neighbors */ hex_neighbors(h, d) { if (d !== undefined) { return [h[0] + this.neighbors.hex[d % 6][0], h[1] + this.neighbors.hex[d % 6][1]]; } else { var n = []; for (d = 0; d < 6; d++) { n.push([h[0] + this.neighbors.hex[d][0], h[1] + this.neighbors.hex[d][1]]); } return n; } } /** Get the neighbors for an hexagon * @param {ol.Coordinate} c cube coord * @param {number} direction * @return { ol.Coordinate | Array } neighbor || array of neighbors */ cube_neighbors(c, d) { if (d !== undefined) { return [c[0] + this.neighbors.cube[d % 6][0], c[1] + this.neighbors.cube[d % 6][1], c[2] + this.neighbors.cube[d % 6][2]]; } else { var n = []; for (d = 0; d < 6; d++) { n.push([c[0] + this.neighbors.cube[d][0], c[1] + this.neighbors.cube[d][1], c[2] + this.neighbors.cube[d][2]]); } for (d = 0; d < 6; d++) n[d] = this.cube2hex(n[d]); return n; } } } /** Grid layout */ ol.HexGrid.prototype.layout = { pointy: [ Math.sqrt(3), Math.sqrt(3)/2, 0, 3/2, Math.sqrt(3)/3, -1/3, 0, 2/3, // corners Math.cos(Math.PI / 180 * (60 * 0 + 30)), Math.sin(Math.PI / 180 * (60 * 0 + 30)), Math.cos(Math.PI / 180 * (60 * 1 + 30)), Math.sin(Math.PI / 180 * (60 * 1 + 30)), Math.cos(Math.PI / 180 * (60 * 2 + 30)), Math.sin(Math.PI / 180 * (60 * 2 + 30)), Math.cos(Math.PI / 180 * (60 * 3 + 30)), Math.sin(Math.PI / 180 * (60 * 3 + 30)), Math.cos(Math.PI / 180 * (60 * 4 + 30)), Math.sin(Math.PI / 180 * (60 * 4 + 30)), Math.cos(Math.PI / 180 * (60 * 5 + 30)), Math.sin(Math.PI / 180 * (60 * 5 + 30)) ], flat: [ 3/2, 0, Math.sqrt(3)/2, Math.sqrt(3), 2/3, 0, -1/3, Math.sqrt(3) / 3, // corners Math.cos(Math.PI / 180 * (60 * 0)), Math.sin(Math.PI / 180 * (60 * 0)), Math.cos(Math.PI / 180 * (60 * 1)), Math.sin(Math.PI / 180 * (60 * 1)), Math.cos(Math.PI / 180 * (60 * 2)), Math.sin(Math.PI / 180 * (60 * 2)), Math.cos(Math.PI / 180 * (60 * 3)), Math.sin(Math.PI / 180 * (60 * 3)), Math.cos(Math.PI / 180 * (60 * 4)), Math.sin(Math.PI / 180 * (60 * 4)), Math.cos(Math.PI / 180 * (60 * 5)), Math.sin(Math.PI / 180 * (60 * 5)) ] }; /** Neighbors list * @private */ ol.HexGrid.prototype.neighbors = { 'cube': [ [+1, -1, 0], [+1, 0, -1], [0, +1, -1], [-1, +1, 0], [-1, 0, +1], [0, -1, +1] ], 'hex': [ [+1, 0], [+1, -1], [0, -1], [-1, 0], [-1, +1], [0, +1] ] }; /* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * French INSEE grids * @classdesc a class to compute French INSEE grids, ie. fix area (200x200m) square grid, * based appon EPSG:3035 * * @requires proj4 * @constructor * @extends {ol.Object} * @param {Object} [options] * @param {number} [options.size] size grid size in meter, default 200 (200x200m) */ ol.InseeGrid = class olInseeGrid extends ol.Object { constructor(options) { options = options || {}; // Define EPSG:3035 if none if (!proj4.defs["EPSG:3035"]) { proj4.defs("EPSG:3035", "+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +units=m +no_defs"); ol.proj.proj4.register(proj4); } super(options); // Options var size = Math.max(200, Math.round((options.size || 0) / 200) * 200); this.set('size', size); } /** Get the grid extent * @param {ol.proj.ProjLike} [proj='EPSG:3857'] */ getExtent(proj) { return ol.proj.transformExtent(ol.InseeGrid.extent, proj || 'EPSG:3035', 'EPSG:3857'); } /** Get grid geom at coord * @param {ol.Coordinate} coord * @param {ol.proj.ProjLike} [proj='EPSG:3857'] */ getGridAtCoordinate(coord, proj) { var c = ol.proj.transform(coord, proj || 'EPSG:3857', 'EPSG:3035'); var s = this.get('size'); var x = Math.floor(c[0] / s) * s; var y = Math.floor(c[1] / s) * s; var geom = new ol.geom.Polygon([[[x, y], [x + s, y], [x + s, y + s], [x, y + s], [x, y]]]); geom.transform('EPSG:3035', proj || 'EPSG:3857'); return geom; } } /** Default grid extent (in EPSG:3035) */ ol.InseeGrid.extent = [3200000,2000000,4300000,3140000]; /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Show a markup a point on postcompose * @deprecated use map.animateFeature instead * @param {ol.coordinates} point to pulse * @param {ol.markup.options} pulse options param * - projection {ol.projection|String|undefined} projection of coords, default none * - delay {Number} delay before mark fadeout * - maxZoom {Number} zoom when mark fadeout * - style {ol.style.Image|ol.style.Style|Array} Image to draw as markup, default red circle * @return Unique key for the listener with a stop function to stop animation */ ol.Map.prototype.markup = function(coords, options) { var listenerKey; var self = this; options = options || {}; // Change to map's projection if (options.projection) { coords = ol.proj.transform(coords, options.projection, this.getView().getProjection()); } // options var start = new Date().getTime(); var delay = options.delay || 3000; var duration = 1000; var maxZoom = options.maxZoom || 100; var easing = ol.easing.easeOut; var style = options.style; if (!style) style = new ol.style.Circle({ radius:10, stroke:new ol.style.Stroke({color:'red', width:2 }) }); if (style instanceof ol.style.Image) style = new ol.style.Style({ image: style }); if (!(style instanceof Array)) style = [style]; // Animate function function animate(event) { var frameState = event.frameState; var elapsed = frameState.time - start; if (elapsed > delay+duration) { ol.Observable.unByKey(listenerKey); listenerKey = null; } else { if (delay>elapsed && this.getView().getZoom()>maxZoom) delay = elapsed; var ratio = frameState.pixelRatio; var elapsedRatio = 0; if (elapsed > delay) elapsedRatio = (elapsed-delay) / duration; var context = event.context; context.save(); context.beginPath(); context.globalAlpha = easing(1 - elapsedRatio); for (var i=0; i= delay) frameState.animate = true; } } setTimeout (function() { if (listenerKey) { try { self.renderSync(); } catch(e) { /* ok */ } } }, delay); // Launch animation listenerKey = this.on('postcompose', animate.bind(this)); try { this.renderSync(); } catch(e) { /* ok */ } listenerKey.stop = function() { delay = duration = 0; try { this.target.renderSync(); } catch(e) { /* ok */ } }; return listenerKey; } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Ordering function for ol.layer.Vector renderOrder parameter * ol.ordering.fn (options) * It will return an ordering function (f0,f1) * @namespace */ ol.ordering = {}; /** y-Ordering * @return ordering function (f0,f1) */ ol.ordering.yOrdering = function() { return function(f0,f1) { return f1.getGeometry().getExtent()[1] - f0.getGeometry().getExtent()[1] ; }; }; /** Order with a feature attribute * @param options * @param {string} options.attribute ordering attribute, default zIndex * @param {function} options.equalFn ordering function for equal values * @return ordering function (f0,f1) */ ol.ordering.zIndex = function(options) { if (!options) options = {}; var attr = options.attribute || 'zIndex'; if (options.equalFn) { return function(f0,f1) { if (f0.get(attr) == f1.get(attr)) return options.equalFn(f0,f1); else return f0.get(attr) < f1.get(attr) ? 1:-1; }; } else { return function(f0,f1) { if (f0.get(attr) == f1.get(attr)) return 0; else return f0.get(attr) < f1.get(attr) ? 1:-1; }; } }; /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Pulse a point on postcompose * @deprecated use map.animateFeature instead * @param {ol.coordinates} point to pulse * @param {ol.pulse.options} pulse options param * - projection {ol.projection||String} projection of coords * - duration {Number} animation duration in ms, default 3000 * - amplitude {Number} movement amplitude 0: none - 0.5: start at 0.5*radius of the image - 1: max, default 1 * - easing {ol.easing} easing function, default ol.easing.easeOut * - style {ol.style.Image|ol.style.Style|Array} Image to draw as markup, default red circle */ ol.Map.prototype.pulse = function(coords, options) { var listenerKey; options = options || {}; // Change to map's projection if (options.projection) { coords = ol.proj.transform(coords, options.projection, this.getView().getProjection()); } // options var start = new Date().getTime(); var duration = options.duration || 3000; var easing = options.easing || ol.easing.easeOut; var style = options.style; if (!style) style = new ol.style.Circle({ radius:30, stroke:new ol.style.Stroke({color:'red', width:2 }) }); if (style instanceof ol.style.Image) style = new ol.style.Style({ image: style }); if (!(style instanceof Array)) style = [style]; var amplitude = options.amplitude || 1; if (amplitude<0) amplitude=0; var maxRadius = options.radius || 15; if (maxRadius<0) maxRadius = 5; /* var minRadius = maxRadius - (options.amplitude || maxRadius); //options.minRadius || 0; var width = options.lineWidth || 2; var color = options.color || 'red'; console.log("pulse") */ // Animate function function animate(event) { var frameState = event.frameState; var ratio = frameState.pixelRatio; var elapsed = frameState.time - start; if (elapsed > duration) { ol.Observable.unByKey(listenerKey); } else { var elapsedRatio = elapsed / duration; var context = event.context; context.save(); context.beginPath(); var e = easing(elapsedRatio) context.globalAlpha = easing(1 - elapsedRatio); // console.log("anim") for (var i=0; i} options.colors predefined color set "classic","dark","pale","pastel","neon" / array of color string, default classic * @param {Array} options.displacement * @param {number} options.offsetX X offset in px (deprecated, use displacement) * @param {number} options.offsetY Y offset in px (deprecated, use displacement) * @param {number} options.animation step in an animation sequence [0,1] * @param {number} options.max maximum value for bar chart * @see [Statistic charts example](../../examples/style/map.style.chart.html) * @extends {ol.style.RegularShape} * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Chart = class olstyleChart extends ol.style.RegularShape { constructor(opt_options) { var options = opt_options || {}; var strokeWidth = 0; if (opt_options.stroke) strokeWidth = opt_options.stroke.getWidth(); super({ radius: options.radius + strokeWidth, fill: new ol.style.Fill({ color: [0, 0, 0] }), rotation: options.rotation, displacement: options.displacement, snapToPixel: options.snapToPixel }); this.setScale(options.scale || 1); this._stroke = options.stroke; this._radius = options.radius || 20; this._donutratio = options.donutRatio || 0.5; this._type = options.type; this._offset = [options.offsetX ? options.offsetX : 0, options.offsetY ? options.offsetY : 0]; this._animation = (typeof (options.animation) == 'number') ? { animate: true, step: options.animation } : this._animation = { animate: false, step: 1 }; this._max = options.max; this._data = options.data; if (options.colors instanceof Array) { this._colors = options.colors; } else { this._colors = ol.style.Chart.colors[options.colors]; if (!this._colors) this._colors = ol.style.Chart.colors.classic; } this.renderChart_(); } /** * Clones the style. * @return {ol.style.Chart} */ clone() { var s = new ol.style.Chart({ type: this._type, radius: this._radius, rotation: this.getRotation(), scale: this.getScale(), data: this.getData(), snapToPixel: this.getSnapToPixel ? this.getSnapToPixel() : false, stroke: this._stroke, colors: this._colors, offsetX: this._offset[0], offsetY: this._offset[1], animation: this._animation }); s.setScale(this.getScale()); s.setOpacity(this.getOpacity()); return s; } /** Get data associatied with the chart */ getData() { return this._data; } /** Set data associatied with the chart * @param {Array} */ setData(data) { this._data = data; this.renderChart_(); } /** Get symbol radius */ getRadius() { return this._radius; } /** Set symbol radius * @param {number} symbol radius * @param {number} donut ratio */ setRadius(radius, ratio) { this._radius = radius; this.donuratio_ = ratio || this.donuratio_; this.renderChart_(); } /** Set animation step * @param {false|number} false to stop animation or the step of the animation [0,1] */ setAnimation(step) { if (step === false) { if (this._animation.animate == false) return; this._animation.animate = false; } else { if (this._animation.step == step) return; this._animation.animate = true; this._animation.step = step; } this.renderChart_(); } /** @private */ renderChart_(pixelratio) { if (!pixelratio) { if (this.getPixelRatio) { pixelratio = window.devicePixelRatio; this.renderChart_(pixelratio); if (this.getPixelRatio && pixelratio !== 1) this.renderChart_(1); } else { this.renderChart_(1); } return; } var strokeStyle; var strokeWidth = 0; if (this._stroke) { strokeStyle = ol.color.asString(this._stroke.getColor()); strokeWidth = this._stroke.getWidth(); } // no atlas manager is used, create a new canvas var canvas = this.getImage(pixelratio); // draw the circle on the canvas var context = (canvas.getContext('2d')); context.save(); // reset transform context.setTransform(pixelratio, 0, 0, pixelratio, 0, 0); context.clearRect(0, 0, canvas.width, canvas.height); context.lineJoin = 'round'; var sum = 0; var i, c; for (i = 0; i < this._data.length; i++) { sum += this._data[i]; } // then move to (x, y) context.translate(0, 0); var step = this._animation.animate ? this._animation.step : 1; //console.log(this._animation.step) // Draw pie switch (this._type) { case "donut": case "pie3D": case "pie": { var a, a0 = Math.PI * (step - 1.5); c = canvas.width / 2 / pixelratio; context.strokeStyle = strokeStyle; context.lineWidth = strokeWidth; context.save(); if (this._type == "pie3D") { context.translate(0, c * 0.3); context.scale(1, 0.7); context.beginPath(); context.fillStyle = "#369"; context.arc(c, c * 1.4, this._radius * step, 0, 2 * Math.PI); context.fill(); context.stroke(); } if (this._type == "donut") { context.save(); context.beginPath(); context.rect(0, 0, 2 * c, 2 * c); context.arc(c, c, this._radius * step * this._donutratio, 0, 2 * Math.PI); context.clip("evenodd"); } for (i = 0; i < this._data.length; i++) { context.beginPath(); context.moveTo(c, c); context.fillStyle = this._colors[i % this._colors.length]; a = a0 + 2 * Math.PI * this._data[i] / sum * step; context.arc(c, c, this._radius * step, a0, a); context.closePath(); context.fill(); context.stroke(); a0 = a; } if (this._type == "donut") { context.restore(); context.beginPath(); context.strokeStyle = strokeStyle; context.lineWidth = strokeWidth; context.arc(c, c, this._radius * step * this._donutratio, Math.PI * (step - 1.5), a0); context.stroke(); } context.restore(); break; } case "bar": default: { var max = 0; if (this._max) { max = this._max; } else { for (i = 0; i < this._data.length; i++) { if (max < this._data[i]) max = this._data[i]; } } var s = Math.min(5, 2 * this._radius / this._data.length); c = canvas.width / 2 / pixelratio; var b = canvas.width / pixelratio - strokeWidth; var x, x0 = c - this._data.length * s / 2; context.strokeStyle = strokeStyle; context.lineWidth = strokeWidth; for (i = 0; i < this._data.length; i++) { context.beginPath(); context.fillStyle = this._colors[i % this._colors.length]; x = x0 + s; var h = this._data[i] / max * 2 * this._radius * step; context.rect(x0, b - h, s, h); //console.log ( x0+", "+(b-this._data[i]/max*2*this._radius)+", "+x+", "+b); context.closePath(); context.fill(); context.stroke(); x0 = x; } } } context.restore(); // Set Anchor if (!this.setDisplacement) { var anchor = this.getAnchor(); anchor[0] = c - this._offset[0]; anchor[1] = c - this._offset[1]; } } }; /** Default color set: classic, dark, pale, pastel, neon */ ol.style.Chart.colors = { "classic": ["#ffa500","blue","red","green","cyan","magenta","yellow","#0f0"], "dark": ["#960","#003","#900","#060","#099","#909","#990","#090"], "pale": ["#fd0","#369","#f64","#3b7","#880","#b5d","#666"], "pastel": ["#fb4","#79c","#f66","#7d7","#acc","#fdd","#ff9","#b9b"], "neon": ["#ff0","#0ff","#0f0","#f0f","#f00","#00f"] }; /* 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). */ /** * @classdesc * Fill style with named pattern * * @constructor * @param {any} options * @param {ol.style.Image|undefined} options.image an image pattern, image must be preloaded to draw on first call * @param {number|undefined} options.opacity opacity with image pattern, default:1 * @param {string} options.pattern pattern name (override by image option) * @param {ol.color} options.color pattern color * @param {ol.style.Fill} options.fill fill color (background) * @param {number|Array} options.offset pattern offset for hash/dot/circle/cross pattern * @param {number} options.size line size for hash/dot/circle/cross pattern * @param {number} options.spacing spacing for hash/dot/circle/cross pattern * @param {number|bool} options.angle angle for hash pattern / true for 45deg dot/circle/cross * @param {number} options.scale pattern scale * @extends {ol.style.Fill} * @api */ ol.style.FillPattern = class olstyleFillPattern extends ol.style.Fill { constructor(options) { super(); options = options || {}; var pattern; var canvas = this.canvas_ = document.createElement('canvas'); var scale = Number(options.scale) > 0 ? Number(options.scale) : 1; var ratio = scale * ol.has.DEVICE_PIXEL_RATIO || ol.has.DEVICE_PIXEL_RATIO; var ctx = canvas.getContext('2d'); if (options.image) { options.image.load(); var i; var img = options.image.getImage(); if (img.width) { canvas.width = Math.round(img.width * ratio); canvas.height = Math.round(img.height * ratio); ctx.globalAlpha = typeof (options.opacity) == 'number' ? options.opacity : 1; ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height); pattern = ctx.createPattern(canvas, 'repeat'); } else { var self = this; pattern = [0, 0, 0, 0]; img.onload = function () { canvas.width = Math.round(img.width * ratio); canvas.height = Math.round(img.height * ratio); ctx.globalAlpha = typeof (options.opacity) == 'number' ? options.opacity : 1; ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height); pattern = ctx.createPattern(canvas, 'repeat'); self.setColor(pattern); }; } } else { var pat = this.getPattern_(options); canvas.width = Math.round(pat.width * ratio); canvas.height = Math.round(pat.height * ratio); ctx.beginPath(); if (options.fill) { ctx.fillStyle = ol.color.asString(options.fill.getColor()); ctx.fillRect(0, 0, canvas.width, canvas.height); } ctx.scale(ratio, ratio); ctx.lineCap = "round"; ctx.lineWidth = pat.stroke || 1; ctx.fillStyle = ol.color.asString(options.color || "#000"); ctx.strokeStyle = ol.color.asString(options.color || "#000"); if (pat.circles) for (i = 0; i < pat.circles.length; i++) { var ci = pat.circles[i]; ctx.beginPath(); ctx.arc(ci[0], ci[1], ci[2], 0, 2 * Math.PI); if (pat.fill) ctx.fill(); if (pat.stroke) ctx.stroke(); } if (!pat.repeat) pat.repeat = [[0, 0]]; if (pat.char) { ctx.font = pat.font || (pat.width) + "px Arial"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; if (pat.angle) { ctx.fillText(pat.char, pat.width / 4, pat.height / 4); ctx.fillText(pat.char, 5 * pat.width / 4, 5 * pat.height / 4); ctx.fillText(pat.char, pat.width / 4, 5 * pat.height / 4); ctx.fillText(pat.char, 5 * pat.width / 4, pat.height / 4); ctx.fillText(pat.char, 3 * pat.width / 4, 3 * pat.height / 4); ctx.fillText(pat.char, -pat.width / 4, -pat.height / 4); ctx.fillText(pat.char, 3 * pat.width / 4, -pat.height / 4); ctx.fillText(pat.char, -pat.width / 4, 3 * pat.height / 4); } else ctx.fillText(pat.char, pat.width / 2, pat.height / 2); } if (pat.lines) for (i = 0; i < pat.lines.length; i++) for (var r = 0; r < pat.repeat.length; r++) { var li = pat.lines[i]; ctx.beginPath(); ctx.moveTo(li[0] + pat.repeat[r][0], li[1] + pat.repeat[r][1]); for (var k = 2; k < li.length; k += 2) { ctx.lineTo(li[k] + pat.repeat[r][0], li[k + 1] + pat.repeat[r][1]); } if (pat.fill) ctx.fill(); if (pat.stroke) ctx.stroke(); ctx.save(); ctx.strokeStyle = 'red'; ctx.strokeWidth = 0.1; //ctx.strokeRect(0,0,canvas.width,canvas.height); ctx.restore(); } pattern = ctx.createPattern(canvas, 'repeat'); if (options.offset) { var offset = options.offset; if (typeof (offset) == "number") offset = [offset, offset]; if (offset instanceof Array) { var dx = Math.round((offset[0] * ratio)); var dy = Math.round((offset[1] * ratio)); // New pattern ctx.scale(1 / ratio, 1 / ratio); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.translate(dx, dy); ctx.fillStyle = pattern; ctx.fillRect(-dx, -dy, canvas.width, canvas.height); pattern = ctx.createPattern(canvas, 'repeat'); } } } // Set pattern as fill color this.setColor(pattern) } /** Static fuction to add char patterns * @param {title} * @param {object} options * @param {integer} [options.size=10] default 10 * @param {integer} [options. width=10] default 10 * @param {integer} [options.height=10] default 10 * @param {Array} [options.circles] * @param {Array} [options.lines] * @param {integer} [options.stroke] * @param {bool} [options.fill] * @param {char} [option.char] * @param {string} [font="10px Arial"] */ static addPattern(title, options) { if (!options) options = {}; ol.style.FillPattern.patterns[title || options.char] = { width: options.width || options.size || 10, height: options.height || options.size || 10, font: options.font, char: options.char, circles: options.circles, lines: options.lines, repeat: options.repeat, stroke: options.stroke, angle: options.angle, fill: options.fill }; } /** * Clones the style. * @return {ol.style.FillPattern} */ clone() { var s = super.clone(); s.canvas_ = this.canvas_; return s; } /** Get canvas used as pattern * @return {canvas} */ getImage() { return this.canvas_; } /** Get pattern * @param {olx.style.FillPatternOption} */ getPattern_(options) { var pat = ol.style.FillPattern.patterns[options.pattern] || ol.style.FillPattern.patterns.dot; var d = Math.round(options.spacing) || 10; var size; switch (options.pattern) { case 'dot': // fallsthrough case 'circle': { size = options.size === 0 ? 0 : options.size / 2 || 2; if (!options.angle) { pat.width = pat.height = d; pat.circles = [[d / 2, d / 2, size]]; if (options.pattern == 'circle') { pat.circles = pat.circles.concat([ [d / 2 + d, d / 2, size], [d / 2 - d, d / 2, size], [d / 2, d / 2 + d, size], [d / 2, d / 2 - d, size], [d / 2 + d, d / 2 + d, size], [d / 2 + d, d / 2 - d, size], [d / 2 - d, d / 2 + d, size], [d / 2 - d, d / 2 - d, size] ]); } } else { d = pat.width = pat.height = Math.round(d * 1.4); pat.circles = [[d / 4, d / 4, size], [3 * d / 4, 3 * d / 4, size]]; if (options.pattern == 'circle') { pat.circles = pat.circles.concat([ [d / 4 + d, d / 4, size], [d / 4, d / 4 + d, size], [3 * d / 4 - d, 3 * d / 4, size], [3 * d / 4, 3 * d / 4 - d, size], [d / 4 + d, d / 4 + d, size], [3 * d / 4 - d, 3 * d / 4 - d, size] ]); } } break; } case 'tile': // fallsthrough case 'square': { size = options.size === 0 ? 0 : options.size / 2 || 2; if (!options.angle) { pat.width = pat.height = d; pat.lines = [[d / 2 - size, d / 2 - size, d / 2 + size, d / 2 - size, d / 2 + size, d / 2 + size, d / 2 - size, d / 2 + size, d / 2 - size, d / 2 - size]]; } else { pat.width = pat.height = d; //size *= Math.sqrt(2); pat.lines = [[d / 2 - size, d / 2, d / 2, d / 2 - size, d / 2 + size, d / 2, d / 2, d / 2 + size, d / 2 - size, d / 2]]; } if (options.pattern == 'square') pat.repeat = [[0, 0], [0, d], [d, 0], [0, -d], [-d, 0], [-d, -d], [d, d], [-d, d], [d, -d]]; break; } case 'cross': { // Limit angle to 0 | 45 if (options.angle) options.angle = 45; } // fallsthrough case 'hatch': { var a = Math.round(((options.angle || 0) - 90) % 360); if (a > 180) a -= 360; a *= Math.PI / 180; var cos = Math.cos(a); var sin = Math.sin(a); if (Math.abs(sin) < 0.0001) { pat.width = pat.height = d; pat.lines = [[0, 0.5, d, 0.5]]; pat.repeat = [[0, 0], [0, d]]; } else if (Math.abs(cos) < 0.0001) { pat.width = pat.height = d; pat.lines = [[0.5, 0, 0.5, d]]; pat.repeat = [[0, 0], [d, 0]]; if (options.pattern == 'cross') { pat.lines.push([0, 0.5, d, 0.5]); pat.repeat.push([0, d]); } } else { var w = pat.width = Math.round(Math.abs(d / sin)) || 1; var h = pat.height = Math.round(Math.abs(d / cos)) || 1; if (options.pattern == 'cross') { pat.lines = [[-w, -h, 2 * w, 2 * h], [2 * w, -h, -w, 2 * h]]; pat.repeat = [[0, 0]]; } else if (cos * sin > 0) { pat.lines = [[-w, -h, 2 * w, 2 * h]]; pat.repeat = [[0, 0], [w, 0], [0, h]]; } else { pat.lines = [[2 * w, -h, -w, 2 * h]]; pat.repeat = [[0, 0], [-w, 0], [0, h]]; } } pat.stroke = options.size === 0 ? 0 : options.size || 4; break; } default: break; } return pat; } } /** Patterns definitions * @see pattern generator http://www.imagico.de/map/jsdotpattern.php */ ol.style.FillPattern.patterns = { "hatch": { width:5, height:5, lines:[[0,2.5,5,2.5]], stroke:1 }, "cross": { width:7, height:7, lines:[[0,3,10,3],[3,0,3,10]], stroke:1 }, "dot": { width:8, height:8, circles:[[5,5,2]], stroke:false, fill:true, }, "circle": { width:10, height:10, circles:[[5,5,2]], stroke:1, fill:false, }, "square": { width:10, height:10, lines:[[3,3, 3,8, 8,8, 8,3, 3,3]], stroke:1, fill:false, }, "tile": { width:10, height:10, lines:[[3,3, 3,8, 8,8, 8,3, 3,3]], fill:true, }, "woven": { width: 12, height: 12, lines: [[ 3,3, 9,9 ],[0,12, 3,9], [9,3, 12,0], [-1,1,1,-1], [13,11,11,13]], stroke: 1 }, "crosses": { width: 8, height: 8, lines: [[ 2,2, 6,6 ],[2,6,6,2]], stroke: 1 }, "caps": { width: 8, height: 8, lines: [[ 2,6, 4,2, 6,6 ]], stroke: 1 }, "nylon": { width: 20, height: 20, // lines: [[ 0,5, 0,0, 5,0 ],[ 5,10, 10,10, 10,5 ], [ 10,15, 10,20, 15,20 ],[ 15,10, 20,10, 20,15 ]], // repeat: [[0,0], [20,0], [0,20], [-20,0], [0,-20], [-20,-20]], lines: [[ 1,6, 1,1, 6,1 ],[ 6,11, 11,11, 11,6 ], [ 11,16, 11,21, 16,21 ],[ 16,11, 21,11, 21,16 ]], repeat: [[0,0], [-20,0], [0,-20] ], stroke: 1 }, "hexagon": { width: 20, height: 12, lines: [[ 0,10, 4,4, 10,4, 14,10, 10,16, 4,16, 0,10 ]], stroke:1, repeat:[[0,0],[10,6],[10,-6],[-10,-6]] }, "cemetry": { width:15, height:19, lines:[[0,3.5,7,3.5],[3.5,0,3.5,10], //[7,12.5,14,12.5],[10.5,9,10.5,19] ], stroke:1, repeat:[[0,0],[7,9]] }, "sand": { width: 20, height: 20, circles:[ [1,2,1],[9,3,1],[2,16,1], [7,8,1],[6,14,1],[4,19,1], [14,2,1],[12,10,1],[14,18,1], [18,8,1],[18,14,1] ], fill:1 }, "conglomerate": { width: 60, height: 40, circles:[[2,4,1],[17,3,1],[26,18,1],[12,17,1],[5,17,2],[28,11,2]], lines:[[7,5, 6,7, 9,9, 11,8, 11,6, 9,5, 7,5], [16,10, 15,13, 16,14, 19,15, 21,13, 22,9, 20,8, 19,8, 16,10], [24,6, 26,7, 27,5, 26,4, 24,4, 24,6]], repeat: [[30,0], [-15,20], [15,20], [45,20]], stroke:1 }, "conglomerate2": { width:60, height:40, circles:[[2,4,1],[17,3,1],[26,18,1],[12,17,1],[5,17,2],[28,11,2]], lines:[ [7,5, 6,7, 9,9, 11,8, 11,6, 9,5, 7,5], [16,10, 15,13, 16,14, 19,15, 21,13, 22,9, 20,8, 19,8, 16,10], [24,6, 26,7, 27,5, 26,4, 24,4, 24,6] ], repeat: [[30,0], [-15,20], [15,20], [45,20]], fill:1 }, "gravel": { width:15, height:10, circles:[[4,2,1],[5,9,1],[1,7,1]],//[9,9,1],,[15,2,1]], lines:[[7,5, 6,6, 7,7, 8,7, 9,7, 10,5, 9,4, 7,5], [11,2, 14,4, 14,1, 12,1, 11,2]], stroke:1 }, "brick": { width:18, height:16, lines:[ [0,1,18,1],[0,10,18,10], [6,1,6,10],[12,10,12,18],[12,0,12,1]], stroke:1 }, "dolomite": { width:20, height:16, lines:[[0,1,20,1],[0,9,20,9],[1,9,6,1],[11,9,14,16],[14,0,14.4,1]], stroke:1 }, "coal": { width:20, height:16, lines:[[1,5, 7,1, 7,7], [11,10, 12,5, 18,9], [5,10, 2,15, 9,15,], [15,16, 15,13, 20,16], [15,0, 15,2, 20,0]], fill:1 }, "breccia": { width:20, height:16, lines:[[1,5, 7,1, 7,7, 1,5], [11,10, 12,5, 18,9, 11,10], [5,10, 2,15, 9,15, 5,10], [15,16, 15,13, 22,18], [15,0, 15,2, 20,0] ], stroke:1, }, "clay": { width:20, height:20, lines:[[0,0, 3,11, 0,20], [11,0, 10,3, 13,13, 11,20], [0,0, 10,3, 20,0], [0,12, 3,11, 13,13, 20,12]], stroke:1 }, "flooded": { width:15, height:10, lines:[ [0,1,10,1],[0,6,5,6], [10,6,15,6]], stroke:1 }, "chaos": { width:40, height:40, lines:[[40,2, 40,0, 38,0, 40,2], [4,0, 3,2, 2,5, 0,0, 0,3, 2,7, 5,6, 7,7, 8,10, 9,12, 9,13, 9,14, 8,14, 6,15, 2,15, 0,20, 0,22, 2,20, 5,19, 8,15, 10,14, 11,12.25, 10,12, 10,10, 12,9, 13,7, 12,6, 13,4, 16,7, 17,4, 20,0, 18,0, 15,3, 14,2, 14,0, 12,1, 11,0, 10,1, 11,4, 10,7, 9,8, 8,5, 6,4, 5,3, 5,1, 5,0, 4,0], [7,1, 7,3, 8,3, 8,2, 7,1], [4,3, 5,5, 4,5, 4,3], [34,5, 33,7, 38,10, 38,8, 36,5, 34,5], [ 27,0, 23,2, 21,8, 30,0, 27,0], [25,8, 26,12, 26,16, 22.71875,15.375, 20,13, 18,15, 17,18, 13,22, 17,21, 19,22, 21,20, 19,18, 22,17, 30,25, 26,26, 24,28, 21.75,33.34375, 20,36, 18,40, 20,40, 24,37, 25,32, 27,31, 26,38, 27,37, 30,32, 32,35, 36,37, 38,40, 38,39, 40,40, 37,36, 34,32, 37,31, 36,29, 33,27, 34,24, 39,21, 40,21, 40,16, 37,20, 31,22, 32,25, 27,20, 29,15, 30,20, 32,20, 34,18, 33,12, 31,11, 29,14, 26,9, 25,8], [39,24, 37,26, 40,28, 39,24], [13,15, 9,19, 14,18, 13,15], [18,23, 14,27, 16,27, 17,25, 20,26, 18,23], [6,24, 2,26, 1,28, 2,30, 5,28, 12,30, 16,32, 18,30, 15,30, 12,28, 9,25, 7,27, 6,24], [29,27, 32,28, 33,31, 30,29, 27,28, 29,27], [5,35, 1,33, 3,36, 13,38, 15,35, 10,36, 5,35]], fill:1, }, "grass": { width:27, height:22, lines: [[0,10.5,13,10.5], [2.5,10,1.5,7], [4.5,10, 4.5,5, 3.5,4 ], [7,10, 7.5,6, 8.5,3], [10,10,11,6]], repeat: [[0,0],[14,10]], stroke:1 }, "swamp": { width:24, height:23, lines:[ [0,10.5,9.5,10.5], [2.5,10,2.5,7], [4.5,10,4.5,4], [6.5,10,6.5,6], [3,12.5,7,12.5] ], repeat: [[0,0],[14,10]], stroke:1 }, "reed": { width:26, height:23, lines:[ [2.5,10,2,7], [4.5,10,4.2,4], [6.5,10,6.8,4], [8.5,10,9,6], [3.7,4,3.7,2.5], [4.7,4,4.7,2.5], [6.3,4,6.3,2.5], [7.3,4,7.3,2.5] ], circles:[ [4.2,2.5,.5], [18.2,12.5,.5], [6.8,2.5,.5], [20.8,12.5,.5], [9,6,.5], [23,16,.5] ], repeat: [[0,0],[14,10]], stroke:1 }, "wave": { width:10, height:8, lines:[ [0,0, 5,4, 10,0] ], stroke:1 }, "vine": { width:13, height:13, lines:[[3,0,3,6],[9,7,9,13]], stroke:1.0 }, "forest": { width:55, height:30, circles:[[7,7,3.5],[20,20,1.5],[42,22,3.5],[35,5,1.5]], stroke:1 }, "forest2": { width:55, height:30, circles:[[7,7,3.5],[20,20,1.5],[42,22,3.5],[35,5,1.5]], fill:1, stroke:1 }, "scrub": { width:26, height:20, lines:[ [1,4, 4,8, 6,4] ], circles:[[20,13,1.5]], stroke:1, }, "tree": { width:30, height:30, lines:[[7.78,10.61,4.95,10.61,4.95,7.78,3.54,7.78,2.12,6.36,0.71,6.36,0,4.24,0.71,2.12,4.24,0,7.78,0.71,9.19,3.54,7.78,4.95,7.07,7.07,4.95,7.78]], repeat: [[3,1],[18,16]], stroke:1 }, "tree2": { width:30, height:30, lines:[[7.78,10.61,4.95,10.61,4.95,7.78,3.54,7.78,2.12,6.36,0.71,6.36,0,4.24,0.71,2.12,4.24,0,7.78,0.71,9.19,3.54,7.78,4.95,7.07,7.07,4.95,7.78,4.95,10.61,7.78,10.61]], repeat: [[3,1],[18,16]], fill:1, stroke:1 }, "pine": { width:30, height:30, lines:[[5.66,11.31,2.83,11.31,2.83,8.49,0,8.49,2.83,0,5.66,8.49,2.83,8.49]], repeat:[[3,1],[18,16]], stroke:1 }, "pine2": { width:30, height:30, lines:[[5.66,11.31,2.83,11.31,2.83,8.49,0,8.49,2.83,0,5.66,8.49,2.83,8.49,2.83,11.31,5.66,11.31]], repeat:[[3,1],[18,16]], fill:1, stroke:1 }, "mixtree": { width:30, height:30, lines:[ [7.78,10.61,4.95,10.61,4.95,7.78,3.54,7.78,2.12,6.36,0.71,6.36,0,4.24,0.71,2.12,4.24,0,7.78,0.71,9.19,3.54,7.78,4.95,7.07,7.07,4.95,7.78,4.95,10.61,7.78,10.61], [23.66, 27.31, 20.83, 27.31, 20.83, 24.49, 18, 24.49, 20.83, 16, 23.66, 24.49, 20.83, 24.49, 20.83, 27.31, 23.66, 27.31] ], repeat: [[3,1]], stroke:1 }, "mixtree2": { width:30, height:30, lines:[ [7.78,10.61,4.95,10.61,4.95,7.78,3.54,7.78,2.12,6.36,0.71,6.36,0,4.24,0.71,2.12,4.24,0,7.78,0.71,9.19,3.54,7.78,4.95,7.07,7.07,4.95,7.78,4.95,10.61,7.78,10.61], [23.66, 27.31, 20.83, 27.31, 20.83, 24.49, 18, 24.49, 20.83, 16, 23.66, 24.49, 20.83, 24.49, 20.83, 27.31, 23.66, 27.31] ], repeat: [[3,1]], fill:1, stroke:1 }, "pines": { width:22, height:20, lines:[[1,4,3.5,1,6,4],[1,8,3.5,5,6,8],[3.5,1,3.5,11],[12,14.5,14.5,14,17,14.5],[12,18,17,18],[14.5,12,14.5,18]], repeat: [[2,1]], stroke:1 }, "rock": { width:20, height:20, lines:[ [1,0,1,9],[4,0,4,9],[7,0,7,9], [10,1,19,1],[10,4,19,4],[10,7,19,7], [0,11,9,11],[0,14,9,14],[0,17,9,17], [12,10,12,19],[15,10,15,19],[18,10,18,19] ], repeat:[[0.5,0.5]], stroke:1 }, "rocks": { width:20, height:20, lines:[ [5,0, 3,0, 5,4, 4,6, 0,3, 0,5, 3,6, 5,9, 3.75,10, 2.5,10, 0,9, 0,10, 4,11, 5,14, 4,15, 0,13, 0,13, 0,13, 0,14, 0,14, 5,16, 5,18, 3,19, 0,19, -0.25,19.9375, 5,20, 10,19, 10,20, 11,20, 12,19, 14,20, 15,20, 17,19, 20,20, 20,19, 19,16, 20,15, 20,11, 20,10, 19,8, 20,5, 20,0, 19,0, 20,2, 19,4, 17,4, 16,3, 15,0, 14,0, 15,4, 11,5, 10,4, 11,0, 10,0, 9,4, 6,5, 5,0,], [18,5, 19,6, 18,10, 16,10, 14,9, 16,5, 18,5], [5,6, 9,5, 10,6, 10,9, 6,10, 5,6], [14,5, 14,8, 13,9, 12,9, 11,7, 12,5, 14,5], [ 5,11, 8,10, 9,11, 10,14, 6,15, 6,15, 5,11], [13,10, 14,11, 15,14, 15,14, 15,14, 11,15, 10,11, 11,10, 13,10], [15,12, 16,11, 19,11, 19,15, 16,14, 16,14, 15,12], [6,16, 9,15, 10,18, 5,19, 6,16], [10,16, 14,16, 14,18, 13,19, 11,18, 10,16], [15,15, 18,16, 18,18, 16,19, 15,18, 15,15]], stroke:1 } }; /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Flow line style * Draw LineString with a variable color / width * NB: the FlowLine style doesn't impress the hit-detection. * If you want your lines to be sectionable you have to add your own style to handle this. * (with transparent line: stroke color opacity to .1 or zero width) * @constructor * @extends {ol.style.Style} * @param {Object} options * @param {boolean} options.visible draw only the visible part of the line, default true * @param {number|function} options.width Stroke width or a function that gets a feature and the position (beetween [0,1]) and returns current width * @param {number} options.width2 Final stroke width (if width is not a function) * @param {number} options.arrow Arrow at start (-1), at end (1), at both (2), none (0), default geta * @param {ol.colorLike|function} options.color Stroke color or a function that gets a feature and the position (beetween [0,1]) and returns current color * @param {ol.colorLike} options.color2 Final sroke color if color is nor a function * @param {ol.colorLike} options.arrowColor Color of arrows, if not defined used color or color2 * @param {string} options.lineCap CanvasRenderingContext2D.lineCap 'butt' | 'round' | 'square', default 'butt' * @param {number|ol.size} options.arrowSize height and width of the arrow, default 16 * @param {boolean} [options.noOverlap=false] prevent segments overlaping * @param {number} options.offset0 offset at line start * @param {number} options.offset1 offset at line end */ ol.style.FlowLine = class olstyleFlowLine extends ol.style.Style { constructor(options) { options = options || {} super({ stroke: options.stroke, text: options.text, zIndex: options.zIndex, geometry: options.geometry }) this.setRenderer(this._render.bind(this)) // Draw only visible this._visible = (options.visible !== false) // Width if (typeof options.width === 'function') { this._widthFn = options.width } else { this.setWidth(options.width) } this.setWidth2(options.width2) // Color if (typeof options.color === 'function') { this._colorFn = options.color } else { this.setColor(options.color) } this.setColor2(options.color2) // LineCap this.setLineCap(options.lineCap) // Arrow this.setArrow(options.arrow) this.setArrowSize(options.arrowSize) this.setArrowColor(options.arrowColor) // Offset this._offset = [0, 0] this.setOffset(options.offset0, 0) this.setOffset(options.offset1, 1) // Overlap this._noOverlap = options.noOverlap } /** Set the initial width * @param {number} width width, default 0 */ setWidth(width) { this._width = width || 0 } /** Set the final width * @param {number} width width, default 0 */ setWidth2(width) { this._width2 = width } /** Get offset at start or end * @param {number} where 0=start, 1=end * @return {number} width */ getOffset(where) { return this._offset[where] } /** Add an offset at start or end * @param {number} width * @param {number} where 0=start, 1=end */ setOffset(width, where) { width = Math.max(0, parseFloat(width)) switch (where) { case 0: { this._offset[0] = width break } case 1: { this._offset[1] = width break } } } /** Set the LineCap * @param {steing} cap LineCap (round or butt), default butt */ setLineCap(cap) { this._lineCap = (cap === 'round' ? 'round' : 'butt') } /** Get the current width at step * @param {ol.feature} feature * @param {number} step current drawing step beetween [0,1] * @return {number} */ getWidth(feature, step) { if (this._widthFn) return this._widthFn(feature, step) var w2 = (typeof (this._width2) === 'number') ? this._width2 : this._width return this._width + (w2 - this._width) * step } /** Set the initial color * @param {ol.colorLike} color */ setColor(color) { try { this._color = ol.color.asArray(color) } catch (e) { this._color = [0, 0, 0, 1] } } /** Set the final color * @param {ol.colorLike} color */ setColor2(color) { try { this._color2 = ol.color.asArray(color) } catch (e) { this._color2 = null } } /** Set the arrow color * @param {ol.colorLike} color */ setArrowColor(color) { try { this._acolor = ol.color.asString(color) } catch (e) { this._acolor = null } } /** Get the current color at step * @param {ol.feature} feature * @param {number} step current drawing step beetween [0,1] * @return {string} */ getColor(feature, step) { if (this._colorFn) return ol.color.asString(this._colorFn(feature, step)) var color = this._color var color2 = this._color2 || this._color return 'rgba(' + +Math.round(color[0] + (color2[0] - color[0]) * step) + ',' + Math.round(color[1] + (color2[1] - color[1]) * step) + ',' + Math.round(color[2] + (color2[2] - color[2]) * step) + ',' + (color[3] + (color2[3] - color[3]) * step) + ')' } /** Get arrow */ getArrow() { return this._arrow } /** Set arrow * @param {number} n -1 | 0 | 1 | 2, default: 0 */ setArrow(n) { this._arrow = parseInt(n) if (this._arrow < -1 || this._arrow > 2) this._arrow = 0 } /** getArrowSize * @return {ol.size} */ getArrowSize() { return this._arrowSize || [16, 16] } /** setArrowSize * @param {number|ol.size} size */ setArrowSize(size) { if (Array.isArray(size)) this._arrowSize = size else if (typeof (size) === 'number') this._arrowSize = [size, size] } /** drawArrow * @param {CanvasRenderingContext2D} ctx * @param {ol.coordinate} p0 * @param ol.coordinate} p1 * @param {number} width * @param {number} ratio pixelratio * @private */ drawArrow(ctx, p0, p1, width, ratio) { var asize = this.getArrowSize()[0] * ratio var l = ol.coordinate.dist2d(p0, p1) var dx = (p0[0] - p1[0]) / l var dy = (p0[1] - p1[1]) / l width = Math.max(this.getArrowSize()[1] / 2, width / 2) * ratio ctx.beginPath() ctx.moveTo(p0[0], p0[1]) ctx.lineTo(p0[0] - asize * dx + width * dy, p0[1] - asize * dy - width * dx) ctx.lineTo(p0[0] - asize * dx - width * dy, p0[1] - asize * dy + width * dx) ctx.lineTo(p0[0], p0[1]) ctx.fill() } /** Renderer function * @param {Array} geom The pixel coordinates of the geometry in GeoJSON notation * @param {ol.render.State} e The olx.render.State of the layer renderer */ _render(geom, e) { if (e.geometry.getType() === 'LineString') { var i, g, p, ctx = e.context // Get geometry used at drawing if (!this._visible) { var a = e.pixelRatio / e.resolution var cos = Math.cos(e.rotation) var sin = Math.sin(e.rotation) g = e.geometry.getCoordinates() var dx = geom[0][0] - g[0][0] * a * cos - g[0][1] * a * sin var dy = geom[0][1] - g[0][0] * a * sin + g[0][1] * a * cos geom = [] for (i = 0; p = g[i]; i++) { geom[i] = [ dx + p[0] * a * cos + p[1] * a * sin, dy + p[0] * a * sin - p[1] * a * cos, p[2] ] } } var asize = this.getArrowSize()[0] * e.pixelRatio ctx.save() // Offsets if (this.getOffset(0)) this._splitAsize(geom, this.getOffset(0) * e.pixelRatio) if (this.getOffset(1)) this._splitAsize(geom, this.getOffset(1) * e.pixelRatio, true) // Arrow 1 if (geom.length > 1 && (this.getArrow() === -1 || this.getArrow() === 2)) { p = this._splitAsize(geom, asize) if (this._acolor) ctx.fillStyle = this._acolor else ctx.fillStyle = this.getColor(e.feature, 0) this.drawArrow(ctx, p[0], p[1], this.getWidth(e.feature, 0), e.pixelRatio) } // Arrow 2 if (geom.length > 1 && this.getArrow() > 0) { p = this._splitAsize(geom, asize, true) if (this._acolor) ctx.fillStyle = this._acolor else ctx.fillStyle = this.getColor(e.feature, 1) this.drawArrow(ctx, p[0], p[1], this.getWidth(e.feature, 1), e.pixelRatio) } // Split into var geoms = this._splitInto(geom, 255, 2) var k = 0 var nb = geoms.length // Draw ctx.lineJoin = 'round' ctx.lineCap = this._lineCap || 'butt' if (geoms.length > 1) { for (k = 0; k < geoms.length; k++) { var step = k / nb g = geoms[k] ctx.lineWidth = this.getWidth(e.feature, step) * e.pixelRatio ctx.strokeStyle = this.getColor(e.feature, step) ctx.beginPath() ctx.moveTo(g[0][0], g[0][1]) for (i = 1; p = g[i]; i++) { ctx.lineTo(p[0], p[1]) } ctx.stroke() } } ctx.restore() } } /** Split extremity at * @param {ol.geom.LineString} geom * @param {number} asize * @param {boolean} end start=false or end=true, default false (start) */ _splitAsize(geom, asize, end) { var p, p1, p0 var dl, d = 0 if (end) p0 = geom.pop() else p0 = geom.shift() p = p0 while (geom.length) { if (end) p1 = geom.pop() else p1 = geom.shift() dl = ol.coordinate.dist2d(p, p1) if (d + dl > asize) { p = [p[0] + (p1[0] - p[0]) * (asize - d) / dl, p[1] + (p1[1] - p[1]) * (asize - d) / dl] dl = ol.coordinate.dist2d(p, p0) if (end) { geom.push(p1) geom.push(p) geom.push([p[0] + (p0[0] - p[0]) / dl, p[1] + (p0[1] - p[1]) / dl]) } else { geom.unshift(p1) geom.unshift(p) geom.unshift([p[0] + (p0[0] - p[0]) / dl, p[1] + (p0[1] - p[1]) / dl]) } break } d += dl p = p1 } return [p0, p] } /** Split line geometry into equal length geometries * @param {Array} geom * @param {number} nb number of resulting geometries, default 255 * @param {number} nim minimum length of the resulting geometries, default 1 */ _splitInto(geom, nb, min) { var i, p var dt = this._noOverlap ? 1 : .9 // Split geom into equal length geoms var geoms = [] var dl, l = 0 for (i = 1; p = geom[i]; i++) { l += ol.coordinate.dist2d(geom[i - 1], p) } var length = Math.max(min || 2, l / (nb || 255)) var p0 = geom[0] l = 0 var g = [p0] i = 1 p = geom[1] while (i < geom.length) { var dx = p[0] - p0[0] var dy = p[1] - p0[1] dl = Math.sqrt(dx * dx + dy * dy) if (l + dl > length) { var d = (length - l) / dl g.push([ p0[0] + dx * d, p0[1] + dy * d ]) geoms.push(g) p0 = [ p0[0] + dx * d * dt, p0[1] + dy * d * dt ] g = [p0] l = 0 } else { l += dl p0 = p g.push(p0) i++ p = geom[i] } } geoms.push(g) return geoms } } /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * @classdesc * A marker style to use with font symbols. * * @constructor * @param {} options Options. * @param {string} [options.color] default #000 * @param {string} options.glyph the glyph name or a char to display as symbol. * The name must be added using the {@link ol.style.FontSymbol.addDefs} function. * @param {string} [options.text] a text to display as a glyph * @param {string} [options.font] font to use with the text option * @param {string} options.form * none|circle|poi|bubble|marker|coma|shield|blazon|bookmark|hexagon|diamond|triangle|sign|ban|lozenge|square * a form that will enclose the glyph, default none * @param {number} options.radius * @param {number} options.rotation * @param {boolean} options.rotateWithView * @param {number} [options.opacity=1] * @param {number} [options.fontSize=1] size of the font compare to the radius, fontSize greater than 1 will exceed the symbol extent * @param {string} [options.fontStyle] the font style (bold, italic, bold italic, etc), default none * @param {boolean} options.gradient true to display a gradient on the symbol * @param {Array} [options.displacement] to use with ol > 6 * @param {number} [options.offsetX=0] Horizontal offset in pixels, deprecated use displacement with ol>6 * @param {number} [options.offsetY=0] Vertical offset in pixels, deprecated use displacement with ol>6 * @param {_ol_style_Fill_} options.fill * @param {_ol_style_Stroke_} options.stroke * @extends {ol.style.RegularShape} * @implements {ol.structs.IHasChecksum} * @api */ ol.style.FontSymbol = class olstyleFontSymbol extends ol.style.RegularShape { constructor(options) { options = options || {}; var strokeWidth = 0; if (options.stroke) { strokeWidth = options.stroke.getWidth(); } if (!options.displacement) { options.displacement = [options.offsetX || 0, -options.offsetY || 0]; } super ({ radius: options.radius, fill: options.fill, rotation: options.rotation, displacement: options.displacement, rotateWithView: options.rotateWithView }); if (typeof (options.opacity) == "number") this.setOpacity(options.opacity); this._color = options.color; this._fontSize = options.fontSize || 1; this._fontStyle = options.fontStyle || ''; this._stroke = options.stroke; this._fill = options.fill; this._radius = options.radius - strokeWidth; this._form = options.form || "none"; this._gradient = options.gradient; this._offset = [options.offsetX ? options.offsetX : 0, options.offsetY ? options.offsetY : 0]; if (options.glyph) this._glyph = this.getGlyph(options.glyph); else this._glyph = this.getTextGlyph(options.text || '', options.font); if (!this.getDisplacement) this.getImage(); } /** Static function : add new font defs * @param {String|Object} font the font name or a description ({ font: font_name, name: font_name, copyright: '', prefix }) * @param {Object} glyphs a key / value list of glyph definitions. * Each key is the name of the glyph, * the value is an object that code the font, the caracter code, * the name and a search string for the glyph. * { char: the char, code: the char code (if no char), theme: a theme for search puposes, name: the symbol name, search: a search string (separated with ',') } */ static addDefs(font, glyphs) { var thefont = font; if (typeof (font) == 'string') { thefont = { font: font, name: font, copyright: '' }; } if (!thefont.font || typeof (thefont.font) !== 'string') { console.log('bad font def'); return; } var fontname = thefont.font; ol.style.FontSymbol.defs.fonts[fontname] = thefont; for (var i in glyphs) { var g = glyphs[i]; if (typeof (g) === 'string' && (g.length == 1 || g.length == 2)) { g = { char: g }; } ol.style.FontSymbol.defs.glyphs[i] = { font: thefont.font, char: g.char || '' + String.fromCharCode(g.code) || '', theme: g.theme || thefont.name, name: g.name || i, search: g.search || '' }; } } /** Clones the style. * @return {ol.style.FontSymbol} */ clone() { var g = new ol.style.FontSymbol({ //glyph: this._glyph, text: this._glyph.char, font: this._glyph.font, color: this._color, fontSize: this._fontSize, fontStyle: this._fontStyle, stroke: this._stroke, fill: this._fill, radius: this._radius + (this._stroke ? this._stroke.getWidth() : 0), form: this._form, gradient: this._gradient, offsetX: this._offset[0], offsetY: this._offset[1], opacity: this.getOpacity(), rotation: this.getRotation(), rotateWithView: this.getRotateWithView() }); g.setScale(this.getScale()); return g; } /** Get the fill style for the symbol. * @return {ol.style.Fill} Fill style. * @api */ getFill() { return this._fill; } /** Get the stroke style for the symbol. * @return {_ol_style_Stroke_} Stroke style. * @api */ getStroke() { return this._stroke; } /** Get the glyph definition for the symbol. * @param {string|undefined} name a glyph name to get the definition, default return the glyph definition for the style. * @return {*} * @api */ getGlyph(name) { if (name) return ol.style.FontSymbol.defs.glyphs[name] || { font: 'sans-serif', char: name.charAt(0), theme: 'none', name: 'none', search: '' }; else return this._glyph; } /** Get glyph definition given a text and a font * @param {string|undefined} text * @param {string} [font] the font for the text * @return {*} * @api */ getTextGlyph(text, font) { return { font: font || 'sans-serif', char: String(text), theme: 'none', name: 'none', search: '' }; } /** * Get the glyph name. * @return {string} the name * @api */ getGlyphName() { for (var i in ol.style.FontSymbol.defs.glyphs) { if (ol.style.FontSymbol.defs.glyphs[i] === this._glyph) return i; } return ''; } /** * Get the stroke style for the symbol. * @return {_ol_style_Stroke_} Stroke style. * @api */ getFontInfo(glyph) { return ol.style.FontSymbol.defs.fonts[glyph.font]; } /** * Get the image icon. * @param {number} pixelRatio Pixel ratio. * @return {HTMLCanvasElement} Image or Canvas element. * @api */ getImage(pixelratio) { pixelratio = pixelratio || 1; // get canvas var canvas = super.getImage(pixelratio); var strokeStyle; var strokeWidth = 0; if (this._stroke) { strokeStyle = ol.color.asString(this._stroke.getColor()); strokeWidth = this._stroke.getWidth(); } /** @type {ol.style.FontSymbol.RenderOptions} */ var renderOptions = { strokeStyle: strokeStyle, strokeWidth: strokeWidth, size: canvas.width / pixelratio, }; // draw the circle on the canvas var context = (canvas.getContext('2d')); context.clearRect(0, 0, canvas.width, canvas.height); this.drawMarker_(renderOptions, context, 0, 0, pixelratio); // Set anchor / displacement if (!this.getDisplacement) { var a = this.getAnchor(); a[0] = canvas.width / 2 - this._offset[0]; a[1] = canvas.width / 2 - this._offset[1]; } return canvas; } /** * @private * @param {ol.style.FontSymbol.RenderOptions} renderOptions * @param {CanvasRenderingContext2D} context */ drawPath_(renderOptions, context) { var s = 2 * this._radius + renderOptions.strokeWidth; var w = renderOptions.strokeWidth / 2; var c = renderOptions.size / 2; // Transfo to place the glyph at the right place var transfo = { fac: 1, posX: renderOptions.size / 2, posY: renderOptions.size / 2 }; context.lineJoin = 'round'; context.lineCap = 'round'; context.beginPath(); // Draw the path with the form switch (this._form) { case "none": { transfo.fac = 1; break; } case "circle": case "ban": { context.arc(c, c, s / 2, 0, 2 * Math.PI, true); break; } case "poi": { context.arc(c, c - 0.4 * this._radius, 0.6 * this._radius, 0.15 * Math.PI, 0.85 * Math.PI, true); context.lineTo(c - 0.89 * 0.05 * s, (0.95 + 0.45 * 0.05) * s + w); context.arc(c, 0.95 * s + w, 0.05 * s, 0.85 * Math.PI, 0.15 * Math.PI, true); transfo = { fac: 0.45, posX: c, posY: c - 0.35 * this._radius }; break; } case "bubble": { context.arc(c, c - 0.2 * this._radius, 0.8 * this._radius, 0.4 * Math.PI, 0.6 * Math.PI, true); context.lineTo(0.5 * s + w, s + w); transfo = { fac: 0.7, posX: c, posY: c - 0.2 * this._radius }; break; } case "marker": { context.arc(c, c - 0.2 * this._radius, 0.8 * this._radius, 0.25 * Math.PI, 0.75 * Math.PI, true); context.lineTo(0.5 * s + w, s + w); transfo = { fac: 0.7, posX: c, posY: c - 0.2 * this._radius }; break; } case "coma": { context.moveTo(c + 0.8 * this._radius, c - 0.2 * this._radius); context.quadraticCurveTo(0.95 * s + w, 0.75 * s + w, 0.5 * s + w, s + w); context.arc(c, c - 0.2 * this._radius, 0.8 * this._radius, 0.45 * Math.PI, 0, false); transfo = { fac: 0.7, posX: c, posY: c - 0.2 * this._radius }; break; } default: { var pts; switch (this._form) { case "shield": { pts = [0.05, 0, 0.95, 0, 0.95, 0.8, 0.5, 1, 0.05, 0.8, 0.05, 0]; transfo.posY = 0.45 * s + w; break; } case "blazon": { pts = [0.1, 0, 0.9, 0, 0.9, 0.8, 0.6, 0.8, 0.5, 1, 0.4, 0.8, 0.1, 0.8, 0.1, 0]; transfo.fac = 0.8; transfo.posY = 0.4 * s + w; break; } case "bookmark": { pts = [0.05, 0, 0.95, 0, 0.95, 1, 0.5, 0.8, 0.05, 1, 0.05, 0]; transfo.fac = 0.9; transfo.posY = 0.4 * s + w; break; } case "hexagon": { pts = [0.05, 0.2, 0.5, 0, 0.95, 0.2, 0.95, 0.8, 0.5, 1, 0.05, 0.8, 0.05, 0.2]; transfo.fac = 0.9; transfo.posY = 0.5 * s + w; break; } case "diamond": { pts = [0.25, 0, 0.75, 0, 1, 0.2, 1, 0.4, 0.5, 1, 0, 0.4, 0, 0.2, 0.25, 0]; transfo.fac = 0.75; transfo.posY = 0.35 * s + w; break; } case "triangle": { pts = [0, 0, 1, 0, 0.5, 1, 0, 0]; transfo.fac = 0.6; transfo.posY = 0.3 * s + w; break; } case "sign": { pts = [0.5, 0.05, 1, 0.95, 0, 0.95, 0.5, 0.05]; transfo.fac = 0.7; transfo.posY = 0.65 * s + w; break; } case "lozenge": { pts = [0.5, 0, 1, 0.5, 0.5, 1, 0, 0.5, 0.5, 0]; transfo.fac = 0.7; break; } case "square": default: { pts = [0, 0, 1, 0, 1, 1, 0, 1, 0, 0]; break; } } for (var i = 0; i < pts.length; i += 2) context.lineTo(pts[i] * s + w, pts[i + 1] * s + w); } } context.closePath(); return transfo; } /** * @private * @param {ol.style.FontSymbol.RenderOptions} renderOptions * @param {CanvasRenderingContext2D} context * @param {number} x The origin for the symbol (x). * @param {number} y The origin for the symbol (y). */ drawMarker_(renderOptions, context, x, y, pixelratio) { var fcolor = this._fill ? this._fill.getColor() : "#000"; var scolor = this._stroke ? this._stroke.getColor() : "#000"; if (this._form == "none" && this._stroke && this._fill) { scolor = this._fill.getColor(); fcolor = this._stroke.getColor(); } // reset transform context.setTransform(pixelratio, 0, 0, pixelratio, 0, 0); // then move to (x, y) context.translate(x, y); var tr = this.drawPath_(renderOptions, context, pixelratio); if (this._fill) { if (this._gradient && this._form != "none") { var grd = context.createLinearGradient(0, 0, renderOptions.size / 2, renderOptions.size); grd.addColorStop(1, ol.color.asString(fcolor)); grd.addColorStop(0, ol.color.asString(scolor)); context.fillStyle = grd; } else { context.fillStyle = ol.color.asString(fcolor); } context.fill(); } if (this._stroke && renderOptions.strokeWidth) { context.strokeStyle = renderOptions.strokeStyle; context.lineWidth = renderOptions.strokeWidth; context.stroke(); } // Draw the symbol if (this._glyph.char) { context.font = this._fontStyle + ' ' + (2 * tr.fac * (this._radius) * this._fontSize) + "px " + this._glyph.font; context.strokeStyle = context.fillStyle; context.lineWidth = renderOptions.strokeWidth * (this._form == "none" ? 2 : 1); context.fillStyle = ol.color.asString(this._color || scolor); context.textAlign = "center"; context.textBaseline = "middle"; var t = this._glyph.char; if (renderOptions.strokeWidth && scolor != "transparent") context.strokeText(t, tr.posX, tr.posY); context.fillText(t, tr.posX, tr.posY); } if (this._form == "ban" && this._stroke && renderOptions.strokeWidth) { context.strokeStyle = renderOptions.strokeStyle; context.lineWidth = renderOptions.strokeWidth; var r = this._radius + renderOptions.strokeWidth; var d = this._radius * Math.cos(Math.PI / 4); context.moveTo(r + d, r - d); context.lineTo(r - d, r + d); context.stroke(); } } /** * @inheritDoc */ getChecksum() { var strokeChecksum = (this._stroke !== null) ? this._stroke.getChecksum() : '-'; var fillChecksum = (this._fill !== null) ? this._fill.getChecksum() : '-'; var recalculate = (this.checksums_ === null) || (strokeChecksum != this.checksums_[1] || fillChecksum != this.checksums_[2] || this._radius != this.checksums_[3] || this._form + "-" + this.glyphs_ != this.checksums_[4] ); if (recalculate) { var checksum = 'c' + strokeChecksum + fillChecksum + ((this._radius !== void 0) ? this._radius.toString() : '-') + this._form + "-" + this.glyphs_; this.checksums_ = [checksum, strokeChecksum, fillChecksum, this._radius, this._form + "-" + this.glyphs_]; } return this.checksums_[0]; } } /** Font defs */ ol.style.FontSymbol.defs = { 'fonts':{}, 'glyphs':{} }; /* Cool stuff to get the image symbol for a style * @param {number} ratio pixelratio * / ol.style.Image.prototype.getImagePNG = function(ratio) { ratio = ratio || window.devicePixelRatio; var canvas = this.getImage(ratio); if (canvas) { try { return canvas.toDataURL('image/png'); } catch(e) { return false; } } else { return false; } } /* */ /* Copyright (c) 2015 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). * * Photo style for vector features */ /** * @classdesc * Set Photo style for vector features. * * @constructor * @param {} options * @param { default | square | circle | anchored | folio } options.kind * @param {boolean} options.crop crop within square, default is false * @param {Number} options.radius symbol size * @param {boolean} options.shadow drop a shadow * @param {ol.style.Stroke} options.stroke * @param {String} options.src image src * @param {String} options.crossOrigin The crossOrigin attribute for loaded images. Note that you must provide a crossOrigin value if you want to access pixel data with the Canvas renderer. * @param {Array} [options.displacement] to use with ol > 6 * @param {number} [options.offsetX=0] Horizontal offset in pixels, deprecated use displacement with ol>6 * @param {number} [options.offsetY=0] Vertical offset in pixels, deprecated use displacement with ol>6 * @param {function} [options.onload] callback when image is loaded (to redraw the layer) * @param {function} [options.onerror] callback when image is on error (not loaded) * @extends {ol.style.RegularShape} * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Photo = class olstylePhoto extends ol.style.RegularShape { constructor(options) { options = options || {} if (!options.displacement) options.displacement = [options.offsetX || 0, -options.offsetY || 0] var sanchor = (options.kind === "anchored" ? 8 : 0) var shadow = (Number(options.shadow) || 0) if (!options.stroke) { options.stroke = new ol.style.Stroke({ width: 0, color: "#000" }) } var strokeWidth = options.stroke.getWidth() if (strokeWidth < 0) strokeWidth = 0; if (options.kind == 'folio') strokeWidth += 6; options.stroke.setWidth(strokeWidth) super({ radius: options.radius + strokeWidth + sanchor / 2 + shadow / 2, points: 0, displacement: [options.displacement[0] || 0, (options.displacement[1] || 0) + sanchor], // No fill to create a hit detection Image (v5) or transparent (v6) fill: ol.style.RegularShape.prototype.render ? new ol.style.Fill({ color: [0, 0, 0, 0] }) : null }) this.sanchor_ = sanchor; this._shadow = shadow; // Hack to get the hit detection Image (v4.6.5 ?) if (!this.getHitDetectionImage) { var img = super.getImage.call(this) if (!this.hitDetectionCanvas_) { for (var i in this) { if (this[i] && this[i].getContext && this[i] !== img) { this.hitDetectionCanvas_ = this[i] break } } } // Clone canvas for hit detection (old versions) this.hitDetectionCanvas_ = document.createElement('canvas') this.hitDetectionCanvas_.width = img.width this.hitDetectionCanvas_.height = img.height var hit = this.hitDetectionCanvas_ this.getHitDetectionImage = function () { return hit } } this._stroke = options.stroke this._fill = options.fill this._crop = options.crop this._crossOrigin = options.crossOrigin this._kind = options.kind || "default" this._radius = options.radius this._src = options.src this._offset = [options.offsetX ? options.offsetX : 0, options.offsetY ? options.offsetY : 0] this._onload = options.onload this._onerror = options.onerror if (typeof (options.opacity) == 'number') this.setOpacity(options.opacity) if (typeof (options.rotation) == 'number') this.setRotation(options.rotation) // Calculate image this.getImage() } /** Set photo offset * @param {ol.pixel} offset */ setOffset(offset) { this._offset = [offset[0] || 0, offset[1] || 0] this.getImage() } /** * Clones the style. * @return {ol.style.Photo} */ clone() { var i = new ol.style.Photo({ stroke: this._stroke, fill: this._fill, shadow: this._shadow, crop: this._crop, crossOrigin: this._crossOrigin, kind: this._kind, radius: this._radius, src: this._src, offsetX: this._offset[0], offsetY: this._offset[1], opacity: this.getOpacity(), rotation: this.getRotation() }) i.getImage() return i } /** * Draw the form without the image * @private */ drawBack_(context, color, strokeWidth, pixelratio) { var shadow = this._shadow var canvas = context.canvas context.beginPath() context.fillStyle = color context.clearRect(0, 0, canvas.width, canvas.height) var width = canvas.width / pixelratio var height = canvas.height / pixelratio switch (this._kind) { case 'square': { context.rect(0, 0, width - shadow, height - shadow) break } case 'circle': { context.arc(this._radius + strokeWidth, this._radius + strokeWidth, this._radius + strokeWidth, 0, 2 * Math.PI, false) break } case 'folio': { var offset = 6 strokeWidth -= offset context.strokeStyle = 'rgba(0,0,0,0.5)' context.lineWidth = 1 var w = width - shadow - 2 * offset var a = Math.atan(6 / w) context.save() context.rotate(-a) context.translate(-6, 2) context.beginPath() context.rect(offset, offset, w, w) context.stroke() context.fill() context.restore() context.save() context.translate(6, -1) context.rotate(a) context.beginPath() context.rect(offset, offset, w, w) context.stroke() context.fill() context.restore() context.beginPath() context.rect(offset, offset, w, w) context.stroke() break } case 'anchored': { context.roundRect(this.sanchor_ / 2, 0, width - this.sanchor_ - shadow, height - this.sanchor_ - shadow, strokeWidth) context.moveTo(width / 2 - this.sanchor_ - shadow / 2, height - this.sanchor_ - shadow) context.lineTo(width / 2 + this.sanchor_ - shadow / 2, height - this.sanchor_ - shadow) context.lineTo(width / 2 - shadow / 2, height - shadow); break } default: { // roundrect context.roundRect(0, 0, width - shadow, height - shadow, strokeWidth) break } } context.closePath() } /** * Get the image icon. * @param {number} pixelRatio Pixel ratio. * @return {HTMLCanvasElement} Image or Canvas element. * @api */ getImage(pixelratio) { pixelratio = pixelratio || window.devicePixelRatio; var canvas = ol.style.RegularShape.prototype.getImage.call(this, pixelratio) if ((this._gethit || this.img_) && this._currentRatio === pixelratio) return canvas; // Calculate image at pixel ratio this._currentRatio = pixelratio; var strokeStyle var strokeWidth = 0 if (this._stroke) { strokeStyle = ol.color.asString(this._stroke.getColor()) strokeWidth = this._stroke.getWidth() } // Draw hitdetection image this._gethit = true var context = this.getHitDetectionImage().getContext('2d') context.save() context.setTransform(1, 0, 0, 1, 0, 0) this.drawBack_(context, "#000", strokeWidth, 1) context.fill() context.restore() this._gethit = false // Draw the image context = canvas.getContext('2d') context.save() context.setTransform(pixelratio, 0, 0, pixelratio, 0, 0) this.drawBack_(context, strokeStyle, strokeWidth, pixelratio) // Draw a shadow if (this._shadow) { context.shadowColor = 'rgba(0,0,0,0.5)' context.shadowBlur = pixelratio * this._shadow / 2 context.shadowOffsetX = pixelratio * this._shadow / 2 context.shadowOffsetY = pixelratio * this._shadow / 2 } context.fill() context.restore() var self = this var img = this.img_ = new Image() if (this._crossOrigin) img.crossOrigin = this._crossOrigin img.src = this._src // Draw image if (img.width) { self.drawImage_(canvas, img, pixelratio) } else { img.onload = function () { self.drawImage_(canvas, img, pixelratio) // Force change (?!) // self.setScale(1); if (self._onload) self._onload() } if (self._onerror) { img.onerror = function () { self._onerror() } } } // Set anchor (ol < 6) if (!this.getDisplacement) { var a = this.getAnchor() a[0] = (canvas.width / pixelratio - this._shadow) / 2 - this._offset[0] if (this.sanchor_) { a[1] = canvas.height / pixelratio - this._shadow - this._offset[1] } else { a[1] = (canvas.height / pixelratio - this._shadow) / 2 - this._offset[1] } } return canvas } /** Returns the photo image * @returns {HTMLImageElement} */ getPhoto() { return this.img_ } /** * Draw an timage when loaded * @private */ drawImage_(canvas, img, pixelratio) { // Remove the circle on the canvas var context = (canvas.getContext('2d')) var strokeWidth = 0 if (this._stroke) strokeWidth = this._stroke.getWidth() var size = 2 * this._radius context.save() if (ol.style.RegularShape.prototype.render) context.setTransform(pixelratio, 0, 0, pixelratio, 0, 0) if (this._kind == 'circle') { context.beginPath() context.arc(this._radius + strokeWidth, this._radius + strokeWidth, this._radius, 0, 2 * Math.PI, false) context.clip() } var s, x, y, w, h, sx, sy, sw, sh // Crop the image to a square vignette if (this._crop) { s = Math.min(img.width / size, img.height / size) sw = sh = s * size sx = (img.width - sw) / 2 sy = (img.height - sh) / 2 x = y = 0 w = h = size + 1 } else { // Fit the image to the size s = Math.min(size / img.width, size / img.height) sx = sy = 0 sw = img.width sh = img.height w = s * sw h = s * sh x = (size - w) / 2 y = (size - h) / 2 } x += strokeWidth + this.sanchor_ / 2 y += strokeWidth context.drawImage(img, sx, sy, sw, sh, x, y, w, h) // Draw a circle to avoid aliasing on clip if (this._kind == 'circle' && strokeWidth) { context.beginPath() context.strokeStyle = ol.color.asString(this._stroke.getColor()) context.lineWidth = strokeWidth / 4 context.arc(this._radius + strokeWidth, this._radius + strokeWidth, this._radius, 0, 2 * Math.PI, false) context.stroke() } context.restore() } } /** * Draws a rounded rectangle using the current state of the canvas. * Draw a rectangle if the radius is null. * @param {Number} x The top left x coordinate * @param {Number} y The top left y coordinate * @param {Number} width The width of the rectangle * @param {Number} height The height of the rectangle * @param {Number} radius The corner radius. */ CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { if (!r) { this.rect(x,y,w,h); } else { if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; this.beginPath(); this.moveTo(x+r, y); this.arcTo(x+w, y, x+w, y+h, r); this.arcTo(x+w, y+h, x, y+h, r); this.arcTo(x, y+h, x, y, r); this.arcTo(x, y, x+w, y, r); this.closePath(); } return this; }; /* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** Profile style * Draw a profile on the map * @extends {ol.style.Style} * @constructor * @param {Object} options * @param {ol.style.Stroke} options.stroke * @param {ol.style.Fill} options.fill * @param {number} options.scale z scale * @param {number} options.zIndex * @param {ol.geom.Geometry} options.geometry */ ol.style.Profile = class olstyleProfile extends ol.style.Style { constructor(options) { options = options || {} super({ zIndex: options.zIndex, geometry: options.geometry }) this.setRenderer(this._render.bind(this)) this.setStroke(options.stroke) this.setFill(options.fill) this.setScale(options.scale) } /** Set style stroke * @param {ol.style.Stroke} */ setStroke(stroke) { this._stroke = stroke || new ol.style.Stroke({ color: '#fff', width: 1 }) } /** Get style stroke * @return {ol.style.Stroke} */ getStroke() { return this._stroke } /** Set style stroke * @param {ol.style.Fill} */ setFill(fill) { this._fill = fill || new ol.style.Fill({ color: 'rgba(255,255,255,.3' }) } /** Get style stroke * @return {ol.style.Fill} */ getFill() { return this._fill } /** Set z scale * @param {number} */ setScale(sc) { this._scale = sc || .2 } /** Get z scale * @return {number} */ getScale() { return this._scale } /** Renderer function * @param {Array} geom The pixel coordinates of the geometry in GeoJSON notation * @param {ol.render.State} e The olx.render.State of the layer renderer */ _render(geom, e) { if (!/Z/.test(e.feature.getGeometry().getLayout())) return var g = e.geometry.getCoordinates() switch (e.geometry.getType()) { case 'LineString': { this._renderLine(geom, g, e.feature.getGeometry(), e) break } case 'MultiLineString': { e.feature.getGeometry().getLineStrings().forEach(function (l, i) { this._renderLine(geom[i], g[i], l, e) }.bind(this)) break } case 'Point': { break } } } /** @private */ _renderLine(geom, g, l, e) { var i, p, ctx = e.context var cos = Math.cos(e.rotation) var sin = Math.sin(e.rotation) // var a = e.pixelRatio / e.resolution; var a = ol.coordinate.dist2d(geom[0], geom[1]) / ol.coordinate.dist2d(g[0], g[1]) var dx = geom[0][0] - g[0][0] * a * cos - g[0][1] * a * sin var dy = geom[0][1] - g[0][0] * a * sin + g[0][1] * a * cos geom = l.getCoordinates() var dz = Infinity for (i = 0; p = geom[i]; i++) { var x = dx + p[0] * a * cos + p[1] * a * sin var y = dy + p[0] * a * sin - p[1] * a * cos dz = Math.min(dz, p[2]) geom[i] = [x, y, p[2]] } ctx.save() ctx.fillStyle = ol.color.asString(this.getFill().getColor()) ctx.strokeStyle = ol.color.asString(this.getStroke().getColor()) ctx.lineWidth = this.getStroke().getWidth() var p0 = geom[0] var ez = this.getScale() * e.pixelRatio for (i = 1; p = geom[i]; i++) { ctx.beginPath() ctx.moveTo(p0[0], p0[1]) ctx.lineTo(p[0], p[1]) ctx.lineTo(p[0], p[1] - (p[2] - dz) * ez) ctx.lineTo(p0[0], p0[1] - (p0[2] - dz) * ez) ctx.lineTo(p0[0], p0[1]) ctx.fill() p0 = p } p0 = geom[0] ctx.beginPath() ctx.moveTo(p0[0], p0[1] - (p0[2] - dz) * ez) for (i = 1; p = geom[i]; i++) { ctx.lineTo(p[0], p[1] - (p[2] - dz) * ez) } ctx.stroke() ctx.restore() } } /** Add a setTextPath style to draw text along linestrings @toto letterpadding/spacing, wordpadding/spacing */ ;(function() { /** Internal drawing function called on postcompose * @param {ol.eventPoscompose} e postcompose event */ function drawTextPath (e) { // Prent drawing at large resolution if (e.frameState.viewState.resolution > this.textPathMaxResolution_) return; var extent = e.frameState.extent; var c2p = e.frameState.coordinateToPixelTransform; // Get pixel path with coordinates var k; function getPath(c, readable) { var path1 = []; for (k=0; kpath1[path1.length-2]) { var path2 = []; for (k=path1.length-2; k>=0; k-=2) { path2.push(path1[k]); path2.push(path1[k+1]); } return path2; } else return path1; } var ctx = e.context; ctx.save(); ctx.scale(e.frameState.pixelRatio,e.frameState.pixelRatio); var features = this.getSource().getFeaturesInExtent(extent); for (var i=0, f; f=features[i]; i++) { { var style = this.textPathStyle_(f,e.frameState.viewState.resolution); for (var s,j=0; s=style[j]; j++) { var g = s.getGeometry() || f.getGeometry(); var c; switch (g.getType()) { case "LineString": c = g.getCoordinates(); break; case "MultiLineString": c = g.getLineString(0).getCoordinates(); break; default: continue; } var st = s.getText(); var path = getPath(c, st.getRotateWithView() ); ctx.font = st.getFont(); ctx.textBaseline = st.getTextBaseline(); ctx.textAlign = st.getTextAlign(); ctx.lineWidth = st.getStroke() ? (st.getStroke().getWidth()||0) : 0; ctx.strokeStyle = st.getStroke() ? (st.getStroke().getColor()||"#fff") : "#fff"; ctx.fillStyle = st.getFill() ? st.getFill().getColor()||"#000" : "#000"; // New params ctx.textJustify = st.getTextAlign()=="justify"; ctx.textOverflow = st.getTextOverflow ? st.getTextOverflow():""; ctx.minWidth = st.getMinWidth ? st.getMinWidth():0; // Draw textpath ctx.textPath(st.getText()||f.get("name"), path); } } } ctx.restore(); } /** Set the style for features. * This can be a single style object, an array of styles, or a function that takes a feature and resolution and * returns an array of styles. If it is undefined the default style is used. * If it is null the layer has no style (a null style). * See ol.style for information on the default style. * @deprecated use ol/style/Text with placement:line * @param {ol.style.Style|Array.|ol.StyleFunction} style * @param {Number} maxResolution to display text, default: 0 */ ol.layer.Vector.prototype.setTextPathStyle = function(style, maxResolution) { // Remove existing style if (style===null) { if (this.textPath_) this.unByKey(this.textPath_); this.textPath_ = null; this.changed(); return; } // New postcompose if (!this.textPath_) { this.textPath_ = this.on(['postcompose','postrender'], drawTextPath.bind(this)); } // Set textPathStyle if (style===undefined) { style = [ new ol.style.Style({ text: new ol.style.Text()}) ]; } if (typeof(style) == "function") this.textPathStyle_ = style; else this.textPathStyle_ = function() { return style; }; this.textPathMaxResolution_ = Number(maxResolution) || Number.MAX_VALUE; // Force redraw this.changed(); } /** Add new properties to ol.style.Text * to use with ol.layer.Vector.prototype.setTextPathStyle * @constructor * @param {} options * @param {visible|ellipsis|string} textOverflow * @param {number} minWidth minimum width (px) to draw text, default 0 */ ol.style.TextPath = function(options) { if (!options) options={}; ol.style.Text.call (this, options); this.textOverflow_ = typeof(options.textOverflow)!="undefined" ? options.textOverflow : "visible"; this.minWidth_ = options.minWidth || 0; } ol.ext.inherits(ol.style.TextPath, ol.style.Text); ol.style.TextPath.prototype.getTextOverflow = function() { return this.textOverflow_; }; ol.style.TextPath.prototype.getMinWidth = function() { return this.minWidth_; }; /**/ })(); /** CanvasRenderingContext2D: draw text along path * @param {string} text * @param {Array} path */ CanvasRenderingContext2D.prototype.textPath = function (text, path) { var ctx = this; function dist2D(x1,y1,x2,y2) { var dx = x2-x1; var dy = y2-y1; return Math.sqrt(dx*dx+dy*dy); } var di, dpos=0; var pos=2; function getPoint(path, dl) { if (!di || dpos+didl) break; pos += 2; if (pos>=path.length) break; dpos += di; } } var x, y, a, dt = dl-dpos; if (pos>=path.length) { pos = path.length-2; } if (!dt) { x = path[pos-2]; y = path[pos-1]; a = Math.atan2(path[pos+1]-path[pos-1], path[pos]-path[pos-2]); } else { x = path[pos-2]+ (path[pos]-path[pos-2])*dt/di; y = path[pos-1]+(path[pos+1]-path[pos-1])*dt/di; a = Math.atan2(path[pos+1]-path[pos-1], path[pos]-path[pos-2]); } return [x,y,a]; } var letterPadding = ctx.measureText(" ").width *0.25; var start = 0; var d = 0; for (var i=2; i} [options.displacement] to use with ol > 6 * @param {number} [options.offsetX=0] Horizontal offset in pixels, deprecated use displacement with ol>6 * @param {number} [options.offsetY=0] Vertical offset in pixels, deprecated use displacement with ol>6 * @extends {ol.style.RegularShape} * @api */ ol.style.Shadow = class olstyleShadow extends ol.style.RegularShape { constructor(options) { options = options || {}; super({ radius: options.radius, fill: options.fill, displacement: options.displacement }); this._fill = options.fill || new ol.style.Fill({ color: "rgba(0,0,0,0.5)" }); this._radius = options.radius; this._blur = options.blur === 0 ? 0 : options.blur || options.radius / 3; this._offset = [options.offsetX ? options.offsetX : 0, options.offsetY ? options.offsetY : 0]; if (!options.displacement) options.displacement = [options.offsetX || 0, -options.offsetY || 0]; // ol < 6 if (!this.setDisplacement) this.getImage(); } /** * Clones the style. * @return {ol.style.Shadow} */ clone() { var s = new ol.style.Shadow({ fill: this._fill, radius: this._radius, blur: this._blur, offsetX: this._offset[0], offsetY: this._offset[1] }); s.setScale(this.getScale()); s.setOpacity(this.getOpacity()); return s; } /** * Get the image icon. * @param {number} pixelRatio Pixel ratio. * @return {HTMLCanvasElement} Image or Canvas element. * @api */ getImage(pixelratio) { pixelratio = pixelratio || 1; var radius = this._radius; var canvas = super.getImage(pixelratio); // Remove the circle on the canvas var context = (canvas.getContext('2d')); context.save(); context.resetTransform(); context.clearRect(0, 0, canvas.width, canvas.height); context.beginPath(); context.setTransform(pixelratio, 0, 0, pixelratio, 0, 0); context.scale(1, 0.5); context.arc(radius, -radius, radius - this._blur, 0, 2 * Math.PI, false); context.fillStyle = '#000'; context.shadowColor = this._fill.getColor(); context.shadowBlur = 0.7 * this._blur * pixelratio; context.shadowOffsetX = 0; context.shadowOffsetY = 1.5 * radius * pixelratio; context.closePath(); context.fill(); context.shadowColor = 'transparent'; context.restore(); // Set anchor if (!this.getDisplacement) { var a = this.getAnchor(); a[0] = canvas.width / 2 - this._offset[0]; a[1] = canvas.height / 2 - this._offset[1]; } return canvas; } } /* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * @classdesc * Stroke style with named pattern * * @constructor * @param {any} options * @param {ol.style.Image|undefined} options.image an image pattern, image must be preloaded to draw on first call * @param {number|undefined} options.opacity opacity with image pattern, default:1 * @param {string} options.pattern pattern name (override by image option) * @param {ol.colorLike} options.color pattern color * @param {ol.style.Fill} options.fill fill color (background) * @param {number|Array} options.offset pattern offset for hash/dot/circle/cross pattern * @param {number} options.size line size for hash/dot/circle/cross pattern * @param {number} options.spacing spacing for hash/dot/circle/cross pattern * @param {number|bool} options.angle angle for hash pattern / true for 45deg dot/circle/cross * @param {number} options.scale pattern scale * @extends {ol.style.Fill} * @implements {ol.structs.IHasChecksum} * @api */ ol.style.StrokePattern = class olstyleStrokePattern extends ol.style.Stroke { constructor(options) { super(options) options = options || {} var pattern, i; var canvas = this.canvas_ = document.createElement('canvas') var scale = Number(options.scale) > 0 ? Number(options.scale) : 1 var ratio = scale * ol.has.DEVICE_PIXEL_RATIO || ol.has.DEVICE_PIXEL_RATIO var ctx = canvas.getContext('2d') if (options.image) { options.image.load() var img = options.image.getImage() if (img.width) { canvas.width = Math.round(img.width * ratio) canvas.height = Math.round(img.height * ratio) ctx.globalAlpha = typeof (options.opacity) == 'number' ? options.opacity : 1 ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height) pattern = ctx.createPattern(canvas, 'repeat') } else { var self = this pattern = [0, 0, 0, 0] img.onload = function () { canvas.width = Math.round(img.width * ratio) canvas.height = Math.round(img.height * ratio) ctx.globalAlpha = typeof (options.opacity) == 'number' ? options.opacity : 1 ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height) pattern = ctx.createPattern(canvas, 'repeat') self.setColor(pattern) } } } else { var pat = this.getPattern_(options) canvas.width = Math.round(pat.width * ratio) canvas.height = Math.round(pat.height * ratio) ctx.beginPath() if (options.fill) { ctx.fillStyle = ol.color.asString(options.fill.getColor()) ctx.fillRect(0, 0, canvas.width, canvas.height) } ctx.scale(ratio, ratio) ctx.lineCap = "round" ctx.lineWidth = pat.stroke || 1 ctx.fillStyle = ol.color.asString(options.color || "#000") ctx.strokeStyle = ol.color.asString(options.color || "#000") if (pat.circles) for (i = 0; i < pat.circles.length; i++) { var ci = pat.circles[i] ctx.beginPath() ctx.arc(ci[0], ci[1], ci[2], 0, 2 * Math.PI) if (pat.fill) ctx.fill() if (pat.stroke) ctx.stroke() } if (!pat.repeat) pat.repeat = [[0, 0]] if (pat.char) { ctx.font = pat.font || (pat.width) + "px Arial" ctx.textAlign = 'center' ctx.textBaseline = 'middle' if (pat.angle) { ctx.fillText(pat.char, pat.width / 4, pat.height / 4) ctx.fillText(pat.char, 5 * pat.width / 4, 5 * pat.height / 4) ctx.fillText(pat.char, pat.width / 4, 5 * pat.height / 4) ctx.fillText(pat.char, 5 * pat.width / 4, pat.height / 4) ctx.fillText(pat.char, 3 * pat.width / 4, 3 * pat.height / 4) ctx.fillText(pat.char, -pat.width / 4, -pat.height / 4) ctx.fillText(pat.char, 3 * pat.width / 4, -pat.height / 4) ctx.fillText(pat.char, -pat.width / 4, 3 * pat.height / 4) } else ctx.fillText(pat.char, pat.width / 2, pat.height / 2) } if (pat.lines) for (i = 0; i < pat.lines.length; i++) for (var r = 0; r < pat.repeat.length; r++) { var li = pat.lines[i] ctx.beginPath() ctx.moveTo(li[0] + pat.repeat[r][0], li[1] + pat.repeat[r][1]) for (var k = 2; k < li.length; k += 2) { ctx.lineTo(li[k] + pat.repeat[r][0], li[k + 1] + pat.repeat[r][1]) } if (pat.fill) ctx.fill() if (pat.stroke) ctx.stroke() ctx.save() ctx.strokeStyle = 'red' ctx.strokeWidth = 0.1 //ctx.strokeRect(0,0,canvas.width,canvas.height); ctx.restore() } pattern = ctx.createPattern(canvas, 'repeat') if (options.offset) { var offset = options.offset if (typeof (offset) == "number") offset = [offset, offset] if (offset instanceof Array) { var dx = Math.round((offset[0] * ratio)) var dy = Math.round((offset[1] * ratio)) // New pattern ctx.scale(1 / ratio, 1 / ratio) ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.translate(dx, dy) ctx.fillStyle = pattern ctx.fillRect(-dx, -dy, canvas.width, canvas.height) pattern = ctx.createPattern(canvas, 'repeat') } } } this.setColor (pattern); } /** * Clones the style. * @return {ol.style.StrokePattern} */ clone() { var s = super.clone() s.canvas_ = this.canvas_ return s } /** Get canvas used as pattern * @return {canvas} */ getImage() { return this.canvas_ } /** Get pattern * @param {olx.style.FillPatternOption} */ getPattern_(options) { var pat = ol.style.FillPattern.patterns[options.pattern] || ol.style.FillPattern.patterns.dot var d = Math.round(options.spacing) || 10 var size // var d2 = Math.round(d/2)+0.5; switch (options.pattern) { case 'dot': case 'circle': { size = options.size === 0 ? 0 : options.size / 2 || 2 if (!options.angle) { pat.width = pat.height = d pat.circles = [[d / 2, d / 2, size]] if (options.pattern == 'circle') { pat.circles = pat.circles.concat([ [d / 2 + d, d / 2, size], [d / 2 - d, d / 2, size], [d / 2, d / 2 + d, size], [d / 2, d / 2 - d, size], [d / 2 + d, d / 2 + d, size], [d / 2 + d, d / 2 - d, size], [d / 2 - d, d / 2 + d, size], [d / 2 - d, d / 2 - d, size] ]) } } else { d = pat.width = pat.height = Math.round(d * 1.4) pat.circles = [[d / 4, d / 4, size], [3 * d / 4, 3 * d / 4, size]] if (options.pattern == 'circle') { pat.circles = pat.circles.concat([ [d / 4 + d, d / 4, size], [d / 4, d / 4 + d, size], [3 * d / 4 - d, 3 * d / 4, size], [3 * d / 4, 3 * d / 4 - d, size], [d / 4 + d, d / 4 + d, size], [3 * d / 4 - d, 3 * d / 4 - d, size] ]) } } break } case 'tile': case 'square': { size = options.size === 0 ? 0 : options.size / 2 || 2 if (!options.angle) { pat.width = pat.height = d pat.lines = [[d / 2 - size, d / 2 - size, d / 2 + size, d / 2 - size, d / 2 + size, d / 2 + size, d / 2 - size, d / 2 + size, d / 2 - size, d / 2 - size]] } else { pat.width = pat.height = d //size *= Math.sqrt(2); pat.lines = [[d / 2 - size, d / 2, d / 2, d / 2 - size, d / 2 + size, d / 2, d / 2, d / 2 + size, d / 2 - size, d / 2]] } if (options.pattern == 'square') pat.repeat = [[0, 0], [0, d], [d, 0], [0, -d], [-d, 0], [-d, -d], [d, d], [-d, d], [d, -d]] break } case 'cross': { // Limit angle to 0 | 45 if (options.angle) options.angle = 45 } // fallthrough case 'hatch': { var a = Math.round(((options.angle || 0) - 90) % 360) if (a > 180) a -= 360 a *= Math.PI / 180 var cos = Math.cos(a) var sin = Math.sin(a) if (Math.abs(sin) < 0.0001) { pat.width = pat.height = d pat.lines = [[0, 0.5, d, 0.5]] pat.repeat = [[0, 0], [0, d]] } else if (Math.abs(cos) < 0.0001) { pat.width = pat.height = d pat.lines = [[0.5, 0, 0.5, d]] pat.repeat = [[0, 0], [d, 0]] if (options.pattern == 'cross') { pat.lines.push([0, 0.5, d, 0.5]) pat.repeat.push([0, d]) } } else { var w = pat.width = Math.round(Math.abs(d / sin)) || 1 var h = pat.height = Math.round(Math.abs(d / cos)) || 1 if (options.pattern == 'cross') { pat.lines = [[-w, -h, 2 * w, 2 * h], [2 * w, -h, -w, 2 * h]] pat.repeat = [[0, 0]] } else if (cos * sin > 0) { pat.lines = [[-w, -h, 2 * w, 2 * h]] pat.repeat = [[0, 0], [w, 0], [0, h]] } else { pat.lines = [[2 * w, -h, -w, 2 * h]] pat.repeat = [[0, 0], [-w, 0], [0, h]] } } pat.stroke = options.size === 0 ? 0 : options.size || 4 break } default: { break } } return pat } } ol.style.Style.defaultStyle; (function() { // Style var white = [255, 255, 255, 1]; var blue = [0, 153, 255, 1]; var width = 3; var defaultEditStyle = [ new ol.style.Style({ stroke: new ol.style.Stroke({ color: white, width: width + 2 }) }), new ol.style.Style({ image: new ol.style.Circle({ radius: width * 2, fill: new ol.style.Fill({ color: blue }), stroke: new ol.style.Stroke({ color: white, width: width / 2 }) }), stroke: new ol.style.Stroke({ color: blue, width: width }), fill: new ol.style.Fill({ color: [255, 255, 255, 0.5] }) }) ]; /** * Get the default style * @param {boolean|*} [edit] true to get editing style or a { color, fillColor } object, default get default blue style * @return {Array} */ ol.style.Style.defaultStyle = function(edit) { if (edit===true) { return defaultEditStyle; } else { edit = edit || {}; var fill = new ol.style.Fill({ color: edit.fillColor || 'rgba(255,255,255,0.4)' }); var stroke = new ol.style.Stroke({ color: edit.color || '#3399CC', width: 1.25 }); var style = new ol.style.Style({ image: new ol.style.Circle({ fill: fill, stroke: stroke, radius: 5 }), fill: fill, stroke: stroke }); return [ style ]; } }; })(); /* * Copyright (c) 2015 Jean-Marc VIGLINO, * released under the CeCILL-B license (French BSD license) * (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ /** * Get a style for Geoportail WFS features * * @param {String} options.typeName * @param {any} options * @param {boolean|number} options.sens true show flow direction or a max resolution to show it, default false * @param {boolean} options.vert 'vert' road section (troncon_de_route) style, default false * @param {boolean} options.symbol show symbol on buildings (batiment), default false * @return {Array} */ ol.style.geoportailStyle; (function(){ var cache = {}; var styleCount = 0; // Troncon de route function troncon_de_route(options) { // Get color according to road properties var getColor = function (feature) { if (options.vert && feature.get('itineraire_vert')) { if (feature.get('position_par_rapport_au_sol') < 0) return [0, 128, 0, .7]; else if (feature.get('position_par_rapport_au_sol') > 0) return [0, 100, 0, 1]; else return [0, 128, 0, 1]; } if (!feature.get('importance')) return "magenta"; if (feature.get('nature') === 'Piste cyclable') { return [27,177,27,.5] } if (feature.get('position_par_rapport_au_sol') != "0") { var col; switch(feature.get('importance')) { case "1": col = [177, 27, 177, 1]; break; case "2": col = [177, 27, 27, 1]; break; case "3": col = [217, 119, 0, 1]; break; case "4": col = [255, 225, 0, 1]; break; case "5": col = [204, 204, 204, 1]; break; default: col = [211, 211, 211, 1]; break; } if (feature.get('position_par_rapport_au_sol') < 0) col[3] = .7; return col; } else { switch(feature.get('importance')) { case "1": return [255,0,255,1]; case "2": return [255,0,0,1]; case "3": return [255, 165, 0, 1]; case "4": return [255,255,0,1]; case "5": return [255,255,255,1]; default: return [211, 211, 211, 1]; } } // return "#808080"; } // Get Width var getWidth = function (feature) { return Math.max ( feature.get('largeur_de_chaussee')||2 , 2 ); } // Zindex var getZindex = function (feature) { if (!feature.get('position_par_rapport_au_sol')) return 100; var pos = Number(feature.get('position_par_rapport_au_sol')); if (pos>0) return 10 + pos*10 - (Number(feature.get('importance')) || 10); else if (pos<0) return Math.max(4 + pos, 0); else return 10 - (Number(feature.get('importance')) || 10); // return 0; } // Get rotation on the center of the line var lrot = function (geom) { //if (sens != options.direct && sens != options.inverse) return 0; var geo = geom.getCoordinates(); var x, y, dl=0, l = geom.getLength(); for (var i=0; i=l/2) break; } return -Math.atan2(y,x); } // Sens circulation var getSens = function (feature) { if (options.sens && !/double|sans/i.test(feature.get('sens_de_circulation'))) { return new ol.style.Text({ text: (feature.get('sens_de_circulation') == 'Sens direct' ? '→' : '←'), font: 'bold 12px sans-serif', placement: 'point', textAlign: 'center', fill: new ol.style.Fill({ color: [0,0,0,.3] }), stroke: new ol.style.Stroke({ color: [0,0,0,.3], width: 1.5 }), rotateWithView: true }) } return null; } var getDash = function(feature) { switch (feature.get('nature')) { case 'Escalier': { return [1,4] } case 'Sentier': { return [8,10] } } } var styleId = 'ROUT-'+(styleCount++)+'-' return function (feature, res) { var useSens = (options.sens === true || res < options.sens); var id = styleId + feature.get('nature') + '-' + feature.get('position_par_rapport_au_sol') + '-' + (useSens ? feature.get('sens_de_circulation') : 'Sans objet') + '-' + feature.get('position_par_rapport_au_sol') + '-' + feature.get('importance') + '-' + feature.get('largeur_de_chaussee') + '-' + feature.get('itineraire_vert'); var style = cache[id]; if (!style) { style = cache[id] = [ new ol.style.Style ({ text: useSens ? getSens(feature) : null, stroke: new ol.style.Stroke({ color: getColor(feature), width: getWidth(feature), lineDash: getDash(feature) }), zIndex: getZindex(feature)-100 }) ]; } // Rotation if (style[0].getText()) style[0].getText().setRotation(lrot(feature.getGeometry())); return style; }; } /** Style for batiments */ function batiment(options) { var getBatiColor = function (feature) { switch (feature.get('nature')) { case "Industriel, agricole ou commercial": return [51, 102, 153,1]; case "Remarquable": return [0,192,0,1]; default: switch ( feature.get('usage_1') ) { case 'Résidentiel': case 'Indifférencié': return [128,128,128,1]; case 'Industriel': case 'Commercial et services': return [51, 102, 153,1]; case "Sportif": return [51,153,102,1]; case "Religieux": return [153,102,51,1]; default: return [153,51,51,1]; } } } var getSymbol = function (feature) { switch ( feature.get('usage_1') ) { case "Commercial et services": return "\uf217"; case "Sportif": return "\uf1e3"; default: return null; } } var styleId = 'BATI-'+(styleCount++)+'-' return function (feature) { if (feature.get('detruit')) return []; var id = styleId + feature.get('usage_1') + '-' + feature.get('nature') + '-' + feature.get('etat_de_l_objet'); var style = cache[id]; if (!style) { var col = getBatiColor(feature); var colfill = [col[0], col[1], col[1], .5] var projet = !/en service/i.test(feature.get('etat_de_l_objet')); if (projet) colfill[3] = .1; var symbol = (options.symbol ? getSymbol(feature): null); return [ new ol.style.Style({ text: symbol ? new ol.style.Text({ text: symbol, font: '12px FontAwesome', fill: new ol.style.Fill({ color: [0,0,0, .6] //col }) }) : null, fill: new ol.style.Fill({ color: colfill }), stroke: new ol.style.Stroke ({ color: col, width: 1.5, lineDash: projet ? [5,5] : null }) }) ] } return style } } // Parcelle / cadastre function parcelle(options) { var style = new ol.style.Style({ text: new ol.style.Text({ text: '0000', font: 'bold 12px sans-serif', fill: new ol.style.Fill({ color: [100, 0, 255, 1] }), stroke: new ol.style.Stroke ({ color: [255,255,255, .8], width: 3 }) }), stroke: new ol.style.Stroke ({ color: [255, 165, 0, 1], width: 1.5 }), fill: new ol.style.Fill({ color: [100, 0, 255, .1] }) }) return function(feature, resolution) { if (resolution < .8) style.getText().setFont('bold 12px sans-serif'); else style.getText().setFont('bold 10px sans-serif'); if (options.section) { style.getText().setText(feature.get('section') +'-'+ (feature.get('numero')||'').replace(/^0*/,'')); } else { style.getText().setText((feature.get('numero')||'').replace(/^0*/,'')); } return style; } } // Corine Land Cover Style var clcColors = { 111: { color: [230,0,77,255], title: 'Continuous urban fabric'}, 112: { color: [255,0,0,255], title: 'Discontinuous urban fabric'}, 121: { color: [204,77,242,255], title: 'Industrial or commercial units'}, 122: { color: [204,0,0,255], title: 'Road and rail networks and associated land'}, 123: { color: [230,204,204,255], title: 'Port areas'}, 124: { color: [230,204,230,255], title: 'Airports'}, 131: { color: [166,0,204,255], title: 'Mineral extraction sites'}, 132: { color: [166,77,0,255], title: 'Dump sites'}, 133: { color: [255,77,255,255], title: 'Construction sites'}, 141: { color: [255,166,255,255], title: 'Green urban areas'}, 142: { color: [255,230,255,255], title: 'Sport and leisure facilities'}, 211: { color: [255,255,168,255], title: 'Non-irrigated arable land'}, 212: { color: [255,255,0,255], title: 'Permanently irrigated land'}, 213: { color: [230,230,0,255], title: 'Rice fields'}, 221: { color: [230,128,0,255], title: 'Vineyards'}, 222: { color: [242,166,77,255], title: 'Fruit trees and berry plantations'}, 223: { color: [230,166,0,255], title: 'Olive groves'}, 231: { color: [230,230,77,255], title: 'Pastures'}, 241: { color: [255,230,166,255], title: 'Annual crops associated with permanent crops'}, 242: { color: [255,230,77,255], title: 'Complex cultivation patterns'}, 243: { color: [230,204,77,255], title: 'Land principally occupied by agriculture with significant areas of natural vegetation'}, 244: { color: [242,204,166,255], title: 'Agro-forestry areas'}, 311: { color: [128,255,0,255], title: 'Broad-leaved forest'}, 312: { color: [0,166,0,255], title: 'Coniferous forest'}, 313: { color: [77,255,0,255], title: 'Mixed forest'}, 321: { color: [204,242,77,255], title: 'Natural grasslands'}, 322: { color: [166,255,128,255], title: 'Moors and heathland'}, 323: { color: [166,230,77,255], title: 'Sclerophyllous vegetation'}, 324: { color: [166,242,0,255], title: 'Transitional woodland-shrub'}, 331: { color: [230,230,230,255], title: 'Beaches dunes sands'}, 332: { color: [204,204,204,255], title: 'Bare rocks'}, 333: { color: [204,255,204,255], title: 'Sparsely vegetated areas'}, 334: { color: [0,0,0,255], title: 'Burnt areas'}, 335: { color: [166,230,204,255], title: 'Glaciers and perpetual snow'}, 411: { color: [166,166,255,255], title: 'Inland marshes'}, 412: { color: [77,77,255,255], title: 'Peat bogs'}, 421: { color: [204,204,255,255], title: 'Salt marshes'}, 422: { color: [230,230,255,255], title: 'Salines'}, 423: { color: [166,166,230,255], title: 'Intertidal flats'}, 511: { color: [0,204,242,255], title: 'Water courses'}, 512: { color: [128,242,230,255], title: 'Water bodies'}, 521: { color: [0,255,166,255], title: 'Coastal lagoons'}, 522: { color: [166,255,230,255], title: 'Estuaries'}, 523: { color: [230,242,255,255], title: 'Sea and ocean'}, }; function corineLandCover (options) { return function(feature) { var code = feature.get('code_'+options.date); var style = cache['CLC-'+code]; if (!style) { var color = clcColors[code].color.slice(); color[3] = options.opacity || 1; style = cache['CLC-'+code] = new ol.style.Style({ fill: new ol.style.Fill({ color: color || [255,255,255,.5] }) }) } return style; } } /** Get ol style for an IGN WFS layer * @param {string} typeName * @param {Object} options */ ol.style.geoportailStyle = function(typeName, options) { options = options || {}; switch (typeName) { // Troncons de route case 'BDTOPO_V3:troncon_de_route': return troncon_de_route(options); // Bati case 'BDTOPO_V3:batiment': return batiment(options); // Parcelles case 'CADASTRALPARCELS.PARCELLAIRE_EXPRESS:parcelle': return parcelle(options); default: { // CLC if (/LANDCOVER/.test(typeName)) { options.date = typeName.replace(/[^\d]*(\d*).*/,'$1'); return (corineLandCover(options)); } else { // Default style console.warn('[ol/style/geoportailStyle] no style defined for type: ' + typeName) return ol.style.Style.defaultStyle(); } } } }; /** List of clc colors */ ol.style.geoportailStyle.clcColors = JSON.parse(JSON.stringify(clcColors)); })();