Agriculture-front-end/public/dist/ol-ext.js
2023-07-04 10:08:33 +08:00

42606 lines
1.4 MiB
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<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);
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);
};
/** 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<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]) {
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<width; x++) for(var y=0; y<height; y++) {
var top = getIndexForCoordinates(x,Math.max(0,y-1))
var left = getIndexForCoordinates(Math.max(0,x-1),y);
var right = getIndexForCoordinates(Math.min(width-1,x+1),y);
var bottom = getIndexForCoordinates(x,Math.min(height,y+1))
// get slope values
var topValue = data[top];
var leftValue = data[left];
var rightValue = data[right];
var bottomValue = data[bottom];
var slx = (rightValue - leftValue)/3;
var sly = ( bottomValue - topValue )/3;
var sl0 = Math.sqrt( slx*slx + sly*sly );
// get aspect
var phi = Math.acos( slx/sl0 );
if ( sl0 == 0 ) {
phi = 0;
}
var azimuth = 0;
if ( slx > 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<data.length; i++) {
var p = getPixelColor(data[i]);
pixels[4*i] = p[0];
pixels[4*i+1] = p[1];
pixels[4*i+2] = p[2];
pixels[4*i+3] = p[3];
}
ctx.putImageData(imgData, 0, 0);
tile.setImage(canvas);
},
function () {
tile.setState(3);
}
)
};
};
/** Convert elevation to pixel as terrain-RGB
* Encode elevation data in raster tiles
* - max deep watter trench min > -12000 m
* - 2 digits (0.01 m)
* @param {number} height elevation
* @returns {Array<number>} 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<number>} 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<Array<number>>}
*/
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<number>}
*/
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<Array<number>>}
*/
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<Array<number>>}
*/
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<Array<number>>} 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<Array<number>>} mx1
* @param {Array<Array<number>>} mx2
* @return {Array<Array<number>>}
*/
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<Array<number>>} tx transform matrix
* @param {Array<Array<number>>} origin transform origin
* @return {Array<Array<number>>}
*/
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<Array<number>>} 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<number>} vert
* @return {Array<number>}
*/
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<Array<number>>} m matrix to transform
* @return {Array<Array<number>>}
*/
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<number> }
*/
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<number>}
*/
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<number>} 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<Object>} 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<number>} [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<number>} [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.style.Style> | 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.style.Style> | 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<list.length; l++) {
var li = list[l].cloneNode();
li.innerHTML = list[l].innerHTML;
table.appendChild(li);
nb++;
}
offsetY += r.getHeight();
}
} else {
if (r instanceof ol.legend.Image) {
// Title
if (r.get('title')) {
table.appendChild(this._title.getElement([width, height], function (b) {
this.dispatchEvent({
type: 'select',
index: -1,
symbol: b,
item: this._title
})
}.bind(this)))
ctx.font = r.get('textStyle') ? r.get('textStyle').getFont() : this._titleStyle.getFont()
if (/\bcenter\b/.test(r.get('className'))) {
ctx.textAlign = 'center'
this._drawText(ctx, r.get('title'), canvas.width / ratio / 2, offsetY + height / 2)
} else {
this._drawText(ctx, r.get('title'), margin, offsetY + height / 2)
}
offsetY += height;
}
// Image
var img = r.getImage()
try {
ctx.drawImage(img, 0,0,img.naturalWidth, img.naturalHeight, 0, offsetY * ratio, r.getWidth() * ratio, r.getHeight() * ratio)
} catch(e) { /* ok */ }
offsetY += r.getHeight();
} else {
var item = r.getProperties()
var h = item.height || height;
ctx.textAlign = 'left'
if (item.feature || item.typeGeom) {
canvas = this.getLegendImage(item, canvas, offsetY)
ctx.font = r.get('textStyle') ? r.get('textStyle').getFont() : this._textStyle.getFont()
this._drawText(ctx, r.get('title'), width + margin, offsetY + h / 2)
} else {
ctx.font = r.get('textStyle') ? r.get('textStyle').getFont() : this._titleStyle.getFont()
if (/\bcenter\b/.test(item.className)) {
ctx.textAlign = 'center'
this._drawText(ctx, r.get('title'), canvas.width / ratio / 2, offsetY + h / 2)
} else {
this._drawText(ctx, r.get('title'), margin, offsetY + h / 2)
}
}
offsetY += h;
}
table.appendChild(r.getElement([width, height], function (b) {
this.dispatchEvent({
type: 'select',
index: i,
symbol: b,
item: r
})
}.bind(this)))
nb++;
}
}.bind(this))
this.set('items', nb, true)
this.dispatchEvent({
type: 'items',
nb: nb
})
// Done
if (!opt_silent) {
this.dispatchEvent({
type: 'refresh',
width: width,
height: (this._items.length + 1) * height
})
}
}
/** Calculate the legend height
* @return {number}
*/
getHeight() {
// default item height
var margin = this.get('margin')
var hitem = this.get('lineHeight') || this.get('size')[1] + 2 * margin
var height = this.getTitle() ? hitem : 0;
this._items.forEach(function (r) {
if (r instanceof ol.legend.Legend) {
if (!r._layer || r._layer.getVisible()) {
height += r.getHeight()
}
} else if (r instanceof ol.legend.Image) {
if (r.get('title')) height += hitem;
height += r.getHeight()
} else {
if (r.get('height')) height += r.get('height') + 2 * margin;
else height += hitem
}
})
return height
}
/** Calculate the legend height
* @return {number}
*/
getWidth() {
var canvas = this.getCanvas()
var ctx = canvas.getContext('2d')
var margin = this.get('margin');
var width = this.get('size')[0] + 2 * margin
ctx.font = this._titleStyle.getFont()
var textWidth = this._measureText(ctx, this.getTitle('title')).width
this._items.forEach(function (r) {
if (r instanceof ol.legend.Legend) {
if (!r._layer || r._layer.getVisible()) {
textWidth = Math.max(textWidth, r.getWidth())
}
} else if (r instanceof ol.legend.Image) {
textWidth = Math.max(textWidth, r.getWidth())
if (r.get('title')) {
ctx.font = r.get('textStyle') ? r.get('textStyle').getFont() : this._titleStyle.getFont()
textWidth = Math.max(textWidth, this._measureText(ctx, r.get('title')).width)
}
} else {
if (r.get('feature') || r.get('typeGeom')) {
ctx.font = r.get('textStyle') ? r.get('textStyle').getFont() : this._textStyle.getFont()
textWidth = Math.max(textWidth, this._measureText(ctx, r.get('title')).width + width)
} else {
ctx.font = r.get('textStyle') ? r.get('textStyle').getFont() : this._titleStyle.getFont()
textWidth = Math.max(textWidth, this._measureText(ctx, r.get('title')).width)
}
}
}.bind(this))
return textWidth + 2 * margin
}
/** Get the image for a style
* @param {olLegendItemOptions} item
* @param {Canvas|undefined} canvas a canvas to draw in, if none create one
* @param {int|undefined} offsetY Y offset to draw in canvas, default 0
* @return {CanvasElement}
*/
getLegendImage(options, canvas, offsetY) {
options = options || {};
var size = this.get('size');
return ol.legend.Legend.getLegendImage({
className: options.className,
feature: options.feature,
typeGeom: options.typeGeom,
style: options.style || this._style,
properties: options.properties,
margin: options.margin || this.get('margin'),
size: [ options.width || size[0], options.height || size[1]],
lineHeight: options.lineHeight || this.get('lineHeight'),
onload: function() {
// Force refresh
this.refresh();
}.bind(this)
}, canvas, offsetY);
}
}
/** A class for legend image
* @constructor
* @fires changed
* @param {Object} options
* @param {string} url
* @param {string} [title]
* @param {HTMLImageElement|HTMLCanvasElement} [img] an image to display
* @param {string} [src] legend image url (if no img option)
* @param {string} [className] 'center' to center the title
* @param {number} [width] legend width, default use the image width
*/
ol.legend.Image = class ollegendImage extends ol.Object {
constructor(options) {
options = options || {};
super(options);
this.set('width', options.width);
// The image
this._img = options.img || new Image()
this._img.onload = function () {
this.changed()
}.bind(this);
// Get source
if (!options.img) {
this._img.src = options.src;
}
}
/** Set the legend title
* @param {string} title
*/
setTitle(title) {
this.set('title', title || '');
this.changed();
}
/** Set the item width
* @param {number} [width] legend width, default use the image width
*/
setWidth(width) {
this.set('width', width || null);
this.changed();
}
/** Get image width
* @return {number}
*/
getWidth() {
if (!this._img.naturalWidth) return 0;
return this.get('width') || this._img.naturalWidth
}
/** Get image height
* @return {number}
*/
getHeight() {
if (!this._img.naturalWidth) return 0;
if (this.get('width')) {
return this.get('width') * this._img.naturalHeight / this._img.naturalWidth
}
return this._img.naturalHeight || 0
}
/** Get Image
* @returns {Image}
*/
getImage() {
return this._img
}
/** Get element
* @param {ol.size} size symbol size
* @param {function} onclick
*/
getElement(size, onclick) {
if (this.get('width')) size[0] = this.get('width');
if (this.get('height')) size[1] = this.get('height');
var element = ol.ext.element.create('LI', {
className: this.get('className'),
click: function (e) {
onclick(false);
e.stopPropagation();
},
style: { height: this.getHeight() + 'px' },
'aria-label': this.get('title')
});
ol.ext.element.create('DIV', {
click: function (e) {
onclick(true);
e.stopPropagation();
},
style: {
width: this.getWidth() + 'px',
height: this.getHeight() + 'px'
},
parent: element
});
return element;
}
}
/** ol/legend/Item options
* @typedef {Object} olLegendItemOptions
* @property {string} title row title
* @property {className} className
* @property {ol.Feature} feature a feature to draw on the legend
* @property {string} typeGeom type geom to draw with the style or the properties if no feature is provided
* @property {Object} properties a set of properties to use with a style function
* @property {ol.style.Style.styleLike} style a style or a style function to use to draw the legend symbol
* @property {ol.style.Text} textStyle a text style to draw the item title in the legend
* @property {number|undefined} width the symbol width, default use the default width
* @property {number|undefined} height ths symbol height, default use the default height
* @property {number|undefined} margin
*/
/** A class for legend items
* @constructor
* @fires select
* @param {olLegendItemOptions} options
*/
ol.legend.Item = class ollegendItem extends ol.Object {
constructor(options) {
options = options || {};
super(options);
if (options.feature) this.set('feature', options.feature.clone());
this.setWidth(options.width)
this.setHeight(options.height)
}
/** Set the legend title
* @param {string} title
*/
setTitle(title) {
this.set('title', title || '');
this.changed();
}
/** Set the item width
* @param {number} [width]
*/
setWidth(width) {
this.set('width', width || null);
this.changed();
}
/** Set the item heigth
* @param {number} [heigth]
*/
setHeight(heigth) {
this.set('heigth', heigth || null);
this.changed();
}
/** Get element
* @param {ol.size} size symbol size
* @param {function} onclick
*/
getElement(size, onclick) {
if (this.get('width')) size[0] = this.get('width');
if (this.get('height')) size[1] = this.get('height');
var element = ol.ext.element.create('LI', {
className: this.get('className'),
click: function (e) {
onclick(false);
e.stopPropagation();
},
style: { height: size[1] + 'px' },
'aria-label': this.get('title')
});
ol.ext.element.create('DIV', {
click: function (e) {
onclick(true);
e.stopPropagation();
},
style: {
width: size[0] + 'px',
height: size[1] + 'px'
},
parent: element
});
return element;
}
}
/**
* @classdesc
* 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.Control}
* @param {Object=} options extend the ol.control options.
* @param {ol.style.Style} options.style style used to draw the title.
*/
ol.control.CanvasBase = class olcontrolCanvasBase extends ol.control.Control {
constructor(options) {
options = options || {}
super(options)
// Define a style to draw on the canvas
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) {
this.getCanvas(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 */ }
}
if (map) {
this._listener = map.on('postcompose', this._draw.bind(this))
// Get a canvas layer on top of the map
}
}
/** Get canvas overlay
*/
getCanvas(map) {
return ol.ext.getMapCanvas(map)
}
/** Get map Canvas
* @private
*/
getContext(e) {
var ctx = e.context
if (!ctx && this.getMap()) {
var c = this.getMap().getViewport().getElementsByClassName('ol-fixedoverlay')[0]
ctx = c ? c.getContext('2d') : null
}
return ctx
}
/** Set Style
* @api
*/
setStyle(style) {
this._style = style || new ol.style.Style({})
}
/** Get style
* @api
*/
getStyle() {
return this._style
}
/** Get stroke
* @api
*/
getStroke() {
var t = this._style.getStroke()
if (!t)
this._style.setStroke(new ol.style.Stroke({ color: '#000', width: 1.25 }))
return this._style.getStroke()
}
/** Get fill
* @api
*/
getFill() {
var t = this._style.getFill()
if (!t)
this._style.setFill(new ol.style.Fill({ color: '#fff' }))
return this._style.getFill()
}
/** Get stroke
* @api
*/
getTextStroke() {
var t = this._style.getText()
if (!t)
t = new ol.style.Text({})
if (!t.getStroke())
t.setStroke(new ol.style.Stroke({ color: '#fff', width: 3 }))
return t.getStroke()
}
/** Get text fill
* @api
*/
getTextFill() {
var t = this._style.getText()
if (!t)
t = new ol.style.Text({})
if (!t.getFill())
t.setFill(new ol.style.Fill({ color: '#fff' }))
return t.getFill()
}
/** Get text font
* @api
*/
getTextFont() {
var t = this._style.getText()
if (!t)
t = new ol.style.Text({})
if (!t.getFont())
t.setFont('12px sans-serif')
return t.getFont()
}
/** Draw the control on canvas
* @protected
*/
_draw( /* e */) {
console.warn('[CanvasBase] draw function not implemented.')
}
}
/* 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).
*/
/**
* This is the base class for Select controls on attributes values.
* Abstract base class;
* normally only used for creating subclasses and not instantiated in apps.
*
* @constructor
* @extends {ol.control.Control}
* @fires select
* @param {Object=} options
* @param {string} options.className control class name
* @param {Element} options.content form element
* @param {Element | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport.
* @param {ol.Collection<ol.Feature>} options.features a collection of feature to search in, the collection will be kept in date while selection
* @param {ol.source.Vector | Array<ol.source.Vector>} 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<ol.source.Vector>|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<ol.Feature>} features
*/
setFeatures(features) {
if (features instanceof ol.Collection)
this._features = features;
else
this._features = null;
}
/** Get feature collection to search in
* @return {ol.Collection<ol.Feature>}
*/
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<ol.Feature>} result the current list of features
* @param {Array<ol.Feature>} 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<ol.source.Vector>}
*/
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<ol.source.Vector>|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<conditions>} options.conditions array of conditions
* @return {Array<ol.Feature>}
* @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<any>} 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 || '<a href="http://www.openstreetmap.org/copyright" target="new">&copy; OpenStreetMap contributors</a>';
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 || "")
+ "<i>"
+ " " + (p.postcode || "")
+ " " + (p.city || "")
+ " (" + p.country
+ ")</i>";
}
/**
* @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<any>} 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 = '<a href="https://www.geoportail.gouv.fr/" target="new">&copy; IGN-Géoportail</a>';
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 = '<?xml version="1.0" encoding="UTF-8"?>'
+ '<XLS xmlns:xls="http://www.opengis.net/xls" xmlns:gml="http://www.opengis.net/gml" xmlns="http://www.opengis.net/xls" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="http://www.opengis.net/xls http://schemas.opengis.net/ols/1.2/olsAll.xsd">'
+ ' <Request requestID="1" version="1.2" methodName="ReverseGeocodeRequest" maximumResponses="1" >'
+ ' <ReverseGeocodeRequest>'
+ ' <ReverseGeocodePreference>' + type + '</ReverseGeocodePreference>'
+ ' <Position>'
+ ' <gml:Point><gml:pos>' + lonlat[1] + ' ' + lonlat[0] + '</gml:pos></gml:Point>'
+ ' </Position>'
+ ' </ReverseGeocodeRequest>'
+ ' </Request>'
+ '</XLS>';
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>(.*)<\/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 type="Municipality">([^<]*)<\/Place>.*/, "$1"));
f.insee = (xml.replace(/.*<Place type="INSEE">([^<]*)<\/Place>.*/, "$1"));
f.zipcode = (xml.replace(/.*<PostalCode>([^<]*)<\/PostalCode>.*/, "$1"));
if (/<Street>/.test(xml)) {
f.kind = '';
f.country = 'StreetAddress';
f.street = (xml.replace(/.*<Street>([^<]*)<\/Street>.*/, "$1"));
var number = (xml.replace(/.*<Building number="([^"]*).*/, "$1"));
f.fulltext = number + ' ' + f.street + ', ' + f.zipcode + ' ' + f.city;
} else {
f.kind = (xml.replace(/.*<Place type="Nature">([^<]*)<\/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<any>} 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 = '<?xml version="1.0" encoding="UTF-8"?>'
+ '<XLS xmlns:xls="http://www.opengis.net/xls" xmlns:gml="http://www.opengis.net/gml" xmlns="http://www.opengis.net/xls" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="http://www.opengis.net/xls http://schemas.opengis.net/ols/1.2/olsAll.xsd">'
+ '<RequestHeader/>'
+ '<Request requestID="1" version="1.2" methodName="LocationUtilityService">'
+ '<GeocodeRequest returnFreeForm="false">'
+ '<Address countryCode="PositionOfInterest">'
+ '<freeFormAddress>' + f.zipcode + ' ' + f.city + '+</freeFormAddress>'
+ '</Address>'
+ '</GeocodeRequest>'
+ '</Request>'
+ '</XLS>';
// 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<ol.layer>} 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<ol.control.Control> } 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<ol.control.Control>}
*/
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<ol.control.Control>}
*/
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: '<i></i>',
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: '<i></i>',
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<any>} 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<ol.layer>} layers list of layers to display on the globe
* @param {ol.style.Style | Array.<ol.style.Style> | 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<number>} 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 a<b, 1 if a>b
* @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.Feature> | ol.Collection<ol.Feature> } 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>|ol.source.Vector} options.source vector sources that contains the images
* @param {Array<ol.layer.Vector>} 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<ol.Layer>} layers
*/
setLayers(layers) {
this._sources = this._getSources(layers);
}
/** Get source from a set of layers
* @param {Array<ol.Layer>} layers
* @returns {Array<ol.source.Vector>}
* @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<ol.Feature>}
*/
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<any>} 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<ol.layer>} options.layers list of layers
* @param {ol.style.Style | Array.<ol.style.Style> | 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<ol.layer>|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('<img src="' + e.canvas.toDataURL() + '"/>')
}
})
}
// 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") + ': <span class="zmin">'
firstTr.appendChild(div_zmin)
var div_zmax = document.createElement("td")
div_zmax.innerHTML = (this.info.zmax || "Zmax") + ': <span class="zmax">'
firstTr.appendChild(div_zmax)
var div_distance = document.createElement("td")
div_distance.innerHTML = (this.info.distance || "Distance") + ': <span class="dist">'
firstTr.appendChild(div_distance)
var div_time = document.createElement("td")
div_time.innerHTML = (this.info.time || "Time") + ': <span class="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") + ': <span class="z">'
secondTr.appendChild(div_altitude)
var div_distance2 = document.createElement("td")
div_distance2.innerHTML = (this.info.distance || "Distance") + ': <span class="dist">'
secondTr.appendChild(div_distance2)
var div_time2 = document.createElement("td")
div_time2.innerHTML = (this.info.time || "Time") + ': <span class="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<ol.coordinate>}
*/
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<ol.layer.Layer>} [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<ol.layer.Layer>} 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')
+ '<i>' + d + (t ? ' - ' + t : '') + '</i>',
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<ol.coordinate>|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 = '<a href="https://adresse.data.gouv.fr/" target="new">&copy; BAN-data.gouv.fr</a>';
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<any>|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<any>|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<any>|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 = '<?xml version="1.0" encoding="UTF-8"?>'
+ '<XLS xmlns:xls="http://www.opengis.net/xls" xmlns:gml="http://www.opengis.net/gml" xmlns="http://www.opengis.net/xls" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="http://www.opengis.net/xls http://schemas.opengis.net/ols/1.2/olsAll.xsd">'
+ '<RequestHeader/>'
+ '<Request requestID="1" version="1.2" methodName="LocationUtilityService">'
+ '<GeocodeRequest returnFreeForm="false">'
+ '<Address countryCode="CadastralParcel">'
+ '<freeFormAddress>' + search + '+</freeFormAddress>'
+ '</Address>'
+ '</GeocodeRequest>'
+ '</Request>'
+ '</XLS>';
// 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 ? '&section=' + 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<Number> | 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 = '<a href="http://www.openstreetmap.org/copyright" target="new">&copy; OpenStreetMap contributors</a>';
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 ? "<i>" + info.join(' - ') + "</i>" : '');
if (f.icon)
title = "<img src='" + f.icon + "' />" + 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<any>} 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 = '<a href="https://' + options.lang + '.wikipedia.org/" target="new">Wikipedia&reg; - CC-By-SA</a>';
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<any>} 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<ol.source.Vector>} 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<ol.source.Vector>|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<conditions>} 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<ol/source/Vector>} 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<ol/source/Vector>} 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<condition>} 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>} 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<ol/source/Vector>} 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<ol/source/Vector>} options.source the source to search in
* @param {Array<ol.control.SelectBase>} 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<ol.control.SelectBase>}
*/
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<ol/source/Vector>} 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 += '<label>' + i + ':</label> ' + html[i] + '<br/>';
}
}
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<ol.layer>} options.layers layers to swipe
* @param {ol.layer|Array<ol.layer>} 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<ol.layer>} 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<ol.layer>} 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<ol.style.Style>} 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<ol.Feature>} 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.Features>|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<ol.Feature>}
*/
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<string>} [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('<a href="' + encodeURI(caps.Attribution.OnlineResource) + '">&copy; ' + caps.Attribution.Title.replace(/</g, '&lt;') + '</a>')
}
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<string>} [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<ol.featureAnimation>} 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<ol.featureAnimation>} 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.filter>}
*/
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.filter>}
*/
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<ol.Coordinate>} [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<number>} [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<w; x+=step) for (var y=offset[1]; y<h; y+=step)
{ if (x>=0 && y<h) ctx2.drawImage (canvas, x, y, 1, 1, x, y, step, step);
}
ctx.clearRect (0, 0, w,h);
ctx.drawImage (c, 0, 0);
*/
// Draw brick stud
ctx.scale(ratio, ratio);
ctx.fillStyle = this.getPattern(offset[0] / ratio, offset[1] / ratio);
ctx.rect(0, 0, w, h);
ctx.fill();
ctx.restore();
}
}
/** Image definition
*/
ol.filter.Lego.prototype.img = {
brick: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAAAnNCSVQICFXsRgQAAAAJcEhZcwAAD10AAA9dAah0GUAAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAGAElEQVRo3sWZy4tkVx3HP+fcc29Vd1dP17TdTcbJPDKPMGR0kVEZkuBCF0EE9Z8QXLhxMUsRF4oLwYWQTSCgSxUXroQhoiEuskgEUUQh+BhHOpkZO11dr3vvefxc3FPlNHNvPbrD1Dl016XoqvM539/znFbcZo3VjbFmxcMA3Mg2fSoAiQJDov7/B1o9+aEgkycv4PBSPU9eHeDEixNwOAFXPYvFia0+rcnQEeBr218cfLIwCqW1UWillEYphUKpCmCCIQAiCEhAJIggTiSISBAfggTvJZTifQghWO+89cOQexuOXN8Pwz/9ff9X/xF0uEA7AmTsjLp/2xZQCgXHlj0OEBEAeRwGkep3qN6pfibDB3DBixMnvdCXt8J3FZowNYFSjgv71RtPaehjD0alalVOqCtHU3qlAGrVAGbidCtUYLUAiV6dCUx8XV4BhUKjY0AJgUB4LE8sA7CkCRSalFYnE72WiBrLSCKCp6TALZNRDEDCwgAKQ/vyRidN9c32K1sbqlCP/C+P9kXJI597PA7HkGJRCLNUGCY767udF9e+9dz1S5ueoRzIEZa1OxcK9td+/fAHvYH0LY6MkgHFIuYwS0ifXe1+qXvn1vk99QfzCwokToUylPrre1/de/vMnf9+5MsSg2HMELegAsl86duvnP3e8y/f1r83v8Li1RO7k/9c2t/avHnt27xpyhRDguEIuxDA3OXXX93+8a0rz6ZvcKgadqUEL73wx+9sb5//WWKTGCOHsxEWM0H71e2ffmF3lPyEkZppVyVYefCw/9a5f3epSvsWh7MMsUgeaL20/dpLu4fJXZUvFCgi46/8i5RNFCCc4bA5JuZ7f/Kp7g9fuLSdvLnY8lEHxz8ItOPcaN7gPAB1tvPl7udupT9nvGSmLLlHSosWLdbJTgpgLna+eVv9hiO1ZIpFOGBEFmejBnrO/tc/0znXTf+sHMuPwD0MrSnETID6/SXPrH/junp3Xiw3atCjxJCRktKu10DHzrZ+pOvpc5cP/6T8CWtt4BATZ4tkBoCvTz8tbTb8TnHiYi/0pgCmPufMUkB1ss9vtU7Trgt9EgyGhIS0zgjRB6RukaSdfHpLPly2xTg2chQJmgRN2qiAa3DBtu5kYXgqAIFYEzTJDAVCnQIqaA+O0wyFjj8q1oY6AB/qd5nLw9JvcpqOOcFMT5dqlg/UAoy5exS2TgGg6DxhkHofqHVCGYf3ho/S904DcHZ6jpZ6lWMY1iogCDxsn8oDduP3BEI9QvSBWgU8YRDeGezsyEk1SNlD8HF51wjQoEAgHNkffXBw+XfJiZbXXCTBT2fZaAJfn4iEEt+z73bTk92jZTxPwOFxVCeGRif0tt4HCtxB+f0P7l//rTlBAN6gjcNicThcfU2NCnjf0NU43L59vf2XZf1A8wzX8JRTgLw+Ckx17SahIZGOyMri7dHalXf6DJdYfovPAgVlRLAzAXwI0gCQU5La8m6SXeH9pi+pWf5lUooIUFKSN6V0A1AE39RyeAYYEpvYNjf4OwP8XNuf50UycnKKKURjSTMALkjzzgpyEhI0LW7ygHvYRh00G7zARQL5dBYU9JtLWvQB52e0VX0MOl5anmOP+3yIjZldpteZijZXuIbBxZ1PAEbkc05GVspZtnX04hlHEDKucpUePYbklCgyNjjDLp9AERhjKSNAQc6IwSzPMQClt37OIeOQ7vQWxJPSZSf2OZMyK1h8jHsbNSgY0Z/tNRWA2HmuVXLIZsxnliw2mROAyR2Rjwmn8vyC0XynrUwQ3PzGs6QX06rDRgD9GIDEjF9pUFLSXyRsowLFIp2/44icDpZ02umq6S3ZxDwupp3hYs1cVMAu1noLBZaMNbJoAD3tl6prOodnTF5feBoBRmGweO8fyClISMlIowkkApRYyqbeZ5YJQrHc4UNieeGYArL8NeUkFcvgJKc/AU56ajxejod+/DT/W/IkQC4P3GoBwoGsFKAf9v2qAGIxej9MU8rTGdNjWtVsJv315aL3YwDYqG5MTDxAPMvTNkJS3ReY6AmtlTrhKsf/AHgAA6ezGE+FAAAAAElFTkSuQmCC",
ol3: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAAAnNCSVQICFXsRgQAAAAJcEhZcwAAD10AAA9dAah0GUAAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAHtUlEQVRo3sWZTWxcVxXHf/d9zIztcTz+pE6cOHXiyLJJadKgKE2oCBLlQwIWSCxYI0WiGxZZIYRAArFAYoEEi0hIsGBBURd0g4iK2lJAaWlaojZVKkU0H26cxB8Zz/f7uPeweHdebDLPnqlQ5l2N5/mN7tz/+Z//OffcM4rPUKCPl0eBAqqfAEAt5Ia1LwCuAg93CyCnAzgj7TstEKMluW+/x0AsWmKBmFggTu4lIpYome2Qw0kA8I2xL9T2Bp5COY6ncJRSDkopFEolANowBEAEATGIGBEkFjEiYkQbI0ZrMaFobYwxkY51pOumpSNTiau6bm7oZX1NP4Ai+ylYADkmGqUPxwSUQsG2ZbcDsBAA2QoGkeSvSZ4kr/alDcRGSyyxbJqqvG5+pHAwbRegVMz+leTBY7qcbTee8vsmQycRmnL6CkD1G4DXFl0fGegvANfpnws8+947AwqFg2MDSjAYzJY80QuAHl2gcPDJF3PiDLiimtIQC0ETEhD3klE8AJeuASg8CgeHir7vLBVOjwypQK3plyoromRNtzSamJg6QbcgvJ7C0J0YnCweG/jek/Ozw5q6bEiFiIHz+wNWBv68+rPNmlQjYnKE1Ai6cYfXA/W5Q6Uvl84f3zel3vH+SIDYoVAeofOdqa9PvbHn/PoDHYZ4eDSpE3fJgLs79YXToz858uxJ5+/en4jQ6hHr5OPZlZHhpcM/4BUv9PFw8agQdQVg1+UHnx/75fG5Gf83lFWGVUrQsmmu/HBsbN8f3Mi1MVLeGUJ3Lig8P/a7s5MN97c01I5+VUIk91err0/fLqFwgBHKOzmimzyQPzX2q1OTZfeianUVKCLNr93EZxiFIOyhnB0Tu6vf/XTp54uzY+4r3S1veYj5CEPBjqFsA3cDoEaLXy199rj/Is0eM2XILXzy5MkzSO6TAvAOFF84qf5KRfWYYhE2aJCzI5MDbxf7B58pTpf89x8qX1yWGKXKFaUBZIF1tWo/KzJPiYi3VAgYbrFEnpiYiBzBTgx0ts99YvDcvHr7YSBJka/Q4k1u3jz5eQ/EYebkXvL241NUeZN/31gkDwibhHjk8PGzTh+OrWw7X/6g/+TB8nuJrQCc4Z/KU08rb+1f/1gCSqy9NUNoP72txtXRb40dfJ+nkgMEZTw78riZLhDRndNP3vGG9GBKnRzhrppmilfhmcWoRYkxyuxv86euUaT24h4W2WN53WQmheB1ygc7MaCKuc+N5LeW6wfOXeUorwFQZIV5RlnbNqcGjBMyaAFUcfHwcHHxOznBakA6JQq34B4dkXtt+8QjvnCQa/Z/jxpFCmdbpPSJI7NyhMVzK/j2UQuFi4OLkz57FECcIcGCU8yZeirQvdxjjuvpTKGAem2EcjpjkjnUC5cvfIm/bRG3Y4e7AwOmEwPKOJotfhvlPj61dGaBEChtAdD88Yeq9et1LqWOUTj2lYzOItSmcxi2ZDXUw+k0n0bqDoXDJBsMM8rHKeIKFbxgIV9nL3cSFlPpZQBoa6AjgCYXK2YkndbckkxmWWfu2D00ozzYNinOlagwbRct/k92zNJARxFK01yur/mX2wDWGE0jfuHyNfa+Y6hQYNsmJQ45hqwwFaPpOVo6s2zDsCMDgsBq2sBR9xj8ZvX70+LJc9w+scA1Sjz49rjMy7zMywE5IY64PMcNDlkHKCbt9xhMZwhOooGODGhMzVyqTUxIm4Pll9797ixnWFZ3WORdSqz//hI+Pv7LT5dXOcNZltUa49y3qplC0Hb5uBMAbwcGDKYS/eLu6YMfrSZCUhWY+QCfGZ7iZYRbarSdYMfd0bvXazh8ii/yF2vcAVwitB1hZirWnROREFLYjN4uLQ5QTZ/WmeA2VwDUHbBks351HRxK3OaqtTTHEQwxmpjkxJApQh111kBAvBH+9O7y/KveFsfcYyNj82qywqZdxmWBAjEREbHdkrNEqNE6o6qJiVeiC4UPHuqg20PvExxGE6YAWp2jwEvabmIyqpoGuTB4ozEwd6lKvYflRzgBBIQWQrQjAG2MZABoEeJH4UU3N8f1rC/psPyz+AQWQEhIK6s09wACk+EC0NTwcCM3KrDAf6ihd6ui2ccxcrRoEaQg6lnQPYDYSLZlAS1cXBzyLHGfW0SZPDgMscgBDK10BARUs48mVgNxtl2GKh6ObVpOM8Uy94hsZpe0nakoMMdhPGJreRtAg9YuJ6NIwp18G7OJsilVyHGIQ2yySZ0WIYocQ+xhknEUhiYRoQUQ0KJBbSfleAChjvQuh4wypbQLovEpMWHrnPY2K0RoG/eR5SCgQXVn1SQAJNpNWiFlhm0+i8jZIrMNoN0j0jbhJMoPaOwu2sQFJt69oRKyadNqTGQBOFsAiM34CQchIdVuwtYyEOgu4jumQosiEX5a6aq0S9Z2T2zTThfdkS0MRN21lISAiBwD5KwDnLReStp0MZomrc4bTyaAhql131gztAhw8cnhWxeIBRASEWbVPju5wAS9/VYgdnthGwPSe5uynYqlpun9EuCTzHt0O67r5uP8teRRAC25H/cXgNmQvgKomhXdLwB2M7pu0pTyeK70mJYUm251sLfo/T8AGEoKes8eIGZ43E5wk36BBwhO2mbqgwZa9C0CAP4LFLGzNDDzmrAAAAAASUVORK5CYII=",
lego: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD8AAAA/CAQAAAD9VthUAAAAAnNCSVQICFXsRgQAAAAJcEhZcwAADzoAAA86AZc528IAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAHvElEQVRYw8WZWWxcVxnHf+fec2fGu+M4qbPH2EmTLk4FApqQNrgiVYyKeClIwBsvPCAQkZAQPND2gRekCoGQEItYHhBLurCksoTKA6QNJW0CBZLWCc3qEBvHy3hsz93O+XiYMxM7nhmPGxGf++Dre2fO7/v+5/u+s4zigzSxVq3osaZNAwzkuq1nPeUrAE9p99JTAKWn5WYEwErpv9TdGbECRlKMgBEjRlIgsqlAKBBaSKUokAjgkcFz+Ce6BvM9sVbKU55WKKWUh1IeoJQCFhsgIIIgIohYEWwq1ooVK9ZasdbG1hhrjUmMsYlZsJEJzYIpmLwZs/8xZ9JpaGYHOYfPyvrChrdbpAxbjFRltCqhnQ2yxBTKf0WQUgNrwYqIFStGUkkllqIU5E/2aQBbEV8pz/ZM3Or8/95UmeUB+J63RiHoAWi1ZHTvNl6pNfXe99Taeq/W1HvuYOzvKG5c4q1afIWHj4eHBwgWwWCQ1aWvBvC8VXngE5DbmO3UxrOeqEhmTFEcPiIiadwEfVttWxmd623tyu7Mfnrjru5cM0Th+Nyp2Z/MztvJNDLkSImZJ27MhNWIr8j1tn+g9at7+/ubivaSHYkmjPF1f+sj7Uc3Xc29VPjm1JSJEzJkCZkjaVT8hvzubDvQ8cz9AwPeG/rHFD3BZkoeTqLwCuqzrQf7nw9+UJhOidEEFAhX0sCJr1fyXm/uPLr5849n/u1/j3mMWtqtYEFm5v/2pXUHdhzNjgaxzunQQzNX3wDdWIT0dT3bP3Qo8wIXSVWNDpWQys2xmW/3fbn1WpAWXUrWNaARvN+/7lu7jzysf8q4siuEh5A8fX5/+8XepLyEs8zfCd7raP/K9scf1T9iQjUUzU+JynOR3TQBgpAS1a16dVtusONTH8kc42ZjcFFKEApcJyBHjizt+O8Wr3e2P7Uv+3curyT7InhJ8nFCMmTJkqWlVlnzVsj0psc69vbrV1SyKnjJgCsEZMiQoanWINcfe39v6xfv808Suu6f5EVlQA7QAcC/1DXp42GmuazOiaJbjjDFSTUNCLOEZMiQEJAjrZYB9b0PmoPe7fpNZQAkYFb1A9CphtWwGlbNkmX/R59TpzhPAAwdf37XKWac1JZJAnc1VSfp0ufSqtK3NT/Y3DJVKZ5tYbHiwfvJAjc5dO7Pw4cZOb4vc51ccvZjh7ZfubaTC8y4evgeAjQaTYCpgZfq06TXpD++Rd6hHHTdZ8JKDs8yAsAD92/gjxSfGNvYzp7Wt3nj6sS2D5NxtXAeHNwnIFpOqSe+bg+2d6ejFXzXS8WlJUSyhBiKoAqj1yFuYQLQZCvFOMLDx8evPFuOF7HV0sqzXmsuP1mJ5tbfVirYc++VITnItvyN8rhJjqIrL7qS50KCX1mWeLXFr5Z02nqiJ2+lXOasIQHJkD75C6DjtQ8dH6Eg99FHyD+LBRclaomnqgL3lo++w4utWsBVbNYtr1htYZFBZgm2299Z5rmXl4+ZtwaPjDlt9CJ0gIeqXNXFN7WKDtMLnW1y+9e6Txc5z2le25Te0BTVic89ovf3yIXE1QeP4FbJbmCla21V723evjklncued/0mZA6AcEABfH/6rXzb2IM5fJD1zLvIB02zm3ak+iK0hK8mvmBnzA/Hoy3LJoyW4XIITn5daAbaX0w3XBnIIsCBL7zDpFNvPWoRvBY+larBZ5Gb6eX20xXxf/2QDMkgmc+sl8MyJH2cf/Seka3yGFv+kR7Ok/1riwxhvruJUYffhGCxWKS0IqqReFXFN5g583qaNokC0aSf/JUaVn95ufNrJ9SwGlapMkkUXuPMAy/E24CJbQVeVWeIXDAFbEYwWCymes3XAMZW9d5gC8k3Rn++79hJjErvvcBB0P53/sBBAOa5knmdnWwlywlQZ7mHfQivOsd6yVDEkGIwxDVrfo2yY4nJ5tMTLe9rkYKSkUtcEqXk9/DKok9d5nLlfpzxyn0Tu7Gk7jLVNx8eQFw98oUi6Vz07NiZ3c/4y+bz+i1gHxliEhJnQFKn6MbVu01ISRaSX2b8vk/4q4D77GErCTGxM2EBW1P8pLr4YJkjiKORhZ91hR1qpsG9m89O9pASOXxMXF6wrCb0ACIidBJe8ZNdjHID24DsA/RhCImInAnztQqr897UeI1lDp3ToU8TO2jiat39q0cLD7GJlNBdERFhLd8dPjamtldx98K8dhNGD91cZ6zKPl6hyNJPP5rYIcsGFGprVva+Nl4GF455lVzI0UcvU0ySX7R5aKabHnrwMRSJlhiQr7fT1QCprYPnmKHgzjQtliwZNrIZHyHBkHHLSMG4KI+JK6Lna+9wFuETUzecLAUHN6QkBARofHwCFImr6Mbld+Lw0Upwhy/acKWUMswS07YI77tllHJTqsW4t4lLtcLKBwyl0JN05YQSiqS0knW+a7eGu4W3rrgmJMwRNpCkLvRsaBoqKAkzZGgi66S/HV+Sf4GQxvor4xPbYDkVIuLS2RZ6CV4wRMQkNNpXGb9go1V8BSElJXRrWIXCupM9We2hvMPPG1bbaqxf3sWhamTzhjVpHsCc/a9dQ3xo82uJL9jRNRLfTTnnBO+u/pTkLT5c8fPNd9nt5tLmRbsVynbsXR704Bbeq775v0uht3btfyZT7OA5knjdAAAAAElFTkSuQmCC"
};
/* 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} FilterPointillismOptions
* @property {number} saturate saturation, default 2
*/
/** @deprecated use canvas filter instead
* @constructor
* @extends {ol.filter.Base}
* @param {object} options
* @param {boolean} [options.active]
* @param {number} [options.scale=1]
*/
ol.filter.Paper = class olfilterPaper extends ol.filter.Base {
constructor(options) {
options = options || {};
super(options);
this._svgfilter = new ol.ext.SVGFilter.Paper(options);
}
/** @private
*/
precompose( /* e */) {
}
/** @private
*/
postcompose(e) {
// var ratio = e.frameState.pixelRatio;
var ctx = e.context;
var canvas = ctx.canvas;
ctx.save();
ctx.filter = 'url(#' + this._svgfilter.getId() + ')';
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'multiply';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
/** Set filter light
* @param {number} light light option. 0: darker, 100: lighter
*/
setLight(light) {
this._svgfilter.setLight(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).
*/
/** @typedef {Object} FilterPencilSketchOptions
* @property {number} blur blur value in pixel, default 8
* @property {number} value intensity value [0,1], default .8
*/
/** Colorize map or layer
* Original idea: https://www.freecodecamp.org/news/sketchify-turn-any-image-into-a-pencil-sketch-with-10-lines-of-code-cf67fa4f68ce/
* @constructor
* @requires ol.filter
* @extends {ol.filter.Base}
* @param {FilterPencilSketchOptions} options
*/
ol.filter.PencilSketch = class olfilterPencilSketch extends ol.filter.Base {
constructor(options) {
options = options || {};
super(options);
this.set('blur', options.blur || 8);
this.set('intensity', options.intensity || .8);
}
/** @private
*/
precompose( /* e */) {
}
/** @private
*/
postcompose(e) {
// Set back color hue
var ctx = e.context;
var canvas = ctx.canvas;
var w = canvas.width;
var h = canvas.height;
// Grayscale image
var bwimg = document.createElement('canvas');
bwimg.width = w;
bwimg.height = h;
var bwctx = bwimg.getContext('2d');
bwctx.filter = 'grayscale(1) invert(1) blur(' + this.get('blur') + 'px)';
bwctx.drawImage(canvas, 0, 0);
ctx.save();
if (!this.get('color')) {
ctx.filter = 'grayscale(1)';
ctx.drawImage(canvas, 0, 0);
} else {
ctx.globalCompositeOperation = 'darken';
ctx.globalAlpha = .3;
ctx.drawImage(canvas, 0, 0);
}
ctx.globalCompositeOperation = 'color-dodge';
ctx.globalAlpha = this.get('intensity');
ctx.drawImage(bwimg, 0, 0);
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).
*/
/** @typedef {Object} FilterPointillismOptions
* @property {number} saturate saturation, default 2
*/
/** A pointillism filter to turn maps into pointillism paintings
* @constructor
* @extends {ol.filter.Base}
* @param {FilterPointillismOptions} options
*/
ol.filter.Pointillism = class olfilterPointillism extends ol.filter.Base {
constructor(options) {
options = options || {};
super(options);
this.set('saturate', Number(options.saturate) || 2);
this.pixels = [];
}
/** Create points to place on the map
* @private
*/
_getPixels(nb) {
if (nb > 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<ol.ext.SVGFilter>} 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<string>|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<string>|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<ol.coordinate>} v
* @param {number} decimal
* @return {string|Array<string>}
* @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<string>}
* @param {number} decimal Number of decimals
* @return {ol.coordinate|Array<ol.coordinate>} 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<ol.Feature>} 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<ol.Feature>}
* @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<ol.coordinate>} v
* @return {string|Array<string>}
* @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<string>}
* @return {ol.coordinate|Array<ol.coordinate>} 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<ol.Feature>} 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<ol.layer>} 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<ol.layer>} 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<ol.layer>} 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<ol.layer>} 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<ol.coordinate>}
* @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<ol.style.Style>} 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<ol.Feature>} options.features list of features to copy
* @param {ol.source.Vector | Array<ol.source.Vector>} 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<ol.source.Vector> } 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<ol.source.Vector> }
*/
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<ol.Feature>}
*/
getFeatures() {
return this.features;
}
/** Set current feature to copy
* @param {Object} options
* @param {Array<ol.Feature> | ol.Collection<ol.Feature>} 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.Feature> | ol.Collection<ol.Feature>} 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<ol.source.Vector>}
*/
_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<ol.Feature>|Array<ol.Feature>} 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<ol.Overlay>} 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<ol.layer.Vector> | 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.Feature> | ol.Collection<ol.Feature> | 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<ol.style.Style> | 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<ol.coordinate>} 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<ol.Layer>} options.source Destination source for the drawn features
* @param {ol.Collection<ol.Feature>} options.features Destination collection for the drawn features
* @param {ol.style.Style | Array.<ol.style.Style> | 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<ol.style.Style>} [options.style] Drawing style
* @param {ol.style.Style|Array<ol.style.Style>} [options.sketchStyle] Sketch style
* @param {ol.style.Style|Array<ol.style.Style>} [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<function(new:ol.format.Feature)>|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<string>|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<ol.Feature>} 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.<ol.Feature> | 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.style.Style> | 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<this.path_.length; step++) {
if (ol.coordinate.equal(p, this.path_[step])) {
p[2] = this.path_[step][2];
p[3] = this.path_[step][3];
break;
}
}
}.bind(this));
*/
// Get 3D geom
if (this.get("type") === 'Polygon') {
geo = new ol.geom.Polygon([simply], 'XYZM')
} else {
geo = new ol.geom.LineString(simply, 'XYZM')
}
return geo
}
/**
* 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.geolocation.setProjection(map.getView().getProjection())
}
/** Activate or deactivate the interaction.
* @param {boolean} active
*/
setActive(active) {
if (active === this.getActive()) return
super.setActive(active)
if (this.getMap()) {
this.geolocation.setTracking(active)
try { this.getMap().renderSync()} catch (e) { /* ok */ }
}
if (!this.overlayLayer_) return;
this.overlayLayer_.setVisible(active)
this.pause(!active)
if (active) {
// Start drawing
this.reset()
this.dispatchEvent({ type: 'drawstart', feature: this.sketch_[1] })
} else {
var f = this.sketch_[1].clone()
if (f.getGeometry()) {
if (this.features_)
this.features_.push(f)
if (this.source_)
this.source_.addFeature(f)
}
this.dispatchEvent({ type: 'drawend', feature: f })
}
}
/** Simulate a track and override current geolocation
* @param {Array<ol.coordinate>|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<any>} 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<ol.layer> | 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<ol.Feature>} 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<ol.source.Vector>} options.sources a list of source to modify (configured with useSpatialIndex set to true)
* @param {ol.Collection.<ol.Feature>} 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<ol.style.Style> | 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<ol.Feature>} 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<number>|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<ol.layer.Vector>} options.layers list of feature to transform
* @param {ol.Collection.<ol.Feature>} 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.Style> | 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<ol.style.Style> | 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<ol.Feature> | 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<ol.coordinate>} 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<ol.coordinate>} 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<ol.source.Vector>} [options.sources] a list of source to split (configured with useSpatialIndex set to true), if none use map visible layers.
* @param {ol.Collection.<ol.Feature>} 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<ol.style.Style> | 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<ol.style.Style> | 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<ol.source.Vector>}
*/
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<ol.source.Vector>|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<ol.coordinate>} 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<ol.source.Vector>} 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.<ol.Feature>} options.features A collection of feature to be split (replace source target).
* @param {ol.Collection.<ol.Feature>} 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])<tol)) ;
console.log ("s11: "+(_ol_coordinate_.dist2d(p,s1[1])<tol)) ;
console.log ("s20: "+(_ol_coordinate_.dist2d(p,s2[0])<tol)) ;
console.log ("s21: "+(_ol_coordinate_.dist2d(p,s2[1])<tol)) ;
*/
return p
}
}
/** Split the source using a feature
* @param {ol.Feature} feature The feature to use to split.
* @private
*/
splitSource(feature, change) {
if (!this.getActive())
return
// Allready perform a split
if (this.splitting)
return
// Start splitting
this.source_.dispatchEvent({ type: 'beforesplit', feaure: feature, source: this.source_ })
this.dispatchEvent({ type: 'beforesplit', feaure: feature, source: this.source_ })
// If the interaction is inserted other interaction, the objet is not consistant
// > 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<ol.Map>} 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<string>} options.types geometry types avaliable, default none
* @param {ol.source.Vector} options.source Destination source for the drawn features
* @param {ol.Collection<ol.Feature>} 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<ol.source.Vector>} options.sources a list of source to modify (configured with useSpatialIndex set to true)
* @param {ol.Collection.<ol.Feature>} 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<ol.style.Style> | 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<ol.Layer>} options.layers array of layers to transform,
* @param {ol.Collection<ol.Feature>} 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<number>|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<ol.style.Style>} 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<ol.Feature>} 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<number>|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<ol.Layer>} 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<ol.Feature>}
*/
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<ol.Features>} 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:
<ol.source.Vector>
*/
/**
* @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 = [ '&copy; <a href="http://dbpedia.org/"">DBpedia</a> 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: <http://www.w3.org/2003/01/geo/wgs84_pos#> "
+ "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<Number>} 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<ol.Point>|Array<Array<ol.Point>>}
*/
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<ol/coordinates>} 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('');
$('<li>')
.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<this.flip.length; i++) {
$('<li>')
.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<ol.coordinates>} d1
* @param {Array<ol.coordinates>} 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<ol.coordinate>} 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<ol.coordinate>} 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<ol.coordinate>} 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<ol.coordinate>} 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<ol.Feature>} 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.<ol.Coordinate>} 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 = '<a href="http://www.geoportail.gouv.fr/">Géoportail</a> &copy; <a href="http://www.ign.fr/">IGN-France</a>';
/** 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<ol.Feature>}
*/
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<number>}
* @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:
<ol.source.Vector>
*/
/** 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 = ["&copy; <a href='https://www.mapillary.com/'>Mapillary</a>"];
// 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<meta.length; k++)
{ if (meta[k].name=="GPSLongitude") pt[0] = meta[k].value;
if (meta[k].name=="GPSLatitude") pt[1] = meta[k].value;
}
if (!pt.length)
{ //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) 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 to turn your maps into oil paintings...
* Original idea: Santhosh G https://www.codeproject.com/Articles/471994/OilPaintEffect
* JS implementation: Loktar (https://github.com/loktar00) https://codepen.io/loktar00/full/Fhzot/
* @constructor
* @extends {ol.source.Vector}
* @param {Object} options
* @param {Array<ol/source/Source|ol/layer/Layer>} 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<string>} 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<string>} 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<number>} 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:
<ol.source.Vector>
*/
/**
* @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 = [ '&copy; <a href="https://commons.wikimedia.org/">Wikimedia Commons</a>']
// 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 '<a href="'+o.href+'"><img src="'+o.logo+'" title="&copy; '+o.attribution+'" /></a>';
} else {
if (!title) return ol.source.Geoportail.prototype.attribution;
else return '&copy; <a href="'+o.href+'" title="&copy; '+(o.attribution||title)+'" >'+title+'</a>'
}
}.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<o.constraint.length; i++) {
if (o.constraint[i].maxZoom > 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.length; i++) {
if ( z <= o.constraint[i].maxZoom
&& z >= 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(/^<!\[CDATA\[(.*)\]\]>$/, '$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 lIGN, 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 lIGN - 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<String>} 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<String>} 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<l.length; i++) {
t = t.concat(l[i].getPreview(lonlat, resolution));
}
}
return t;
};
/* 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).
*/
/** Layer that use Maplibre GL as render
* @constructor
* @extends {ol.layer.Layer}
* @param {any} options layer options
* @param {string} options.layer Geoportail layer name
* @param {string} options.gppKey Geoportail API key
* @param {olx.source.WMTSOptions=} tileoptions WMTS options if not defined default are used
*/
ol.layer.Maplibre = function(options) {
if (!ol.layer.Layer) {
console.error('[ol/layer/MapLibre] bad ol version (need ol@6+)');
}
options = options || {};
options.render = function (frameState) {
// Create map if not exists
if (!this._container) {
this._create(options.style);
}
/**/
var fac = 1
var dzoom = 1
//fac = Math.pow(2,.5);
//dzoom = .5;
/*/
var fac = 2
var dzoom = 0
/**/
this._container.style.width = fac*frameState.size[0]+'px';
this._container.style.height = fac*frameState.size[1]+'px';
var glMap = this.glMap;
if (!glMap) return null;
var canvas = glMap.getCanvas();
canvas.style.transform = 'scale('+(1/fac)+')';
// Force map to resize
if (frameState.size[0] !== canvas.width || frameState.size[1] !== canvas.height) {
glMap.resize();
}
canvas.style.opacity = this.getOpacity();
// adjust view parameters in mapbox
var viewState = frameState.viewState;
glMap.jumpTo({
center: ol.proj.toLonLat(viewState.center),
zoom: viewState.zoom - dzoom,
bearing: (-viewState.rotation * 180) / Math.PI,
animate: false,
});
// cancel the scheduled update & trigger synchronous redraw
// see https://github.com/mapbox/mapbox-gl-js/issues/7893#issue-408992184
// NOTE: THIS MIGHT BREAK IF UPDATING THE MAPBOX VERSION
if (glMap._frame) {
glMap._frame.cancel();
glMap._frame = null;
}
glMap._render();
return this._container;
}
ol.layer.Layer.call(this, options);
};
if (!ol.layer.Layer) ol.layer.Layer = function() {};
ol.ext.inherits (ol.layer.Maplibre, ol.layer.Layer);
/** Get the Maplibre map
* @return {Object}
*/
ol.layer.Maplibre.prototype.getMapGL = function() {
return this.glMap;
}
/** Set style
* @param {Object|string} style Mapbox style Object or a URL to JSON
*/
ol.layer.Maplibre.prototype.setStyle = function(style) {
this.set('style', style);
if (this.getMapGL()) {
this.getMapGL().setStyle(style);
}
this.changed();
}
/** Returns the map's Mapbox style object.
* @returns {Object}
*/
ol.layer.Maplibre.prototype.getStyle = function() {
return this.getMapGL().get('style');
}
/** Create the map libre map
* @param {Object|string} style Mapbox style Object or a URL to JSON
* @private
*/
ol.layer.Maplibre.prototype._create = function(style) {
this._container = ol.ext.element.create('DIV', {
className: 'ol-maplibre-gl',
style: {
position: 'absolute',
top: 0,
left: 0
},
parent: document.body
})
this.glMap = new mapboxgl.Map({
container: this._container,
style: style,
center: [3, 47],
zoom: 5,
pitch: 0,
antialias: true,
attributionControl: false,
boxZoom: false,
doubleClickZoom: false,
dragPan: false,
dragRotate: false,
interactive: false,
keyboard: false,
pitchWithRotate: false,
scrollZoom: false,
touchZoomRotate: false,
});
};
/** ol.layer.Vector.prototype.setRender3D
* @extends {ol.layer.Vector}
* @param {ol.render3D}
*/
ol.layer.Vector.prototype.setRender3D = function (r) {
r.setLayer(this);
}
/**
* @classdesc
* 3D vector layer rendering
* @constructor
* @param {Object} param
* @param {ol.layer.Vector} param.layer the layer to display in 3D
* @param {ol.style.Style} options.style drawing style
* @param {function|boolean} param.active a function that returns a boolean or a boolean ,default true
* @param {boolean} param.ghost use ghost style
* @param {number} param.maxResolution max resolution to render 3D
* @param {number} param.defaultHeight default height if none is return by a propertie
* @param {function|string|Number} param.height a height function (returns height giving a feature) or a popertie name for the height or a fixed value
*/
ol.render3D = class olrender3D extends ol.Object {
constructor(options) {
options = options || {}
options.maxResolution = options.maxResolution || 100
options.defaultHeight = options.defaultHeight || 0
super(options)
this.setStyle(options.style)
this.set('ghost', options.ghost)
this.setActive(options.active || options.active !== false)
this.height_ = options.height = this.getHfn(options.height)
if (options.layer)
this.setLayer(options.layer)
}
/**
* Set style associated with the renderer
* @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: 'rgba(0,0,255,0)'
}))
}
if (!this._style.getFill()) {
this._style.setFill(new ol.style.Fill({ color: 'rgba(0,0,255,0.5)' }))
}
// 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 renderer
* @return {ol.style.Style}
*/
getStyle() {
return this._style
}
/** Set active
* @param {function|boolean} active
*/
setActive(active) {
if (typeof (active) === 'function') {
this._active = active
}
else {
this._active = function () { return active }
}
if (this.layer_)
this.layer_.changed()
}
/** Get active
* @return {boolean}
*/
getActive() {
return this._active()
}
/** Calculate 3D at potcompose
* @private
*/
onPostcompose_(e) {
if (!this.getActive())
return
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 / 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<ol.style.Style>} options.style Drawing style
* @param {ol.style.Style|Array<ol.style.Style>} 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<ol.coordinate>} 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<Array<number>>}
*/
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<number>} [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<number>} 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.<TemplateAttributes>} 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<number>} 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<ol.Feature>} 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<number>} 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 ? '<br/>' + 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<ol.geom.Point>} points an array of 2D points
* @return {Array<ol.geom.Point>} 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<ol.geom.Point>} 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<ol/Feature>}
*/
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<ol/coordinate>} 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<ol/Feature>}
*/
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<ol/Feature>}
* @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<ol.coordinate>} 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<ol.Coordinate>} coords
* @param {number} offset
* @return {Array<ol.Coordinate>} 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<ol.Coordinate>} coords
* @return {} the index (-1 if not found) and the segment
*/
ol.coordinate.findSegment = function (pt, coords) {
for (var i=0; i<coords.length-1; i++) {
var p0 = coords[i];
var p1 = coords[i+1];
if (ol.coordinate.equal(pt, p0) || ol.coordinate.equal(pt, p1)) {
return { index:1, segment: [p0,p1] };
} else {
var d0 = ol.coordinate.dist2d(p0,p1);
var v0 = [ (p1[0] - p0[0]) / d0, (p1[1] - p0[1]) / d0 ];
var d1 = ol.coordinate.dist2d(p0,pt);
var v1 = [ (pt[0] - p0[0]) / d1, (pt[1] - p0[1]) / d1 ];
if (Math.abs(v0[0]*v1[1] - v0[1]*v1[0]) < 1e-10) {
return { index:1, segment: [p0,p1] };
}
}
}
return { index: -1 };
};
/**
* Split a Polygon geom with horizontal lines
* @param {Array<ol.Coordinate>} geom
* @param {number} y the y to split
* @param {number} n contour index
* @return {Array<Array<ol.Coordinate>>}
*/
ol.coordinate.splitH = function (geom, y, n) {
var x, abs;
var list = [];
for (var i=0; i<geom.length-1; i++) {
// Hole separator?
if (!geom[i].length || !geom[i+1].length) continue;
// Intersect
if (geom[i][1]<=y && geom[i+1][1]>y || geom[i][1]>=y && geom[i+1][1]<y) {
abs = (y-geom[i][1]) / (geom[i+1][1]-geom[i][1]);
x = abs * (geom[i+1][0]-geom[i][0]) + geom[i][0];
list.push ({ contour: n, index: i, pt: [x,y], abs: abs });
}
}
// Sort x
list.sort(function(a,b) { return a.pt[0] - b.pt[0] });
// Horizontal segment
var result = [];
for (var j=0; j<list.length-1; j += 2) {
result.push([list[j], list[j+1]])
}
return result;
};
/** Create a geometry given a type and coordinates */
ol.geom.createFromType = function (type, coordinates) {
switch (type) {
case 'LineString': return new ol.geom.LineString(coordinates);
case 'LinearRing': return new ol.geom.LinearRing(coordinates);
case 'MultiLineString': return new ol.geom.MultiLineString(coordinates);
case 'MultiPoint': return new ol.geom.MultiPoint(coordinates);
case 'MultiPolygon': return new ol.geom.MultiPolygon(coordinates);
case 'Point': return new ol.geom.Point(coordinates);
case 'Polygon': return new ol.geom.Polygon(coordinates);
default:
console.error('[createFromType] Unsupported type: '+type);
return null;
}
};
/** Intersect 2 lines
* @param {Arrar<ol.coordinate>} d1
* @param {Arrar<ol.coordinate>} 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) || (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) || (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[0]) p[0] = extent[0];
else if (p[0]>extent[2]) p[0] = extent[2];
if (p[1]<extent[1]) p[1] = extent[1];
else 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>}
*/
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<nb; i++) {
pts.push([p1[0] + dx*i, p1[1] + dy*i])
}
}
}
pts.push(p2);
return pts;
};
/** Sample a LineString at a distance
* @param {number} d
* @returns {ol.geom.LineString}
*/
ol.geom.LineString.prototype.sampleAt = function(d) {
var line = this.getCoordinates();
var result = [];
for (var i=1; i<line.length; i++) {
result = result.concat(ol.coordinate.sampleAt(line[i-1], line[i], d, i===1));
}
return new ol.geom.LineString(result);
};
/** Sample a MultiLineString at a distance
* @param {number} d
* @returns {ol.geom.MultiLineString}
*/
ol.geom.MultiLineString.prototype.sampleAt = function(d) {
var lines = this.getCoordinates();
var result = [];
lines.forEach(function(p) {
var l = [];
for (var i=1; i<p.length; i++) {
l = l.concat(ol.coordinate.sampleAt(p[i-1], p[i], d, i===1));
}
result.push(l);
})
return new ol.geom.MultiLineString(result);
};
/** Sample a Polygon at a distance
* @param {number} d
* @returns {ol.geom.Polygon}
*/
ol.geom.Polygon.prototype.sampleAt = function(res) {
var poly = this.getCoordinates();
var result = [];
poly.forEach(function(p) {
var l = [];
for (var i=1; i<p.length; i++) {
l = l.concat(ol.coordinate.sampleAt(p[i-1], p[i], res, i===1));
}
result.push(l);
})
return new ol.geom.Polygon(result);
};
/** Sample a MultiPolygon at a distance
* @param {number} res
* @returns {ol.geom.MultiPolygon}
*/
ol.geom.MultiPolygon.prototype.sampleAt = function(res) {
var mpoly = this.getCoordinates();
var result = [];
mpoly.forEach(function(poly) {
var a = [];
result.push(a);
poly.forEach(function(p) {
var l = [];
for (var i=1; i<p.length; i++) {
l = l.concat(ol.coordinate.sampleAt(p[i-1], p[i], res, i===1));
}
a.push(l);
})
});
return new ol.geom.MultiPolygon(result);
};
/** Intersect a geometry using a circle
* @param {ol.geom.Geometry} geom
* @param {number} resolution circle resolution to sample the polygon on the circle, default 1
* @returns {ol.geom.Geometry}
*/
ol.geom.Circle.prototype.intersection = function(geom, resolution) {
if (geom.sampleAt) {
var ext = ol.extent.buffer(this.getCenter().concat(this.getCenter()), this.getRadius());
geom = ol.extent.intersection(ext, geom);
geom = geom.simplify(resolution);
var c = this.getCenter();
var r = this.getRadius();
//var res = (resolution||1) * r / 100;
var g = geom.sampleAt(resolution).getCoordinates();
switch (geom.getType()) {
case 'Polygon': g = [g];
// fallthrough
case 'MultiPolygon': {
var hasout = false;
// var hasin = false;
var result = [];
g.forEach(function(poly) {
var a = [];
result.push(a);
poly.forEach(function(ring) {
var l = [];
a.push(l);
ring.forEach(function(p) {
var d = ol.coordinate.dist2d(c, p);
if (d > 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<ol.Coordinate>} 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<pt.length; i++) {
var r = [];
for (var k=0; k<result.length; k++) {
var ri = result[k].splitAt(pt[i], tol);
r = r.concat(ri);
}
result = r;
}
return result;
}
// Nothing to do
if (ol.coordinate.equal(pt,this.getFirstCoordinate())
|| ol.coordinate.equal(pt,this.getLastCoordinate())) {
return [this];
}
// Get
var c0 = this.getCoordinates();
var ci=[c0[0]];
var c = [];
for (i=0; i<c0.length-1; i++) {
// Filter equal points
if (ol.coordinate.equal(c0[i],c0[i+1])) continue;
// Extremity found
if (ol.coordinate.equal(pt,c0[i+1])) {
ci.push(c0[i+1]);
c.push(new ol.geom.LineString(ci));
ci = [];
}
// Test alignement
else if (!ol.coordinate.equal(pt,c0[i])) {
var d1, d2, split=false;
if (c0[i][0] == c0[i+1][0]) {
d1 = (c0[i][1]-pt[1]) / (c0[i][1]-c0[i+1][1]);
split = (c0[i][0] == pt[0]) && (0 < d1 && d1 <= 1)
} else if (c0[i][1] == c0[i+1][1]) {
d1 = (c0[i][0]-pt[0]) / (c0[i][0]-c0[i+1][0]);
split = (c0[i][1] == pt[1]) && (0 < d1 && d1 <= 1)
} else {
d1 = (c0[i][0]-pt[0]) / (c0[i][0]-c0[i+1][0]);
d2 = (c0[i][1]-pt[1]) / (c0[i][1]-c0[i+1][1]);
split = (Math.abs(d1-d2) <= tol && 0 < d1 && d1 <= 1)
}
// pt is inside the segment > 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<coords.length; i++) {
// Add a separator
coord.push([]);
// Add the hole
coord = coord.concat(coords[i]);
}
// Extent
var ext = geom.getExtent();
// Split polygon with horizontal lines
var lines = [];
for (var y = (Math.floor(ext[1]/step)+1)*step; y<ext[3]; y += step) {
l = ol.coordinate.splitH(coord, y, i);
lines = lines.concat(l);
}
if (!lines.length) return null;
// Order lines on segment index
var mod = coord.length-1;
var first = lines[0][0].index;
for (k=0; l=lines[k]; k++) {
lines[k][0].index = (lines[k][0].index-first+mod) % mod;
lines[k][1].index = (lines[k][1].index-first+mod) % mod;
}
var scribble = [];
while (true) {
for (k=0; l=lines[k]; k++) {
if (!l[0].done) break;
}
if (!l) break;
var scrib = [];
while (l) {
l[0].done = true;
scrib.push(l[0].pt);
scrib.push(l[1].pt);
var nexty = l[0].pt[1] + step;
var d0 = Infinity;
var l2 = null;
while (lines[k]) {
if (lines[k][0].pt[1] > 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<d0 && d<d2) {
d0 = d;
if (!lines[k][0].done) l2 = lines[k];
else l2 = null;
}
}
k++;
}
l = l2;
}
if (scrib.length) {
scribble.push(scrib);
}
}
// Return the scribble as MultiLineString
if (!scribble.length) return null;
var mline = new ol.geom.MultiLineString(scribble);
mline.rotate(-angle,[0,0]);
return mline.cspline({ pointsPerSeg:8, tension:.9 });
};
/** Calculate a MultiPolyline to fill a geomatry (Polygon or MultiPolygon) with a scribble effect that appears hand-made
* @param {ol.geom.Geometry} geom the geometry to scribble
* @param {Object} options
* @param {Number} options.interval interval beetween lines
* @param {Number} options.angle hatch angle in radian, default PI/2
* @return {ol.geom.Geometry} the resulting MultiLineString geometry or initial geometry
*/
ol.geom.scribbleFill = function(geom, options) {
switch (geom.getType()) {
case 'Polygon': {
return ol.geom.Polygon.prototype.scribbleFill.call(geom, options)
}
case 'MultiPolygon': {
return ol.geom.MultiPolygon.prototype.scribbleFill.call(geom, options)
}
default: return geom
}
}
//
/** An object to simplify geometry
* @extends {ol.Object}
* @param {Object=} options
* @api
*/
ol.geom.Simplificator = class olgeomSimplificator extends ol.Object {
constructor(options) {
super(options);
this._edges = [];
}
/** Get source edge
*/
getEdges() {
return this._edges;
}
/** Set the features to process
* @param {Array<ol.Feature>} 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<ol.Feature>}
*/
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<coord.length; i++) {
coordinates.push(coord[i]);
}
} else if (ol.coordinate.equal(coordinates[0], coord[0])) {
for (var i=1; i<coord.length; i++) {
coordinates.unshift(coord[i]);
}
} else if (ol.coordinate.equal(coordinates[0], coord[coord.length-1])) {
for (var i=coord.length-2; 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<ol.Features>} features
* @returns {Array<Object>}
* @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<c.length; i++) {
p1 = c[i];
// p1 = round ? [Math.round(c[i][0] * round) / round, Math.round(c[i][1] * round) / round] : c[i];
if (!ol.coordinate.equal(p0, p1)) {
arcs.push({ seg: [p0, p1], contour: ct });
}
p0 = p1;
}
});
}
return arcs
}
/** Chain edges backward
* @param {*} edges
* @returns {Array<ol.Feature>}
*/
_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<geohash.length; i++) {
var chr = geohash.charAt(i);
var idx = ol.geohash.base32.indexOf(chr);
if (idx == -1) return false;
for (var n=4; n>=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.coordinate>}
*/
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 [VisvalingamWhyatt](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<g0.length; i++) {
g.push(g0[i].cspline(options));
}
return new ol.geom.GeometryCollection(g);
};
ol.geom.MultiLineString.prototype.calcCSpline_ = function(options) {
var g=[], lines = this.getLineStrings();
for (var i=0; i<lines.length; i++) {
g.push(lines[i].cspline(options).getCoordinates());
}
return new ol.geom.MultiLineString(g);
};
ol.geom.Polygon.prototype.calcCSpline_ = function(options){
var g=[], g0=this.getCoordinates();
for (var i=0; i<g0.length; i++){
g.push((new ol.geom.LineString(g0[i])).cspline(options).getCoordinates());
}
return new ol.geom.Polygon(g);
};
ol.geom.MultiPolygon.prototype.calcCSpline_ = function(options) {
var g=[], g0=this.getPolygons();
for (var i=0; i<g0.length; i++) {
g.push(g0[i].cspline(options).getCoordinates());
}
return new ol.geom.MultiPolygon(g);
}
/** Calculate cspline on coordinates
* @param {Array<ol.geom.Geometry.coordinate>} 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.geom.Geometry.coordinate>}
*/
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<d2) d = d1/d2;
else d = d2/d1;
}
// calc tension vectors
t1x = (pts[i+1][0] - pts[i-1][0]) * tension *d;
t2x = (pts[i+2][0] - pts[i][0]) * tension *d;
t1y = (pts[i+1][1] - pts[i-1][1]) * tension *d;
t2y = (pts[i+2][1] - pts[i][1]) * tension *d;
for (t=0; t <= numOfSegments; t++) {
// calc step
st = t / numOfSegments;
// calc cardinals
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1;
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2);
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st;
c4 = Math.pow(st, 3) - Math.pow(st, 2);
// calc x and y cords with common control vectors
x = c1 * pts[i][0] + c2 * pts[i+1][0] + c3 * t1x + c4 * t2x;
y = c1 * pts[i][1] + c2 * pts[i+1][1] + c3 * t1y + c4 * t2y;
//store points in array
if (x && y) res.push([x,y]);
}
}
return res;
};
/** @private */
ol.geom.LineString.prototype.calcCSpline_ = function(options) {
var line = this.getCoordinates();
var res = ol.coordinate.cspline(line, options)
return new ol.geom.LineString(res);
}
// To use this module with ol/geom/Geometry
//
/* 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).
*/
/** @typedef {'pointy' | 'flat'} HexagonLayout
* Layout of a Hexagon. Flat means the bottom part of the hexagon is flat.
*/
/**
* Hexagonal grids
* @classdesc ol.HexGrid is a class to compute hexagonal grids
* @see http://www.redblobgames.com/grids/hexagons
*
* @constructor ol.HexGrid
* @extends {ol.Object}
* @param {Object} [options]
* @param {number} [options.size] size of the exagon in map units, default 80000
* @param {ol.Coordinate} [options.origin] orgin of the grid, default [0,0]
* @param {HexagonLayout} [options.layout] grid layout, default pointy
*/
ol.HexGrid = class olHexGrid extends ol.Object {
constructor(options) {
options = options || {};
super(options);
// Options
this.size_ = options.size || 80000;
this.origin_ = options.origin || [0, 0];
this.layout_ = this.layout[options.layout] || this.layout.pointy;
}
/** Set layout
* @param {HexagonLayout | undefined} layout name, default pointy
*/
setLayout(layout) {
this.layout_ = this.layout[layout] || this.layout.pointy;
this.changed();
}
/** Get layout
* @return {HexagonLayout} layout name
*/
getLayout() {
return (this.layout_[9] != 0 ? 'pointy' : 'flat');
}
/** Set hexagon origin
* @param {ol.Coordinate} coord origin
*/
setOrigin(coord) {
this.origin_ = coord;
this.changed();
}
/** Get hexagon origin
* @return {ol.Coordinate} coord origin
*/
getOrigin() {
return this.origin_;
}
/** Set hexagon size
* @param {number} hexagon size
*/
setSize(s) {
this.size_ = s || 80000;
this.changed();
}
/** Get hexagon size
* @return {number} hexagon size
*/
getSize() {
return this.size_;
}
/** Convert cube to axial coords
* @param {ol.Coordinate} c cube coordinate
* @return {ol.Coordinate} axial coordinate
*/
cube2hex(c) {
return [c[0], c[2]];
}
/** Convert axial to cube coords
* @param {ol.Coordinate} h axial coordinate
* @return {ol.Coordinate} cube coordinate
*/
hex2cube(h) {
return [h[0], -h[0] - h[1], h[1]];
}
/** Convert offset to axial coords
* @param {ol.Coordinate} h axial coordinate
* @return {ol.Coordinate} offset coordinate
*/
hex2offset(h) {
if (this.layout_[9])
return [h[0] + (h[1] - (h[1] & 1)) / 2, h[1]];
else
return [h[0], h[1] + (h[0] + (h[0] & 1)) / 2];
}
/** Convert axial to offset coords
* @param {ol.Coordinate} o offset coordinate
* @return {ol.Coordinate} axial coordinate
*/
offset2hex(o) {
if (this.layout_[9])
return [o[0] - (o[1] - (o[1] & 1)) / 2, o[1]];
else
return [o[0], o[1] - (o[0] + (o[0] & 1)) / 2];
}
/** Convert offset to cube coords
* @param {ol.Coordinate} c cube coordinate
* @return {ol.Coordinate} offset coordinate
* /
cube2offset(c) {
return this.hex2offset(this.cube2hex(c));
};
/** Convert cube to offset coords
* @param {ol.Coordinate} o offset coordinate
* @return {ol.Coordinate} cube coordinate
* /
offset2cube(o) {
return this.hex2cube(this.offset2Hex(o));
};
/** Round cube coords
* @param {ol.Coordinate} h cube coordinate
* @return {ol.Coordinate} rounded cube coordinate
*/
cube_round(h) {
var rx = Math.round(h[0]);
var ry = Math.round(h[1]);
var rz = Math.round(h[2]);
var x_diff = Math.abs(rx - h[0]);
var y_diff = Math.abs(ry - h[1]);
var z_diff = Math.abs(rz - h[2]);
if (x_diff > 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<ol.Coordinate>}
*/
getHexagonAtCoord(coord) {
return (this.getHexagon(this.coord2hex(coord)));
}
/** Get hexagon coordinates at hex
* @param {ol.Coordinate} hex
* @return {Arrary<ol.Coordinate>}
*/
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<ol.Coordinate>} 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<ol.Coordinate> } 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<ol.Coordinate> } 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<ol.style.Style>} 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<style.length; i++)
{ var imgs = style[i].getImage();
var sc = imgs.getScale();
imgs.setScale(sc*ratio);
event.vectorContext.setStyle(style[i]);
event.vectorContext.drawGeometry(new ol.geom.Point(coords));
imgs.setScale(sc);
}
context.restore();
// tell OL3 to continue postcompose animation
if (elapsed >= 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<ol.style.Style>} 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<style.length; i++) {
var imgs = style[i].getImage();
var sc = imgs.getScale();
imgs.setScale(ratio*sc*(1+amplitude*(e-1)));
event.vectorContext.setStyle(style[i]);
event.vectorContext.drawGeometry(new ol.geom.Point(coords));
imgs.setScale(sc);
}
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 */ }
}
/* 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).
*
* Add a chart style to display charts (pies or bars) on a map
*/
/**
* @classdesc
* Set chart style for vector features.
*
* @constructor
* @param {} options
* @param {String} options.type Chart type: pie,pie3D, donut or bar
* @param {number} options.radius Chart radius/size, default 20
* @param {number} options.rotation Rotation in radians (positive rotation clockwise). Default is 0.
* @param {bool} options.snapToPixel use integral numbers of pixels, default true
* @param {_ol_style_Stroke_} options.stroke stroke style
* @param {String|Array<ol.color>} options.colors predefined color set "classic","dark","pale","pastel","neon" / array of color string, default classic
* @param {Array<number>} 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<number>}
*/
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<number>} 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<circles>} [options.circles]
* @param {Array<pointlist>} [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<ol.coordinate>} 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<ol.coordinate>} 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<number>} [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<number>} [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<ol.coordinate>} 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; k<c.length; k++)
{ path1.push(c2p[0]*c[k][0]+c2p[1]*c[k][1]+c2p[4]);
path1.push(c2p[2]*c[k][0]+c2p[3]*c[k][1]+c2p[5]);
}
// Revert line ?
if (readable && path1[0]>path1[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.style.Style>|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<Number>} 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+di<dl)
{ for (; pos<path.length; )
{ di = dist2D(path[pos-2],path[pos-1],path[pos],path[pos+1]);
if (dpos+di>dl) 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<path.length; i+=2)
{ d += dist2D(path[i-2],path[i-1],path[i],path[i+1])
}
if (d < ctx.minWidth) return;
var nbspace = text.split(" ").length -1;
// Remove char for overflow
if (ctx.textOverflow != "visible")
{ if (d < ctx.measureText(text).width + (text.length-1 + nbspace) * letterPadding)
{ var overflow = (ctx.textOverflow=="ellipsis") ? '\u2026' : ctx.textOverflow;
do
{ nbspace = text.split(" ").length -1;
text = text.slice(0,text.length-1);
} while (text && d < ctx.measureText(text+overflow).width + (text.length + overflow.length-1 + nbspace) * letterPadding)
text += overflow;
}
}
switch (ctx.textJustify || ctx.textAlign)
{ case true: // justify
case "center":
case "end":
case "right":
{ // Text align
if (ctx.textJustify)
{ start = 0;
letterPadding = (d - ctx.measureText(text).width) / (text.length-1 + nbspace);
}
else
{ start = d - ctx.measureText(text).width - (text.length + nbspace) * letterPadding;
if (ctx.textAlign == "center") start /= 2;
}
break;
}
default: break;
}
for (var t=0; t<text.length; t++)
{ var letter = text[t];
var wl = ctx.measureText(letter).width;
var p = getPoint(path, start+wl/2);
ctx.save();
ctx.textAlign = "center";
ctx.translate(p[0], p[1]);
ctx.rotate(p[2]);
if (ctx.lineWidth) ctx.strokeText(letter,0,0);
ctx.fillText(letter,0,0);
ctx.restore();
start += wl+letterPadding*(letter==" "?2: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).
*
* Shadow image style for point vector features
*/
/**
* @classdesc
* Set Shadow style for point vector features.
*
* @constructor
* @param {} options Options.
* @param {ol.style.Fill | undefined} options.fill fill style, default rgba(0,0,0,0.5)
* @param {number} options.radius point radius
* @param {number} options.blur lur radius, default radius/3
* @param {Array<number>} [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<number>} 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>}
*/
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.Style>}
*/
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<geo.length-1; i++) {
x = geo[i+1][0]-geo[i][0];
y = geo[i+1][1]-geo[i][1];
dl += Math.sqrt(x*x+y*y);
if (dl>=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));
})();