UNPKG

ol-ext

Version:

A set of cool extensions for OpenLayers (ol) in node modules structure

1,538 lines (1,527 loc) 1.61 MB
/** * 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.34 * @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<ol.ext.SVGOperation>} 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<lines.length; i++) { if (getLine(lines[i]) === false) { // Stop condition if (progress) progress(this.chunk_ / this.fileSize_, true); return; } } if (progress) progress(this.chunk_ / this.fileSize_, false); // Red next chunk if (!this.nexChunk_() && progress) { // EOF progress(1, true); } }.bind(this), progress); }; /** Read a set of line chunk from the stream * @param {function} getLines a function that gets lines read as an Array<String>. * @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<viewTourDestinations>|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<number>} 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<number>} hsl as h:[0,360], s:[0,100], l:[0,100] * @param {number} [round=1000] * @returns {Array<number>} 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<number>} 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<number>} hsl as h:[0,360], s:[0,100], l:[0,100] * @param {number} [round=1000] * @returns {Array<number>} 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.toLowerCase()); 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<string>} 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<string>} 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); }; /** Set cursor * @param {Element|ol/Map} elt * @param {string} cursor */ ol.ext.element.setCursor = function(elt, cursor) { if (elt instanceof ol.Map) elt = elt.getTargetElement() // prevent flashing on mobile device if (!('ontouchstart' in window) && elt instanceof Element) { elt.style.cursor = cursor; } } /** 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 && ol.ext.olVersion < 700 && 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 (anchor && 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<ol.color.Color>} 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]) { pixel