/**
 * 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.21
 * @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);
};
/** Set cursor
 * @param {Element|ol/Map} elt
 * @param {string} cursor
 */
ol.ext.element.setCursor = function(elt, cursor) {
  if (elt instanceof ol.Map) elt = elt.getTargetElement()
  // prevent flashing on mobile device
  if (!('ontouchstart' in window) && elt instanceof Element) {
    elt.style.cursor = cursor;
  }
}

/** Get a canvas overlay for a map (non rotated, on top of the map)
 * @param {ol.Map} map
 * @return {canvas}
 */
ol.ext.getMapCanvas = function(map) {
  if (!map) return null;
  var canvas = map.getViewport().getElementsByClassName('ol-fixedoverlay')[0];
  if (!canvas) {
    if (map.getViewport().querySelector('.ol-layers')) {
      // Add a fixed canvas layer on top of the map
      canvas = document.createElement('canvas');
      canvas.className = 'ol-fixedoverlay';
      map.getViewport().querySelector('.ol-layers').after(canvas);
      // Clear before new compose
      map.on('precompose', function (e){
        canvas.width = map.getSize()[0] * e.frameState.pixelRatio;
        canvas.height = map.getSize()[1] * e.frameState.pixelRatio;
      });
    } else {
      canvas = map.getViewport().querySelector('canvas');
    }
  }
  return canvas;
};
  
ol.ext.olVersion = ol.util.VERSION.split('.');
ol.ext.olVersion = parseInt(ol.ext.olVersion[0])*100 + parseInt(ol.ext.olVersion[1]);
/** Get style to use in a VectorContext
 * @param {} e
 * @param {ol.style.Style} s
 * @return {ol.style.Style}
 */
ol.ext.getVectorContextStyle = function(e, s) {
  var ratio = e.frameState.pixelRatio;
  // Bug with Icon images
  if (ol.ext.olVersion > 605 
    && ol.ext.olVersion < 700 
    && ratio !== 1 
    && (s.getImage() instanceof ol.style.Icon)) {
    s = s.clone();
    var img = s.getImage();
    img.setScale(img.getScale()*ratio);
    /* BUG anchor don't use ratio */
    var anchor = img.getAnchor();
    if (anchor && img.setDisplacement) {
      var disp = img.getDisplacement();
      if (disp) {
        disp[0] -= anchor[0]/ratio;
        disp[1] += anchor[1]/ratio;
        img.setAnchor([0,0]);
      }
    } else {
      if (anchor) {
        anchor[0] /= ratio;
        anchor[1] /= ratio;
      }
    }
    /**/
  }
  return s;
}

/** Helper for loading BIL-32 (Band Interleaved by Line) image
 * @param {string} src
 * @param {function} onload a function that takes a Float32Array and a ol.size.Size (array size)
 * @param {function} onerror
 * @private
 */
ol.ext.imageLoader.loadBILImage = function(src, onload, onerror) {
  var size = [
    parseInt(src.replace(/.*WIDTH=(\d*).*/i,'$1')), 
    parseInt(src.replace(/.*HEIGHT=(\d*).*/i,'$1'))
  ];
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'blob';
  xhr.addEventListener('loadend', function () {
    var resp = this.response;
    if (resp !== undefined) {
      var reader = new FileReader();
      // Get as array
      reader.addEventListener('loadend', (e) => {
        var data = new Float32Array(e.target.result);
        onload(data, size);
      });
      // Start reading the blob
      reader.readAsArrayBuffer(resp);
      // tile.getImage().src = URL.createObjectURL(blob);
    } else {
      onerror();
    }
  });
  xhr.addEventListener('error', function () {
    onerror();
  });
  xhr.open('GET', src);
  xhr.send();
};
/** Helper for loading image
 * @param {string} src
 * @param {function} onload a function that takes a an image and a ol.size.Size
 * @param {function} onerror
 * @private
 */
ol.ext.imageLoader.loadImage = function(src, onload, onerror) {
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'blob';
  xhr.addEventListener('loadend', function () {
    var resp = this.response;
    if (resp !== undefined) {
      var img = new Image();
      img.onload = function() {
        onload(img, [img.naturalWidth, img.naturalHeight]);
      }
      img.src = URL.createObjectURL(resp);
    } else {
      onerror();
    }
  });
  xhr.addEventListener('error', function () {
    onerror();
  });
  xhr.open('GET', src);
  xhr.send();
};
/** Get a TileLoadFunction to transform tiles images
 * @param {function} setPixel a function that takes a Uint8ClampedArray and the pixel position to transform
 * @returns {function} an ol/Tile~LoadFunction
 */
ol.ext.imageLoader.pixelTransform = function(setPixel) {
  return function(tile, src) {
    ol.ext.imageLoader.loadImage(
      src, 
      function(img, size) {
        var canvas = document.createElement('canvas');
        canvas.width = size[0];
        canvas.height = size[1];
        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        var imgData = ctx.getImageData(0, 0, size[0], size[1]);
        var pixels = imgData.data;
        for (var i = 0; i < pixels.length; i += 4) {
          setPixel(pixels, i, size);
        }
        ctx.putImageData(imgData, 0, 0);
        tile.setImage(canvas);
      },
      function() {
        tile.setState(3);
      }
    );
  }
};
/** Get a TileLoadFunction to transform tiles into grayscale images
 * @returns {function} an ol/Tile~LoadFunction
 */
ol.ext.imageLoader.grayscale = function() {
  return ol.ext.imageLoader.pixelTransform(function(pixels, i) {
    pixels[i] = pixels[i + 1] = pixels[i + 2] = parseInt(3*pixels[i] + 4*pixels[i + 1] + pixels[i + 2] >>> 3);
  })
};
/** Get a TileLoadFunction to turn color or a color range transparent
 * @param {ol.color.Color|Array<ol.color.Color>} colors color or color range to turn transparent
 * @returns {function} an ol/Tile~LoadFunction
 */
ol.ext.imageLoader.transparent = function(colors) {
  var color1, color2;
  if (colors instanceof Array) {
    color1 = colors[0];
    color2 = colors[1];
  }
  var color = color1 = ol.color.asArray(color1);
  if (!color2) {
    return ol.ext.imageLoader.pixelTransform(function(pixels, i) {
      if (pixels[i]===color[0] && pixels[i+1]===color[1] && pixels[i+2]===color[2]) {
        pixels[i+3] = 0;
      }
    })
  } else {
    color2 = ol.color.asArray(color2);
    color = [Math.min(color1[0], color2[0]), Math.min(color1[1], color2[1]), Math.min(color1[2], color2[2])];
    color2 = [Math.max(color1[0], color2[0]), Math.max(color1[1], color2[1]), Math.max(color1[2], color2[2])];
    return ol.ext.imageLoader.pixelTransform(function(pixels, i) {
      if (pixels[i]>=color1[0] && pixels[i]<=color2[0] 
        && pixels[i+1]>=color[1] && pixels[i+1]<=color2[1] 
        && pixels[i+2]>=color[2] && pixels[i+2]<=color2[2]) {
        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;
      input.focus();
      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);
    this.input.disabled = true;
    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;

/** An input as a list of values
 * @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);
  }
  /**
   * Test if the control is disabled.
   * @return {bool}
   * @api stable
   */
  getDisable() {
    var button = this.element.querySelector('button');
    return button && button.disabled;
  }
  /** Disable the control button. 
   * @param {bool} b disable (or enable) the control, default false (enable)
   * @api stable
   */
  setDisable(b) {
    if (this.getDisable() == b) return;
    this.element.querySelector('button').disabled = b;
  }
  /**
   * 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 1 or 2 or geoplateforme (latest), default latest
 *	@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
 *	@param {string} [options.terr] territory METROPOLE|DOMTOM|dep code
 *  @param {boolean} [options.position] Search, with priority to geo position (map center), default false
 *	@param {ol.extent} [options.bbox] if set search inside the bbox (in map projection)
 *	@param {boolean} [options.useExtent] returns candidates inside the current map extent, default false
 * @see {@link https://geoservices.ign.fr/documentation/geoservices/geocodage.html}
 * @see {@link https://geoservices.ign.fr/documentation/services/services-deprecies/itineraires-deprecies/autocompletion-rest}
 * @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';
      options.copy = '<a href="https://www.geoportail.gouv.fr/" target="new">&copy; IGN-Géoportail</a>';
    } else if (options.version == 2) {
      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>';
    } else {
      options.url = 'https://data.geopf.fr/geocodage/completion';
      options.copy = '<a href="https://geoservices.ign.fr/" target="new">&copy; IGN-Géoplateforme</a>';
    }
    super(options);
    this.set('position', options.position);
    this.set('useExtent', options.useExtent);
    this.set('bbox', options.bbox)
    this.set('type', options.type || 'StreetAddress,PositionOfInterest');
    this.set('terr', options.terr);
    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) {
    var rdata = {
      text: s,
      type: this.get('type') === 'Commune' ? 'PositionOfInterest' : this.get('type') || 'StreetAddress,PositionOfInterest',
      terr: this.get('terr') || undefined,
      maximumResponses: this.get('maxItems')
    };
    if (this.get('type') === 'Commune') rdata.poiType = 'commune';
    if (this.get('position')) {
      var center = this.getMap().getView().getCenter()
      rdata.lonlat = ol.proj.transform(center, this.getMap().getView().getProjection(), 'EPSG:4326').join(',');
    }
    if (this.get('bbox')) {
      rdata.bbox = ol.proj.transformExtent(this.get('bbox'), this.getMap().getView().getProjection(), 'EPSG:4326').join(',')
    } else if (this.get('useExtent')) {
      var bbox = this.getMap().getView().calculateExtent()
      rdata.bbox = ol.proj.transformExtent(bbox, this.getMap().getView().getProjection(), 'EPSG:4326').join(',')
    }
    return rdata;
  }
  /**
   * 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;
  }
  /** 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);
        }.bind(this));
      } else {
        super.select(f, reverse, c, 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 + '&index=parcel&limit=1', 
        {},
        function (resp) {
          try {
            var r = JSON.parse(resp).features[0];
            f.insee = r.properties.departmentcode + r.properties.municipalitycode
            f.districtcode = r.properties.districtcode
            // 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
          // Get the HTML node within web component on click drag
          if(e.target.shadowRoot){
            li = e.composedPath()[0]
          }
        } else {
          li = document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY);
          //Get actual HTML node within web component on touch drag
          while(li.shadowRoot){
            li = li.shadowRoot.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}
   * @private
   */
  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)
    }
  }
}

/** Feature list control
 *
 * @constructor
 * @extends {ol.control.Control}
 * @fires select
 * @fires dblclick
 * @fires collapse
 * @fires resize
 * @fires sort
 * @param {Object=} options
 *  @param {number} [options.title] table title
 *  @param {Element} [options.target] to display the control outside the map
 *  @param {string} [options.className] use `ol-bottom` to scroll at bottom (default top)
 *  @param {boolean} [options.collapsed=true] collapse the list on start, default true
 *  @param {Array<ol.Feature>|ol.Collection<ol.Feature>|ol.source.Vector} [features] a set of feature to display. If provided as Source or Collection the features will stay in sync.
 *  @param {number} [options.pageLength=100] number of row to display in the table (page optimzation)
 */
ol.control.FeatureList = class olcontrolFeatureList extends ol.control.Control {
  constructor(options) {
    options = options || {};
    // Control element
    var element = ol.ext.element.create('DIV', {
      className: ((options.className || '') + ' ol-feature-list').trim()
    })
    if (!options.target) {
      element.classList.add('ol-unselectable');
      element.classList.add('ol-control');
      element.dataset.control = 1;
    }
    super({
      element: element,
      target: options.target
    });
    // List of features / sort
    this._listFeatures = [];
    // Current columns
    this._columns = [];
    // Bottom scroll
    this._bottomScroll = element.classList.contains('ol-bottom');
    // Page lengeh
    this.set('pageLength', options.pageLength || 100);
    // Enable sort
    this._canSort = [];
    this._sort = {};
    // Button
    if (!options.target) {
      ol.ext.element.create('BUTTON', {
        type: 'button',
        click: function() {
          this.collapse()
        }.bind(this),
        parent: element
      })
    }
    // List
    var content = ol.ext.element.create('DIV', {
      className: 'ol-content',
      parent: element
    });
    // Menu header
    this._menu = ol.ext.element.create('DIV', {
      className: 'ol-header',
      html: '<div class="ol-buttons"></div>',
      parent: content
    })
    ol.ext.element.create('P', {
      text: options.title || '',
      parent: this._menu
    })
    this.addButton({
        className: 'ol-closebox',
        type: 'button',
        click: function() {
          this.collapse()
        }.bind(this),
        parent: this._menu
    })
    // Sizer
    var sizer = ol.ext.element.create('DIV', {
      className: 'ol-sizer',
      parent: content
    })
    sizer.addEventListener('mousedown', this._dragSizer.bind(this));
    sizer.addEventListener('touchstart', this._dragSizer.bind(this));
    // List array
    var list = this._list = ol.ext.element.create('TABLE', {
      className: 'ol-list',
      parent: ol.ext.element.create('DIV', {
        className: 'ol-scroll-container',
        tabindex: -1,
        parent: content
      })
    });
    list.parentNode.addEventListener('scroll', this._drawPage.bind(this))
    this._head = ol.ext.element.create('THEAD', {
      parent: list
    })
    this._listbody = ol.ext.element.create('TBODY', {
      parent: list
    })
    // Collapse
    this.collapse(options.collapsed !== false);
    // Source
    this.setFeatures(options.features)
  }
  /**
   * 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);
    map.addEventListener('change:size', this.resize.bind(this));
    this.resize();
  }
};
/** Scroll resize at botton
 * @param {boolean} b
 */
ol.control.FeatureList.prototype.setBottomScroll = function(b) {
  this._bottomScroll = !!b
  if (this._bottomScroll) {
    this.element.classList.add('ol-bottom')
  } else {
    this.element.classList.remove('ol-bottom')
  }
}
/** Scroll to an element / feature
 * @param {ol.Feature|string} feature a featue or `select` to scroll at the selected row
 */
ol.control.FeatureList.prototype.scrollTo = function(feature) {
  if (feature === 'select') {
    if (this._curSelection) return this.scrollTo(this._curSelection.feature);
    else return false;
  } else {
    var i = this._findFeatureIndex(feature)
    if (i >= 0) {
      var scrollDiv = this._list.parentNode;
      scrollDiv.scrollTo(scrollDiv.scrollLeft, (i+1.5)*this.getRowHeight() - scrollDiv.getBoundingClientRect().height/2)
      var fpage = this._findInPage(feature)
      return {
        feature: feature,
        tr: fpage ? fpage.tr : ol.ext.element.create('TR')
      }
    }
  }
  return false;
}
/** Find the feature index in the list
 * @param {ol.Feature} feature
 * @returns {Object}
 * @private
 */
ol.control.FeatureList.prototype._findFeatureIndex = function(feature) {
  var list = this._listFeatures;
  if (list) {
    for (var i=0; i<list.length; i++) {
      if (list[i] === feature) {
        return i
      }
    }
  }
  return -1;
}
/** Find a feature in the current page
 * @param {ol.Feature} feature
 * @returns {Object} 
 */
ol.control.FeatureList.prototype._findInPage = function(feature) {
  var list = this._page;
  if (list) {
    for (var i=0; i<list.length; i++) {
      if (list[i].feature === feature) {
        return list[i]
      }
    }
  }
  return false;
}
/** Collapse the table
 * @param {boolean} [b] if no parameters toggle it
 */
ol.control.FeatureList.prototype.collapse = function(b) {
  if (b === undefined) b = !this._collapsed;
  if (b !== this._collapsed) {
    this._collapsed = b;
    if (this._collapsed) {
      this.element.classList.add('ol-collapsed')
    } else {
      this.element.classList.remove('ol-collapsed')
    }
    this.dispatchEvent({
      type: 'collapse',
      collapsed: this._collapsed
    })
    if (!this._collapsed) this.refresh();
  }
}
/** Is the control collapsed
 */
ol.control.FeatureList.prototype.isCollapsed = function() {
  return this._collapsed;
}
/** Set the features list
 * @param {ol.source.Vector|ol.Collection<ol.Feature>|Array<ol.Feature>} source a vector source or a collection of features to list
 */
ol.control.FeatureList.prototype.setFeatures = function(source) {
  if (!this._drawbind) {
    this._drawbind = this._drawList.bind(this)
  }
  // Remove listeners
  if (this._source && this._source.un) {
    this._source.un(['loadend', 'addfeature', 'removefeature', 'add', 'remove'], this._drawbind)
  }
  // New source
  this._source = source || [];
  this._drawList();
  // Refresh on change
  if (this._source.on) {
    this._source.on(['loadend', 'addfeature', 'removefeature', 'add', 'remove'], this._drawbind)
  }
}
/** Sort list by properties
 * @param {string} prop property name
 * @param {string} [sort] 'up' or 'down', default remove sort
 * 
 */
ol.control.FeatureList.prototype.sortBy = function(prop, sort) {
  if (!sort) {
    delete this._sort[prop]
  } else {
    this._sort[prop] = (sort !== 'down');
  }
  this.sort(true);
  this.dispatchEvent({ type: 'sort', property: prop, sort: sort });
}
/** Get sorted properties list
 * @return Object
 */
ol.control.FeatureList.prototype.getSort = function() {
  return this._sort;
}
/** Reset all sorts
 */
ol.control.FeatureList.prototype.resetSort = function() {
  this._sort = {};
  this.sort();
}
/** Enable sort list by properties in the header
 * @param {...string} propName 
 */
ol.control.FeatureList.prototype.enableSort = function() {
  this._canSort = [...arguments];
  Object.keys(this._sort).forEach(function(s) {
    if (this._canSort.indexOf(s) < 0) {
      delete this._sort[s];
    }
  }.bind(this))
  this.sort();
}
/** Draw the list
 * @param {boolean} delay to prevent to many redraw
 * @private
 */
ol.control.FeatureList.prototype._drawList = function(delay) {
  if (delay) {
    clearTimeout(this._drawout);
    this._drawout = setTimeout(this._drawList.bind(this), 100);
    return;
  }
  var features = this._source.getFeatures ? this._source.getFeatures() : this._source;
  this._columns = this.getColumns(features);
  // Head
  this._drawHead()
  // List of features
  this._listFeatures = [];
  features.forEach(function (f) {
    this._listFeatures.push(f)
  }.bind(this))
  // Sort
  this.sort();
}
/** Update a feature line in the table (attribute changed)
 * @param {ol.Feature} feature
 * @returns {boolean}
 */
ol.control.FeatureList.prototype.updateFeature = function(feature) {
  var pfeature = this._findInPage(feature)
  if (pfeature) {
    this._updateFeature(pfeature.feature, pfeature.tr)
    return true;
  }
  return false;
}
/** Update a feature line in the table
 * @param {ol.Feature} feature
 * @param {Element} tr
 * @private
 */
ol.control.FeatureList.prototype._updateFeature = function(f, tr) {
  tr.innerHTML = '';
  this._columns.forEach(function(c) {
    var p = this.formatProperty(f, c);
    ol.ext.element.create('TD', {
      text: p.val,
      html: p.html,
      className: p.type,
      'data-prop': c,
      parent: tr
    })
  }.bind(this))
  // Selected ?
  if (this._curSelection && this._curSelection.feature === f) {
    this._curSelection.tr = tr;
    tr.classList.add('ol-selected')
  }
}
/** Get the list element
 * @param {ol.Feature} f
 * @returns {Element}
 */
ol.control.FeatureList.prototype._getLi = function(f) {
  var tr = ol.ext.element.create('TR', {
    on: {
      click: function (e) {
        var td = e.target.closest('TD');
        this.dispatchEvent({
          type: 'select',
          property: td.dataset.prop,
          feature: f,
          row: e.target.closest('TR'),
          col: td,
          originalEvent: e
        })
        this.select(f, true)
      }.bind(this),
      dblclick: function(e) {
        var td = e.target.closest('TD');
        this.dispatchEvent({
          type: 'dblclick',
          property: td.dataset.prop,
          feature: f,
          row: e.target.closest('TR'),
          col: td,
          originalEvent: e
        })
      }.bind(this)
    }
  })
  this._updateFeature(f, tr)
  return tr;
}
/** Draw columns heads
 * @private
 */
ol.control.FeatureList.prototype._drawHead = function() {
  var columns = this._columns;
  this._head.innerHTML = '';
  var tr = ol.ext.element.create('TR', {
    parent: this._head
  })
  columns.forEach(function(c) {
    var td = ol.ext.element.create('TD', {
      html: ol.ext.element.create('P', { text: c }),
      parent: tr
    })
    if (this._canSort.length && this._canSort.indexOf(c) >= 0) {
      td.classList.add('sort')
      var b = ol.ext.element.create('BUTTON', {
        className: 'sort' + (this._sort[c]===true ? ' sortup' : this._sort[c]===false ? ' sortdown' : ''),
        click: function() {
          var sort;
          if (b.classList.contains('sortup')) {
            sort = 'down';
          } else if (!b.classList.contains('sortdown')) {
            sort = 'up';
          } 
          this.sortBy(c, sort);
        }.bind(this),
        parent: td
      })
    }
  }.bind(this))
}
/** Sort features in the table
 * @param {boolean} [silent] sort without triggering an event
 */
ol.control.FeatureList.prototype.sort = function(silent) {
  var sort = Object.keys(this._sort)
  if (sort.length) {
    this._listFeatures.sort(function(a, b) {
      for (var i=0; i<sort.length; i++) {
        var p = sort[i];
        var s = this.sortFn(a, b, p);
        if (s) return this._sort[p] ? s : -s;
      }
      return 0;
    }.bind(this));
  }
  // Refresh list
  this.refresh(true)
  if (!silent) this.dispatchEvent({ type: 'sort' });
}
/** Refresh the list draw + resize use update if features have changed
 * @param {boolean} [force]
 */
ol.control.FeatureList.prototype.refresh = function(force) {
  if (force) this._curPage = -1;
  // Draw Head
  this._drawHead();
  // Draw list
  this._drawPage()
  // resize
  this.resize();
}
/** Update the list (when features have changed without dispatching event).
 */
ol.control.FeatureList.prototype.update = function() {
  this._drawList();
  if (this._curSelection && this._curSelection.feature) {
    this.select(this._curSelection.feature, true)
  }
}
/** Get height of a row
 * @return {number}
 */
ol.control.FeatureList.prototype.getRowHeight = function() {
  return this._head.getBoundingClientRect().height;
}
/** Pagination draw
 * @private
 */
ol.control.FeatureList.prototype._drawPage = function() {
  // Page length
  var nb = Math.round(this.get('pageLength') / 2);
  var nb2 = Math.round(nb/2);
  // Get current page on scrollTop
  var top = this._list.parentNode.scrollTop;
  var h = this.getRowHeight();
  var page = Math.round((top / h - nb2) / nb);
  if (page*nb > this._listFeatures.length) page = 0;
  // No changes
  if (page === this._curPage) return;
  // Draw page
  this._curPage = page;
  this._listbody.innerHTML = '';
  // Fist row to preserve space
  this._listbody.appendChild(ol.ext.element.create('TR', {
    style: {
      height: Math.max(0, (page*nb - nb2)*h) + 'px'
    }
  }))
  // Page list
  var nmax = Math.min(this._listFeatures.length, (page+1)*nb + nb2)
  this._page = [];
  for (var i = Math.max(0, page*nb - nb2); i < nmax; i++) {
    // this._listbody.appendChild(this._listFeatures[i].tr)
    var tr = this._getLi(this._listFeatures[i])
    this._page.push({
      feature: this._listFeatures[i],
      tr: tr
    })
    if (this._curSelection && this._listFeatures[i] === this._curSelection.feature) {
      this._curSelection.tr = tr;
      tr.classList.add('ol-selected')
    }
    this._listbody.appendChild(tr)
  }
  // Last row to preserve space
  this._listbody.appendChild(ol.ext.element.create('TR', {
    style: {
      height: Math.max(0, (this._listFeatures.length - nmax) * h) + 'px'
    }
  }))
  // force focus on list
  if (this.get('focus') !== false) this._listbody.focus();
}
/** A sort function to compare 2 properties
 * @param {ol.Feature} f1
 * @param {ol.Feature} f2
 * @param {string} prop property name to sort at
 * @return number -1: v1 < v2, 1: v1 > v2, 0: v1 = v2
 * @api
 */
ol.control.FeatureList.prototype.sortFn = function(f1, f2, p) {
  var v1 = f1.get(p) || '';
  var v2 = f2.get(p) || '';
  if (v1 < v2) return -1;
  if (v1 > v2) return 1;
  return 0;
}
/** Format feature property
 * @param {ol.Feature} feature,
 * @param {string} prop property name
 * @api
 */
ol.control.FeatureList.prototype.formatProperty = function(feature, prop) {
  var p = feature.get(prop);
  return {
    val: p === undefined ? '-' : p,
    type: typeof(p)
  }
}
/** Get the list of columns
 * @param {Array<ol.Feature>} [features] a list of features to retrieve columns (if none, returns columns defined by setColumns)
 */
ol.control.FeatureList.prototype.getColumns = function(features) {
  var columns = this.columns || [];
  if (!columns.length && features) {
    var col = {}
    features.forEach(function (f) {
      // Get properties but geom
      Object.keys(f.getProperties()).forEach(function(p) {
        if (!(f.get(p) instanceof ol.geom.Geometry)) {
          col[p] = true;
        }
      })
    })
    columns = Object.keys(col);
  }
  return columns;
}
/** Set the list of columns to display
 * @param {Array<string>} columns
 */
ol.control.FeatureList.prototype.setColumns = function(columns) {
  this.columns = columns || [];
  this._drawList();
  if (this._curSelection && this._curSelection.feature) {
    this.select(this._curSelection.feature, true)
  }
}
/** Dragging sizer
 * @private
 */
ol.control.FeatureList.prototype._dragSizer = function(e) {
  var self = this;
  if (!this._movefn) {
    this._movefn = this._dragSizer.bind(this);
  }
  e.stopPropagation();
  e.preventDefault();
  switch (e.type) {
    case 'touchcancel':
    case 'touchend':
    case 'mouseup': {
      self._pageY = false;
      ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"]
        .forEach(function (eventName) {
          document.removeEventListener(eventName, self._movefn);
        });
      break;
    }
    case 'mousedown':
    case 'touchstart': {
      self._contentHeight = self._list.parentNode.getBoundingClientRect().height;
      self._pageY =  e.pageY
        || (e.touches && e.touches.length && e.touches[0].pageY)
        || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY);
      ["mouseup", "mousemove", "touchend", "touchcancel", "touchmove"]
        .forEach(function (eventName) {
          document.addEventListener(eventName, self._movefn);
        });
    }
    // fallthrough
    case 'mousemove':
    case 'touchmove': {
      if (self.pageY !== false) {
        var pageY = e.pageY
          || (e.touches && e.touches.length && e.touches[0].pageY)
          || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY);
        if (this._bottomScroll) {
          self.resize(self._contentHeight + pageY - self._pageY);
        } else {
          self.resize(self._contentHeight - pageY + self._pageY);
        }
      }
      break;
    }
    default: break;
  }
}
/** Resize the control to the map
 * @param {number} [height] the table height (if in a map sticks to the viewport height)
 */
ol.control.FeatureList.prototype.resize = function(height) {
  if (!this.getMap()) return;
  var h0 = this._list.parentNode.getBoundingClientRect().height
  // Set new height
  if (height !== undefined) {
    this._list.parentNode.style.height = height + 'px';
  }
  // Prevent getting out of the map
  if (this.element.dataset.control) {
    var h = this.getMap().getTargetElement().getBoundingClientRect().height 
      - this._head.getBoundingClientRect().height;
    this._list.parentNode.style.maxHeight = Math.min(h, this._list.getBoundingClientRect().height)  + 'px';
  }
  // Has changed?
  if (h0 !== this._list.parentNode.getBoundingClientRect().height) {
    this.dispatchEvent({
      type: 'resize',
      height: this._list.parentNode.getBoundingClientRect().height
    })
  }
}
/** Get the current selection in the list
 * @returns {ol.Feature} 
 */
ol.control.FeatureList.prototype.getSelection = function() {
  if (this._curSelection) {
    return this._curSelection.feature || null;
  }
  return null;
}
/** Select a feature in the list
 * @param {ol.Feature} [feature] if none remove selection
 * @param {boolean} [noScroll=false] prevent scrolling to the selected row
 */
ol.control.FeatureList.prototype.select = function(feature, noScroll) {
  // Remove previous
  if (this._curSelection) {
    this._curSelection.tr.classList.remove('ol-selected');
  }
  this._curSelection = null;
  // New selection
  if (feature) {
    if (noScroll) {
      var f = this._findInPage(feature)
      this._curSelection = {
        feature: feature,
        tr: f ? f.tr : ol.ext.element.create('TR')
      };
    } else {
      this._curSelection = this.scrollTo(feature)
    }
  }
  if (this._curSelection) {
    this._curSelection.tr.classList.add('ol-selected');
  }
}
/** Add a button to the list header menu
 * @param {Object} options
 *  @param {string} className
 *  @param {string} [title]
 *  @param {string} [html]
 *  @param {function} click on click function
 * @returns {Element}
 */
ol.control.FeatureList.prototype.addButton = function(options) {
  return ol.ext.element.create('BUTTON', {
    className: options.className,
    title: options.title || '',
    html: options.html || '',
    click: options.click,
    parent: this._menu.querySelector('.ol-buttons')
  })
}

/*	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
    });
    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) {
          this.element.classList.add('ol-collapsed')
        }
      }.bind(this));
      // Show bookmarks on click
      this.button = ol.ext.element.create('BUTTON', {
        type: 'button',
        title: options.title || 'Geobookmarks',
        click: function () {
          var show = !this.element.classList.contains('ol-collapsed');
          this.element.classList.toggle('ol-collapsed')
          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 = input.value;
        if (title) {
          this.addBookmark(title);
          input.value = '';
          this.dispatchEvent({
            type: "add",
            name: title
          });
        }
        this.element.classList.add('ol-collapsed')
      }
    }.bind(this));
    input.addEventListener('blur', function () {
      this.element.classList.add('ol-collapsed')
    }.bind(this));
    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 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 (e) {
        var bm = JSON.parse(e.target.getAttribute("data-bookmark"));
        this.getMap().getView().setCenter(bm.pos);
        this.getMap().getView().setZoom(bm.zoom);
        this.getMap().getView().setRotation(bm.rot || 0);
        this.element.classList.add('ol-collapsed')
        this.dispatchEvent({ type: 'select', name: e.target.getAttribute("data-name"), bookmark: bm });
      }.bind(this));
      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://data.geopf.fr/navigation/isochrone')
    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) {
    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 => Capabilities: https://data.geopf.fr/navigation/getcapabilities
      var data = {
        // resource: 'jmk_valhalla_cost_type_test', 
        resource: 'bdtopo-valhalla',
        point: ol.proj.toLonLat(coord, proj),
        profile: this.get('mode') === 'pedestrian' ? 'pedestrian' : 'car',
        costType: method,
        costValue: dt,
        geometryFormat: 'geojson',
        direction: (this.get('direction') === 'reverse') ? 'arrival' : 'departure',
        // crs: 'EPSG:4326'
      };
      this._ajax.send(this.get('url'), data, {
        coord: coord,
        option: option,
        data: data,
        iteration: (iter || 0) + 1
      });
    }
  }
  /** Trigger result
   * @private
   */
  _success(e) {
    console.log(e)
    var proj = this.getMap() ? this.getMap().getView().getProjection() : 'EPSG:3857';
    // Convert to features
    var evt = e.response;
    var format;
    if (evt.wktGeometry) {
      format = new ol.format.WKT();
      evt.feature = format.readFeature(evt.wktGeometry, {
        dataProjection: 'EPSG:4326',
        featureProjection: proj
      });
      delete evt.wktGeometry;
    } else {
      format = new ol.format.GeoJSON();
      evt.feature = format.readFeature(evt.geometry, {
        dataProjection: 'EPSG:4326',
        featureProjection: proj
      });
      delete evt.geometry;
    }
    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]);
    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=true] 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=false] use geohash instead of lonlat, default false
 *  @param {integer} [options.fixed=6] number of digit in coords, default 6
 *  @param {boolean} [options.anchor] use "#" instead of "?" in href
 *  @param {boolean} [options.visible=true] 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
 *  @param {number} [options.refreshDelay=500] 
 */
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)
    }
    this.set('refreshDelay', options.refreshDelay || 500)
    // 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] : '')
        }
        this.replaceUrl_(document.location.origin + document.location.pathname + s, true)
      } else {
        this.replaceUrl_(this.getLink(), true)
      }
    } catch (e) { /* ok */ }
    /*
    if (this._localStorage) {
      localStorage['ol@permalink'] = this.getLink(true);
    }
    */
  }
  /** Refresh the url
   * @private
   */
  replaceUrl_(url, force) {
    clearTimeout(this.refreshTout_)
    if (force) {
      window.history.replaceState(null, null, url)
    } else {
      this.refreshTout_ = setTimeout(function() {
        window.history.replaceState(null, null, url)
      }, this.get('refreshDelay'))
    }
  }
  /**
   * On view change refresh link
   * @param {ol.event} The map instance.
   * @private
   */
  viewChange_() {
    try {
      if (this.replaceState_) {
        this.replaceUrl_(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 to clipboard select option
 *	@param {boolean} [options.save=true] add a save 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'
    if (!options.target) {
      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 = ol.ext.element.create('DIV')
    var printCtrl = this._printCtrl = new ol.control.Print(options)
    printCtrl.on(['print', 'error', 'printing'], function (e) {
      this._printing(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
    })
    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 () {
          var saveas = save.value;
          save.value = ''
          // Copy to clipboard
          if (this.formats[saveas].clipboard) {
            if (this._copyMap(saveas)) return;
          } 
          // 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[saveas])
          // console.log('OPTIONS',opt)
          printCtrl.print(opt)
        }.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))
    // No options
    if (save.querySelectorAll('option').length === 1) {
      save.style.display = 'none';
    }
    // 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] || what;
    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._printDialog.element.dataset.orientation = this._orientation
    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().dataset.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' })
  }
  /** Dispatch print events
   * @private
   */
  _printing(e) {
    this._printDialog.getContentElement().dataset.status = e.type
    if (!e.clipboard) {
      this.dispatchEvent(e)
    }
  }
  /** Copy map to clipboard
   * @param {string} format
   * @return {boolean} if copy
   * @private
   */
  _copyMap(format) {
    var copied = this._printDialog.element.querySelector('.ol-clipboard-copy')
    this._printCtrl.copyMap(this.formats[format], function (isok) {
      if (isok) {
        copied.classList.add('visible')
        setTimeout(function () { copied.classList.remove('visible') }, 1000)
      }
    })
    return true
  }
  /** 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}
   */
  getPrintControl() {
    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'
};

/** Print control to get an image of the map
 * @constructor
 * @fire show
 * @fire print
 * @fire error
 * @fire printing
 * @extends {ol.control.PrintDialog}
 */
ol.control.PrintDialog2x = class olcontrolPrintDialog2x extends ol.control.PrintDialog {
  constructor(options) {
    options = options || {}
    super(options);
    this._printDialog.element.classList.add('ol-ext-print-dialog2x')
    // Add printmap
    var printMap = ol.ext.element.create('DIV', {
      className: 'ol-map2',
      parent: this._pages[0]
    })
    // Print control
    var printCtrl2 = this._printCtrl2 = new ol.control.Print(options)
    printCtrl2.on(['print', 'error', 'printing'], function (e) {
      if (e.type === 'print') {
        var canvas = document.createElement('canvas');
        // Get clipping (ol/control/SwipeMap or ol/control/ClipMap)
        var clipDiv = printMap.querySelector('.ol-layers');
        var clip = clipDiv.style.clipPath || clipDiv.style.clip
        // Print in canvas
        if (clip) {
          var param = clip.replace(/^(.*)\((.*)\)/, '$2');
          clip = {
            type: clip.replace(/^(.*)\(.*/, '$1'),
          }
          switch(clip.type) {
            case 'circle': {
              param = param.split(' ')
              clip.radius = parseFloat(param[0]);
              clip.x = parseFloat(param[2]);
              clip.y = parseFloat(param[3]);
              break;
            }
            case 'rect': {
              param = param.split(',')
              clip.top = parseFloat(param[0]);
              clip.right = parseFloat(param[1]);
              clip.bottom = parseFloat(param[2]);
              clip.left = parseFloat(param[3]);
              break;
            }
            default: {
              console.warn('no clip (' + clip.type + ')')
              break;
            }
          }
          canvas.width = this._canvas1.width;
          canvas.height = this._canvas1.height;
        } else if (this.getOrientation() === 'landscape') {
          canvas.width = this._canvas1.width + e.canvas.width;
          canvas.height = this._canvas1.height;
        } else {
          canvas.width = this._canvas1.width;
          canvas.height = this._canvas1.height + e.canvas.height;
        }
        var ctx = canvas.getContext('2d');
        ctx.drawImage(this._canvas1, 0, 0);
        if (clip) {
          ctx.save()
          switch (clip.type) {
            case 'rect': {
              ctx.beginPath();
              ctx.rect(clip.left, clip.top, clip.right - clip.left, clip.bottom - clip.top)
              break;
            }
            case 'circle': {
              ctx.beginPath();
              ctx.arc(clip.x, clip.y, clip.radius, 0, Math.PI * 2);
              break;
            }
          }
          ctx.clip()
          ctx.drawImage(e.canvas, 0, 0);
          ctx.restore()
        } else {
          ctx.drawImage(e.canvas, 
            (this.getOrientation() === 'landscape' ? this._canvas1.width : 0), 
            (this.getOrientation() !== 'landscape' ? this._canvas1.height : 0)
          );
        }
        e.canvas = canvas;
        e.image = canvas.toDataURL(e.imageType, e.quality);
        var w = canvas.width / 96 * 25.4
        var h = canvas.height / 96 * 25.4
        var size = e.print.size
        if (this.getOrientation() === 'landscape') size = [size[1], size[0]]
        e.print.position = [
          (size[0] - w) / 2,
          (size[1] - h) / 2
        ]
        e.print.imageWidth = w;
        e.print.imageHeight = h;
      }
      if (this._clipboard) {
        try {
          e.canvas.toBlob(function (blob) {
            try {
              navigator.clipboard.write([
                new window.ClipboardItem(
                  Object.defineProperty({}, blob.type, {
                    value: blob,
                    enumerable: true
                  })
                )
              ]);
              //this.dispatchEvent(e)
              // Show copy
              var copied = this._printDialog.element.querySelector('.ol-clipboard-copy')
              copied.classList.add('visible')
              setTimeout(function () { copied.classList.remove('visible') }, 1000)
              // OK
            } catch (err) {
              // this.dispatchEvent(e)
            }
          }.bind(this));
        } catch (err) {
          // this.dispatchEvent(e)
        }
      } else {
        this.dispatchEvent(e)
      }
    }.bind(this))
    // Show map 2
    var originalTarget
    this._printDialog.on('show', function() {
      if (this.getMap2()) {
        originalTarget = this.getMap2().getTargetElement()
        this.getMap2().setTarget(printMap)
        if (originalTarget.dataset.swipeOrientation) {
          this._printDialog.element.dataset.swipeOrientation = originalTarget.dataset.swipeOrientation
        }
        if (originalTarget.dataset.clipMap) {
          this._printDialog.element.dataset.clipMap = originalTarget.dataset.clipMap
        }
      }
    }.bind(this))
    this._printDialog.on('hide', function () {
      delete this._printDialog.element.dataset.swipeOrientation
      delete this._printDialog.element.dataset.clipMap
      if (!originalTarget) return
      if (this.getMap2()) {
        this.getMap2().setTarget(originalTarget)
      }
      originalTarget = null
    }.bind(this))
    // Add second map
    if (options.map2) this.setMap2(options.map2)
  }
  /** Set the second map to print
   * @param {ol.Map} map
   * @API
   */
  setMap2(map) {
    if (this.getMap2()) {
      this.getMap2().removeControl(this._printCtrl2)
    }
    this._map2 = map;
    if (map) {
      this.getMap2().addControl(this._printCtrl2)
    }
  }
  /** Set the second map to print
   * @returns {ol.Map}
   * @API
   */
  getMap2() {
    return this._map2;
  }
  /** First map printing
   * @private
   */
  _printing(e) {
    this._printDialog.getContentElement().dataset.status = e.type
    if (e.type === 'print') {
      this._canvas1 = e.canvas
      this._clipboard = e.clipboard
      this._printCtrl2.print({
        format: e.print.format,
        imageType: e.imageType,
        margin: e.margin,
        pdf: e.pdf,
        orient: e.print.orientation,
        quality: e.quality,
        size: e.print.size,
        title: e.title
      })
    } else if (!e.clipboard) {
      this.dispatchEvent(e)
    }
  }
  /** Prevent first copy
   * @private
   */
  _copyMap(/* format */) { 
    /* prevent first copy */ 
    return 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).
*/
/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
/**
 * @classdesc OpenLayers 3 Profile 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 profile, 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 profile
 *  @param {boolean} [options.selectable=false] enable selection on the profile, default false
 *  @param {boolean} [options.zoomable=false] can zoom in the profile
 */
ol.control.Profile = class olcontrolProfile 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.Profile.prototype.info
    if (options.target) {
      element.classList.add(options.className || 'ol-profile')
    } else {
      element.className = ((options.className || 'ol-profile') + ' 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-profilebar")
    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-profilecursor")
    div.appendChild(this.cursor_)
    this.popup_ = document.createElement("div")
    this.popup_.classList.add("ol-profilepopup")
    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 profile
   * @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 profile
   * @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 profile
   * @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 profile
   * @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 profile.
   * @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 profile
   */
  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/Profile] 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 profile 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.Profile.prototype.info = {
  "zmin": "Zmin",
  "zmax": "Zmax",
  "ytitle": "Altitude (m)",
  "xtitle": "Distance (km)",
  "time": "Time",
  "altitude": "Altitude",
  "distance": "Distance",
  "altitudeUnits": "m",
  "distanceUnitsM": "m",
  "distanceUnitsKM": "km",
};
// For backward compatibility
// eslint-disable-next-line no-unused-vars
ol.control.Profil = ol.control.Profile;

/*	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} [options.leng=en] control language
 *	@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();
    this.set('lang', options.lang || 'en')
    // 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://data.geopf.fr/navigation/itineraire')
    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 || 'use shift to add / ctrl to 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('mode') === 'pedestrian' ? '' : 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 infoType = ol.control.RoutingGeoportail.prototype.instructions[this.get('lang') || 'en']
    var infoClassName = {
      'straight': '',
      'left': 'L',
      'right': 'R',
      'slight left': 'FL',
      'slight right': 'FR',
    }
    routing.features.forEach(function (f, i) {
      var d = this.getDistanceString(f.get('distance'));
      t = this.getTimeString(f.get('durationT'));
      // Decode instructions
      var instruction = infoType[f.get('instruction_type')] || infoType['none'];
      instruction += ' ' + (infoType[f.get('instruction_modifier')] || infoType.straight) + ' ';
      // console.log(f.get('instruction_type'), '-',f.get('instruction_modifier'))
      // Show info
      ol.ext.element.create('LI', {
        className: infoClassName[f.get('instruction_modifier')] || '',
        html: (instruction || '#')
          + ' ' + 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++) {
        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();
  }
}
/** Instructions labels  */
ol.control.RoutingGeoportail.prototype.instructions = {
  'en': {
    // Instruction type
    'none': 'Go ',
    'continue': 'Continue ',
    'new name': 'Continue ',
    'depart': 'Start',
    'arrive': 'Arrival',
    'turn': 'Turn',
    'fork': 'Fork',
    // Instruction modifier
    'straight': 'on',
    'left': 'left on',
    'right': 'right on',
    'slight left': 'slight left on',
    'slight right': 'slight right on',
  },
  'fr': {
    // Instruction type
    'none': 'Continuer ',
    'continue': 'Continuer ',
    'new name': 'Continuer ',
    'depart': 'Départ',
    'arrive': 'Arrivée',
    'turn': 'Tourner',
    'fork': 'Prendre',
    // Instruction modifier
    'straight': 'sur',
    'left': 'à gauche sur',
    'right': 'à droite sur',
    'slight left': 'légèrement à gauche sur',
    'slight right': 'légèrement à droite sur',
  }
};

/*	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) 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 {ol/proj/ProjectionLike} [options.projection="EPSG:3857"] control projection
 *  @param {string} [options.className] control class name
 *  @param {Element | string } [options.target] Specify a target if you want the control to be rendered outside of the map's viewport.
 *  @param {string} [options.label="search"] Text label to use for the search button, default "search"
 *  @param {string} [options.labelGPS="Locate with GPS"] placeholder
 *  @param {string} [options.labelCenter="Map center"] placeholder
 *  @param {number} [options.typing=300] a delay on each typing to start searching (ms), default 300.
 *  @param {integer} [options.minLength=1] minimum length to start searching, default 1
 *  @param {integer} [options.maxItems=10] maximum number of items to display in the autocomplete list, default 10
 *  @param {integer} [options.digit=3] number of digit in coords
 */
ol.control.SearchCoordinates = class olcontrolSearchCoordinates extends ol.control.Search {
  constructor(options) {
    options = options || {};
    options.className = (options.className || '') + ' ol-searchcoord';
    options.placeholder = options.placeholder || 'x,y';
    super(options);
    // Projection
    this.projection_ = options.projection || 'EPSG:3857'
    this.set('digit', typeof(options.digit) === 'number' ? options.digit : 3)
    // 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: options.labelGPS || 'Locate with GPS',
      parent: this.element,
      click: function () {
        this.geolocation.setTracking(true);
      }.bind(this)
    });
    ol.ext.element.create('BUTTON', {
      className: 'ol-centerloc',
      title: options.labelCenter || 'Map center',
      parent: this.element,
      click: function () {
        this.setInput()
      }.bind(this)
    });
    this._createForm();
    // Move list to the end
    var ul = this.element.querySelector("ul.autocomplete");
    this.element.appendChild(ul);
  }
  /** Set the input value in the form (for initialisation purpose)
   *	@param {Array<number>} [coord] if none get the map center
   *	@api
   */
  setInput(coord) {
    if (!coord) {
      if (!this.getMap()) return
      coord = this.getMap().getView().getCenter();
      coord = ol.proj.transform(coord, this.getMap().getView().getProjection(), this.getProjection())
    }
    var d = Math.pow(10, this.get('digit'))
    this.inputs_[0].value = Math.round(coord[0] * d) / d
    this.inputs_[1].value = Math.round(coord[1] * d) / d;
    this._triggerCustomEvent('keyup', this.inputs_[0]);
  }
  /** Get the control projection
   * @returns {ol/proj/ProjectionLike}
   */
  getProjection() {
    return this.projection_
  }
  /** Set the projection
   * @param {ol/proj/ProjectionLike} proj
   */
  setProjection(proj) {
    if (this.projection_ !== proj) {
      this.projection_ = proj;
      this.clearHistory();
      this.element.querySelectorAll('INPUT[type="number"]').forEach(function(i) {
        i.value = '';
      })
    }
  }
  /** Create input form
   * @private
   */
  _createForm() {
    // Value has change
    var onchange = function() {
      if (lonx.value || laty.value) {
        this._input.value = lonx.value + ',' + laty.value;
      } else {
        this._input.value = '';
      }
      // Center on coords
      this.search();
    }.bind(this);
    function createInput(className) {
      var input = ol.ext.element.create('INPUT', {
        className: className,
        type: 'number',
        step: 'any',
        lang: 'en',
        parent: div,
        on: {
          'change keyup': onchange
        }
      });
      return input;
    }
    // X
    var div = ol.ext.element.create('DIV', {
      className: 'ol-longitude',
      parent: this.element
    });
    ol.ext.element.create('LABEL', {
      html: 'X',
      parent: div
    });
    var lonx = createInput('ol-decimal');
    // Y
    div = ol.ext.element.create('DIV', {
      className: 'ol-latitude',
      parent: this.element
    });
    ol.ext.element.create('LABEL', {
      html: 'Y',
      parent: div
    });
    var laty = createInput('ol-decimal');
    // Focus on open
    if (this.button) {
      this.button.addEventListener("click", function () {
        lonx.focus();
      });
    }
    this.inputs_ = [ lonx, laty ];
    // Change value on click
    this.on('select', function (e) {
      lonx.value = e.search.gps[0];
      laty.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();
      coord = ol.proj.transform(coord, 'EPSG:4326', this.projection_)
      var d = Math.pow(10, this.get('digit'))
      lonx.value = Math.round(coord[0] * d) / d;
      laty.value = Math.round(coord[1] * d) / d;
      this._triggerCustomEvent('keyup', lonx);
    }.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]);
    // 
    var coord = ol.proj.transform([c[0], c[1]], this.projection_, 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 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=search] Text label to use for the search button, default "search"
 *  @param {string | undefined} [options.labelGPS="Locate with GPS"] placeholder, default "Locate with GPS"
 *  @param {number | undefined} [options.typing=300] a delay on each typing to start searching (ms), default 300.
 *  @param {integer | undefined} [options.minLength=1] minimum length to start searching, default 1
 *  @param {integer | undefined} [options.maxItems=10] 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: options.labelGPS || '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);
  }
  /** Set the input value in the form (for initialisation purpose)
   *	@param {Array<number>} [coord] if none get the map center
   *	@api
   */
  setInput(coord) {
    if (!coord) {
      if (!this.getMap()) return
      coord = this.getMap().getView().getCenter();
      coord = ol.proj.transform(coord, this.getMap().getView().getProjection(), 'EPSG:4326')
    }
    this.inputs_[0].value = coord[0];
    this.inputs_[1].value = coord[1];
    this._triggerCustomEvent('keyup', this.inputs_[0]);
  }
  /** 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', '"');
    this.inputs_ = [lon, lat]
    // 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 commune
 * @fires parcelle
 * @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 for city input, default "Choisissez une commune..."
 *	@param {string | undefined} options.prefixlabel label for prefix input, default "Préfixe"
 *	@param {string | undefined} options.sectionLabel label for section input, default "Section"
 *	@param {string | undefined} options.numberLabel label for number input, default "Numéro"
 *	@param {string | undefined} options.arrondLabel label for arrondissement, default "Arrond."
 *	@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 = ol.ext.element.create('DIV', {
      parent: element
    });
    options.arrondLabel = options.arrondLabel || 'Arrond.'
    options.prefixLabel = options.prefixLabel || 'Préfixe'
    options.sectionLabel = options.sectionLabel || 'Section'
    options.numberLabel = options.numberLabel || 'Numéro'
    // Labels
    ol.ext.element.create('LABEL', {
      text: options.arrondLabel,
      className: 'district',
      parent: div
    });
    ol.ext.element.create('LABEL', {
      text: options.prefixLabel,
      parent: div
    });
    ol.ext.element.create('LABEL', {
      text: options.sectionLabel,
      parent: div
    });
    ol.ext.element.create('LABEL', {
      text: options.numberLabel,
      parent: div
    });
    ol.ext.element.create('BR', {
      parent: div
    });
    // Input
    this._inputParcelle = {
      arrond: ol.ext.element.create('INPUT', { className: 'district', disabled: true }),
      prefix: document.createElement('INPUT'),
      section: document.createElement('INPUT'),
      numero: document.createElement('INPUT')
    };
    this._inputParcelle.arrond.setAttribute('maxlength', 2);
    this._inputParcelle.arrond.setAttribute('placeholder', options.arrondLabel);
    this._inputParcelle.prefix.setAttribute('maxlength', 3);
    this._inputParcelle.prefix.setAttribute('placeholder', options.prefixLabel);
    this._inputParcelle.section.setAttribute('maxlength', 2);
    this._inputParcelle.section.setAttribute('placeholder', options.sectionLabel);
    this._inputParcelle.numero.setAttribute('maxlength', 4);
    this._inputParcelle.numero.setAttribute('placeholder', options.numberLabel);
    // 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', function(e) {
      this.selectCommune(e);
      e.type = 'commune';
      this.dispatchEvent(e);
    }.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._arrond = e.search.districtcode;
    if (this._arrond !== '000') {
      this._inputParcelle.arrond.disabled = false;
      // Paris
      if (this._arrond.charAt(0) === '1') this._arrond = '100';
      // Marseille
      if (this._arrond.charAt(0) === '2') this._arrond = '200';
    } else {
      this._inputParcelle.arrond.disabled = true;
      this._inputParcelle.arrond.value = '';
    }
    this._input.value = e.search.insee + ' - ' + e.search.fulltext;
    this.activateParcelle(true);
    this._inputParcelle.numero.focus();
    this.autocompleteParcelle();
  }
  /** Get the input field
   * @param {string} what the search input id commune|arrond|prefix|section|numero, default commune 
   * @return {Element}
   * @api
   */
  getInputField(what) {
    return this._inputParcelle[what] || this._input;
  }
  /** 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');
    }
  }
  /** Clear the parcel list
   */
  clearParcelList() {
    this._listParcelle([])
  }
  /** 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 = '___';
    }
    // Arrondissement
    var district = this._inputParcelle.arrond.value;
    if (district) {
      var n = this._arrond.length - district.length;
      district = this._arrond.substr(0,n) + district;
    }
    // Get parcelle number
    var section = complete(this._inputParcelle.section.value, 2);
    var numero = complete(this._inputParcelle.numero.value, 4, '0');
    this.searchParcelle(commune, district, 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, district, 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') : '')
        + (district ? '&districtcode=' + district : '')
        + (section ? '&section=' + section.toUpperCase() : '')
        + (numero ? '&number=' + numero : '')
        + '&limit=20',
        {},
        function(resp) {
          var jsonResp = [];
          if (resp.features) {
            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, 
                Arrondissement: prop.districtcode, 
                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.Arrondissement + '-' + 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
   * @param {boolean} [sort] true to sort the 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 (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
        if (this._lefttime != e.frameState.time) {
          ctx.clearColor(0, 0, 0, 0);
          ctx.clear(ctx.COLOR_BUFFER_BIT);
          this._lefttime = e.frameState.time;
        }
        // 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);
        ctx.clearColor(0, 0, 0, 0);
        ctx.clear(ctx.COLOR_BUFFER_BIT);
      }
    } 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
        if (this._righttime != e.frameState.time) {
          ctx.clearColor(0, 0, 0, 0);
          ctx.clear(ctx.COLOR_BUFFER_BIT);
          this._righttime = e.frameState.time;
        }
        // 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);
        ctx.clearColor(0, 0, 0, 0);
        ctx.clear(ctx.COLOR_BUFFER_BIT);
      }
    } 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'));
        if (this.getMap()) {
          this.getMap().getTargetElement().dataset.swipeOrientation = 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 = '';
      delete this.getMap().getTargetElement().dataset.swipeOrientation
    } 
    super.setMap(map);
    if (map) {
      this._listener = map.on('change:size', this._clip.bind(this));
      this._clip();
    }
  }
  /** Clip
   * @private
   */
  _clip() {
    if (this.getMap()) {
      var layerDiv = this.getMap().getViewport().querySelector('.ol-layers');
      var rect = this.getRectangle();
      if (rect) {
        layerDiv.style.clip = 'rect('
        + rect[1] + 'px,' // top
        + rect[2] + 'px,' // right
        + rect[3] + 'px,' // bottom
        + rect[0] + 'px' //left
        + ')';
      }
      // Orientation
      this.getMap().getTargetElement().dataset.swipeOrientation = this.get('orientation')
    }
  }
  /** Get visible rectangle
   * @returns {ol.extent}
   */
  getRectangle() {
    var s = this.getMap().getSize();
    if (!s) return;
    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: ol.control.VideoRecorder.prototype.tips.start,
      click: function () {
        this.start()
      }.bind(this),
      parent: element
    })
    ol.ext.element.create('BUTTON', {
      type: 'button',
      className: 'ol-stop',
      title: ol.control.VideoRecorder.prototype.tips.stop,
      click: function () {
        this.stop()
      }.bind(this),
      parent: element
    })
    ol.ext.element.create('BUTTON', {
      type: 'button',
      className: 'ol-pause',
      title: ol.control.VideoRecorder.prototype.tips.pause,
      click: function () {
        this.pause()
      }.bind(this),
      parent: element
    })
    ol.ext.element.create('BUTTON', {
      type: 'button',
      className: 'ol-resume',
      title: ol.control.VideoRecorder.prototype.tips.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')
    })
  }
  /** Set button title
   * @param {string} button button name (start, stop, pause or resume)
   * @param {string} title
   */
  setTooltip(button, title) {
    var elt = this.element.querySelector('button.ol-'+button)
    if (elt) {
      elt.title = title;
    }
  }
  /**
   * 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')
    }
  }
}
/** Default button tips */
ol.control.VideoRecorder.prototype.tips = {
  start: 'start video',
  stop: 'stop',
  pause: 'pause',
  resume: 'resume'
}

/* 
  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 {
          caps.url = evt.options.url
          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
    var uri = 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
      }, {
        url: uri,
        timeout: options.timeout || 10000,
        callback: options.onload,
        abort: false
      })
    } else {
      this._ajax.send(url, request, {
        url: uri,
        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 && caps.Attribution.Title) {
      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 || '').replace(/service=wms&?/i,''),
      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().set('className', this.getDialog().get('className') + ' 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 this.supportedSets.indexOf(tm.TileMatrixSet) >= 0;
  }
  /** 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.url,
      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;
  }
}
/** An array of supported sets (basically EPSG:3857)
 * @api
 */
ol.control.WMSCapabilities.prototype.supportedSets = [
  'PM',
  '3857',
  'EPSG:3857',
  'EPSG:900913',
  'webmercator',
  'GoogleMapsCompatible'
]

/*
  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
* @fires drawing
* @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) {
    // Drawing event
    var drawingEvt = {
      type: 'drawing',
      time: e.time,
      feature: e.feature,
      start: e.start,
      stop: e.stop,
      rotation: e.rotation,
      style: e.style,
      extra: e.extra
    }
    this.dispatchEvent(drawingEvt)
    var style = (drawingEvt.style instanceof Array) ? drawingEvt.style : [drawingEvt.style];
    // Draw
    if (this.fade_) {
      e.context.globalAlpha = this.fade_(1 - e.elapsed)
    }
    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 (error) { /* 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();
    }
    // Before anim
    /*
    var beforeEvent = { 
      type: 'beforeanim', 
      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 || {}, 
      style: flashStyle
    };
    fanim[step].dispatchEvent(beforeEvent);
    self.dispatchEvent(beforeEvent);
    */
    // 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 || {}, 
        style: flashStyle
      };
      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];
          // Prevent same coords
          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 && v.length > 1) {
          p = 'A,A' + (hasZ ? ',A':'') + (hasM ? ',A':'');
          xy.push(p);
        }
        // encoded
        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
    this._olinteractionCurrentMap_cleanup = [];
    if (options.onKeyDown) {
      var listener = function (e) {
        if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) {
          options.onKeyDown({ type: e.type, map: this.getMap(), originalEvent: e });
        }
      }.bind(this);
      document.addEventListener('keydown', listener);
      this._olinteractionCurrentMap_cleanup.push(() => document.removeEventListener('keydown', listener));
    }
    if (options.onKeyPress) {
      var listener = function (e) {
        if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) {
          options.onKeyPress({ type: e.type, map: this.getMap(), originalEvent: e });
        }
      }.bind(this);
      document.addEventListener('keypress', listener);
      this._olinteractionCurrentMap_cleanup.push(() => document.removeEventListener('keypress', listener));
    }
    if (options.onKeyUp) {
      var listener = function (e) {
        if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) {
          options.onKeyUp({ type: e.type, map: this.getMap(), originalEvent: e });
        }
      }.bind(this);
      document.addEventListener('keyup', listener);
      this._olinteractionCurrentMap_cleanup.push(() => document.removeEventListener('keyup', listener));
    }
  }
  /** 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;
  }
  /** @private */
  disposeInternal() {
    super.disposeInternal();
    this._olinteractionCurrentMap_cleanup.forEach(f => f())
  }
}
/** 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 = '';
      delete this.getMap().getTargetElement().dataset.clipMap
    }
    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 && (e.type==='pointerdown' || !this.get('freeze'))) {
      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)';
      this.getMap().getTargetElement().dataset.clipMap = '1'
    }
  }
}

/** 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);
  }
  /**
   * Remove the interaction from its current map and attach it to the new map.
   * @param {ol.Map} map The map instance.
   */
  setMap(map) {
    super.setMap(map);
    this.features = [];
  }
  /** 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]
 *  @param {boolean} options.centerOnClick wheter a click inside the popup should move it to the click coordinates, default false
 */
ol.interaction.DragOverlay = class olinteractionDragOverlay extends ol.interaction.Pointer {
  constructor(options) {
    options = options || {};
    var offset = options.offset || [0, 0];
    var centerOnClick = options.centerOnClick || false;
    // 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 (centerOnClick) {
            this._dragging.setPosition(coordinate, true);
          }
          var coordinateInitial = this._dragging.getPosition();
          this._dragging.offsetClick = [coordinate[0]-coordinateInitial[0], coordinate[1]-coordinateInitial[1]];
          this.dispatchEvent({
            type: 'dragstart',
            overlay: this._dragging,
            originalEvent: evt.originalEvent,
            frameState: evt.frameState,
            coordinate: coordinateInitial
          });
          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) {
          coordinate = [coordinate[0]-this._dragging.offsetClick[0], coordinate[1]-this._dragging.offsetClick[1]];
          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) {
          coordinate = [coordinate[0]-this._dragging.offsetClick[0], coordinate[1]-this._dragging.offsetClick[1]];
          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;
    }
    var element = ov.getElement();
    // Stop event overlay
    if (element.parentElement && 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
    });
    // element.addEventListener('pointerdown', handler);
    element.parentNode.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.getElement().parentNode.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
 * 	@param {function | undefined}	options.geometryFunction Draw interaction geometry function to customize the hole
 */
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, p) {
        g = _geometryFn.bind(this)(c, g)
        return geomFn.bind(this)(c, g, p)
      }
    } 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;
        ol.ext.element.setCursor(this.getMap(), this._cursor);
        //      console.log('setCursor',this._cursor)
      } else {
        ol.ext.element.setCursor(this.getMap(), 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()) {
      ol.ext.element.setCursor(this.getMap(), 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()) {
      if (this.previousCursor_ !== undefined) {
        ol.ext.element.setCursor(this.getMap(), 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()) {
      ol.ext.element.setCursor(this.getMap(), 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;
            ol.ext.element.setCursor(map, this.cursor_);
          }
        } else if (this.previousCursor_ !== undefined) {
          ol.ext.element.setCursor(map, 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
          ol.ext.element.setCursor(element, this.cursor_)
        }
      } else if (this.previousCursor_ !== undefined) {
        ol.ext.element.setCursor(element, 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;
      }
      ol.ext.element.setCursor(e.map, 'pointer');
    } else {
      ol.ext.element.setCursor(e.map, 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
          ol.ext.element.setCursor(element, this.cursor_)
        }
      } else if (this.previousCursor_ !== undefined) {
        ol.ext.element.setCursor(element, 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-3.
 *  @param {function|undefined} options.alignTolerance Tolerance to check allignment. Default is 1e-3.
 * @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))
    }
    // Node tolerance to snap
    this.tolerance_ = options.tolerance || 1e-3
    // Split tolerance between the calculated intersection and the geometry
    this.tolerance2_ = options.alignTolerance || options.tolerance || 1e-3
    // 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, extent, 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.tolerance2_)
            if (g && g.length > 1) {
              found = f
              return true
            }
          }
        }
      }
      return false
    }
    // Split with a point
    if (c.length === 1) {
      seg = [c[0], c[0]]
      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.tolerance2_)
          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]]
      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.tolerance2_)
      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) {
    if (!this.getActive()) return;
    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.getActive()) return;
    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.getActive()) return;
    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') || '')])
      },
      updateWhileAnimating: true,
      updateWhileInteracting: true,
    })
    // 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) {
      oldMap.removeLayer(this.overlayLayer_)
      if (this.previousCursor_) {
        ol.ext.element.setCursor(oldMap, 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} [options]
   *  @param {ol.style.Stroke} [stroke] stroke style for selection rectangle, default red dash
   *  @param {ol.style.Fill} [fill] fill style for selection rectangle, default red
   *  @param {ol.style.Stroke} [pointStroke] stroke style for handles, default red
   *  @param {ol.style.Fill} [pointFill] fill style for handles, default white
   */
  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(ol.extent.getCenter(ext))
        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()
      // Get coordinate of the handle (for snapping)
      this.coordinate_ = feature.get('handle') ? feature.getGeometry().getCoordinates() : evt.coordinate;
      this.pixel_ = this.getMap().getCoordinateFromPixel(this.coordinate_) // 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()
        ol.ext.element.setCursor(element, 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
        }
        ol.ext.element.setCursor(element, c);
      } else {
        if (this.previousCursor_ !== undefined) {
          ol.ext.element.setCursor(element, 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()
      ol.ext.element.setCursor(element, 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.getSource() instanceof ol.source.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="https://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 || 'https://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 none
 *  @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://data.geopf.fr/wmts' // 'https://wxs.ign.fr/geoportail/wmts' old version
    var gppKey = options.gppKey || options.key || ''
    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="https://geoservices.ign.fr/">Géoservices</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) {
  // Old gppkey
  if (gppKey === 'gpf') gppKey = '';
  // Check server
  if (!server) {
    if (gppKey) {
      server = 'https://data.geopf.fr/private/wmts';
    } else {
      server = 'https://data.geopf.fr/wmts';
    }
  } 
  // Add api key
  if (/geopf/.test(server)) {
    if (gppKey) {
      return server + '?apikey=' + gppKey;
    } else {
      return server;
    }
  } else {
    return server.replace(/^(https?:\/\/[^/]*)(.*)$/, "$1/" + gppKey + "$2")
  }
  /*
    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(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.typeName WFS type name parameter
 *  @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.outputFormat=application/json] WFS outputFormat parameter
 *  @param {number} [options.tileZoom=14] zoom to load the tiles
 *  @param {number} [options.maxFeatures] maximum features returned in the WFS
 *  @param {number} [options.featureLimit=Infinity] 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=' + (options.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 = this._ratio = e.frameState.pixelRatio
    var ctx = e.context
    this.matrix_ = e.frameState.coordinateToPixelTransform
    this.inversePixelTransform_ = e.inversePixelTransform;
    if (e.frameState.size) {
      this.center_ = [e.frameState.size[0] / 2, e.frameState.size[1]]
    }
    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]
    ]
    var p1 = [
      p0[0] + h / this.res_ * (p0[0] - this.center_[0]),
      p0[1] + h / this.res_ * (p0[1] - this.center_[1])
    ]
    var version = parseFloat(ol.util.VERSION);
    // ol@v9.1+
    if (version > 9.0) {
      p0 = [
        p0[0] * this.inversePixelTransform_[0] - p0[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
        - p0[0] * this.inversePixelTransform_[2] + p0[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
      ]
      p1 = [
        p1[0] * this.inversePixelTransform_[0] - p1[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
        - p1[0] * this.inversePixelTransform_[2] + p1[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
      ]
      return {
        p0: [p0[0]/this._ratio, p0[1]/this._ratio],
        p1: [p1[0]/this._ratio, p1[1]/this._ratio]
      }
    }
    // Old versions
    return {
      p0: p0,
      p1: p1
    }
  }
  /** 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)
    // Animate the cluster
    this.on(['precompose', 'prerender'], this.animate.bind(this))
    this.on(['postcompose', 'postrender'], this.postanimate.bind(this))
  }
  /** Set the cluster source
   * @param {ol.source.Vector} source
   */
  setSource(source) {
    if (!this._saveClusterFn) this._saveClusterFn = this.saveCluster.bind(this)
    // Save cluster before change
    if (this.getSource()) this.getSource().un('change', this._saveClusterFn)
    ol.layer.Vector.prototype.setSource.call(this, source)
    if (this.getSource()) this.getSource().on('change', this._saveClusterFn)
  }
  /** 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 (options.gppKey || options.key) tileoptions.gppKey = options.gppKey || options.key;
    // 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 / OLD VERSION: 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) {
    var onSuccess = function () { }
    var onError = function () { }
    var onFinally = function () { }
    this.getCapabilities(gppKey).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, use then, catch and finally to get the response
   */
  static getCapabilities(gppKey) {
    // Generic API key (for compatibility)
    if (gppKey === 'gpf') gppKey = undefined;
    var onSuccess = function () { }
    var onError = function () { }
    var onFinally = function () { }
    // Find min max zoom in the list
    function getMinMaxZoom(z) {
      var zoom = {
        min: parseFloat(z[0].TileMatrix),
        max: parseFloat(z[0].TileMatrix)
      }
      for (var k=1; k<z.length; k++) {
        zoom.min = Math.min(zoom.min, parseFloat(z[k].TileMatrix));
        zoom.max = Math.max(zoom.max, parseFloat(z[k].TileMatrix));
      }
      return zoom;
    }
    // Guess a theme in the list
    function getTheme(id) {
      for (var i=0; i<ol.layer.Geoportail.themes.length; i++) {
        if (ol.layer.Geoportail.themes[i].rex.test(id)) return ol.layer.Geoportail.themes[i].theme;
      }
      return 'autre';
    }
    // Next version Geoplateforme with getcapabilities
    var server = gppKey ? 'https://data.geopf.fr/private/wmts' : 'https://data.geopf.fr/wmts';
    var url = server + "?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities";
    if (gppKey) {
      url += "&apikey="+gppKey;
    }
    ol.ext.Ajax.get({
      url: url,
      dataType: 'TEXT',
      error: function (e) {
        onError(e)
        onFinally({})
      },
      success: function (resp) {
        // Get layerinfo
        var layersInfo = (new DOMParser()).parseFromString(resp, "text/xml")
        layersInfo = layersInfo.getElementsByTagName('Layer')
        // Parse config
        var parser = new ol.format.WMTSCapabilities()
        resp = parser.read(resp)
        var layers = resp.Contents.Layer;
        var capabilities = {};
        var themes = {};
        for (var i=0; i<layers.length; i++) {
          var l = layers[i];
          var zoom = getMinMaxZoom(l.TileMatrixSetLink[0].TileMatrixSetLimits)
          var theme = getTheme(l.Identifier)
          if (!themes[theme]) themes[theme] = {};
          // Legend
          var legend = []
          if (l.Style) {
            l.Style.forEach(function (s) {
              if (s.LegendURL) {
                legend.push(s.LegendURL[0].href)
              }
            })
          }
          themes[theme][l.Identifier] = capabilities[l.Identifier] = {
            layer: l.Identifier,
            key: gppKey,
            theme: theme,
            desc: l.Abstract,
            server: server,
            bbox: l.WGS84BoundingBox,
            format: l.Format[0],
            minZoom: zoom.min,
            maxZoom: zoom.max,
            originators: { 'Geoservices': { attribution: 'Géoservices', href: 'https://geoservices.ign.fr/' } },
            queryable: layersInfo[i].getElementsByTagName('InfoFormat').length > 0,
            style: (l.Style && l.Style.length ? l.Style[0].Identifier : 'normal'),
            tilematrix: 'PM',
            title: l.Title,
            legend: legend
          }
        }
        // Return capabilities
        onSuccess(capabilities, themes)
        onFinally(capabilities, themes)
      }
    })
    // 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 = {
  "GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2": {"layer":"GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2","theme":"cartes","desc":"Cartographie multi-échelles sur le territoire national, issue des bases de données vecteur de l’IGN, mis à jour régulièrement et réalisée selon un processus entièrement automatisé.","server":"https://data.geopf.fr/wmts","bbox":[-175,-85,175,85],"format":"image/png","minZoom":0,"maxZoom":19,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Plan IGN v2"},
  "CADASTRALPARCELS.PARCELLAIRE_EXPRESS": {"layer":"CADASTRALPARCELS.PARCELLAIRE_EXPRESS","theme":"parcellaire","desc":"Plan cadastral informatisé vecteur de la DGFIP édition Juillet 2023.","server":"https://data.geopf.fr/wmts","bbox":[-63.3725,-21.4756,55.9259,51.3121],"format":"image/png","minZoom":0,"maxZoom":19,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"PCI vecteur"},
  "ORTHOIMAGERY.ORTHOPHOTOS": {"layer":"ORTHOIMAGERY.ORTHOPHOTOS","theme":"ortho","desc":"Photographies aériennes","server":"https://data.geopf.fr/wmts","bbox":[-180,-89,180,89],"format":"image/jpeg","minZoom":0,"maxZoom":20,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":true,"style":"normal","tilematrix":"PM","title":"Photographies aériennes"},
  // Need API key
  "GEOGRAPHICALGRIDSYSTEMS.MAPS": {"layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS","theme":"cartes","desc":"Cartes IGN","server":"https://data.geopf.fr/private/wmts","bbox":[-180,-68.1389,180,80],"format":"image/jpeg","minZoom":0,"maxZoom":18,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Cartes IGN"},
  // Other layers
  "ADMINEXPRESS-COG-CARTO.LATEST": {"layer":"ADMINEXPRESS-COG-CARTO.LATEST","theme":"administratif","desc":"Limites administratives Express COG code officiel géographique 2023","server":"https://data.geopf.fr/wmts","bbox":[-63.3725,-21.4756,55.9259,51.3121],"format":"image/png","minZoom":6,"maxZoom":16,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":true,"style":"normal","tilematrix":"PM","title":"ADMINEXPRESS COG CARTO"},
  "GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN": {"layer":"GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN","theme":"cartes","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","server":"https://data.geopf.fr/wmts","bbox":[-63.1614,-21.5446,56.0018,51.0991],"format":"image/png","minZoom":0,"maxZoom":17,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Carte des pentes"},
  "ELEVATION.SLOPES": {"layer":"ELEVATION.SLOPES","theme":"altimetrie","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®.","server":"https://data.geopf.fr/wmts","bbox":[-179.5,-75,179.5,75],"format":"image/jpeg","minZoom":6,"maxZoom":14,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Altitude"},
  "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]}]}}},
  // "GEOGRAPHICALGRIDSYSTEMS.MAPS.BDUNI.J1": { "theme":"cartes", "server":"https://data.geopf.fr/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": {"layer":"TRANSPORTNETWORKS.ROADS","theme":"topographie","desc":"Affichage du réseau routier français et européen.","server":"https://data.geopf.fr/wmts","bbox":[-63.9692,-21.4969,55.9644,71.5841],"format":"image/png","minZoom":6,"maxZoom":18,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Routes"},
  "GEOGRAPHICALNAMES.NAMES": {"layer":"GEOGRAPHICALNAMES.NAMES","theme":"topographie","desc":"Affichage des noms des villes, villages, lieux-dits ...","server":"https://data.geopf.fr/wmts","bbox":[-63.9692,-21.4969,55.9644,71.5841],"format":"image/png","minZoom":6,"maxZoom":18,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Dénominations géographiques","legend":["https://data.geopf.fr/annexes/ressources/legendes/LEGEND.jpg"]},
  "CARTES.NATURALEARTH": {"layer":"CARTES.NATURALEARTH","theme":"cartes","desc":"Carte générale du monde politique avec les frontières et capitales d'Etat, les entités administratives secondaires, villes principales, fleuves, lacs et océans. Edition : 1.4.0","server":"https://data.geopf.fr/wmts","bbox":[-179.5,-75,179.5,75],"format":"image/jpeg","minZoom":1,"maxZoom":9,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Monde Natural Earth","legend":["https://data.geopf.fr/annexes/ressources/legendes/LEGEND.jpg"]},
  "GEOGRAPHICALGRIDSYSTEMS.MAPS.OVERVIEW": {"layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS.OVERVIEW","key":"","theme":"cartes","desc":"Carte Mondiale pour la mini-vue","server":"https://data.geopf.fr/wmts","bbox":[-179.5,-75,179.5,75],"format":"image/jpeg","minZoom":1,"maxZoom":8,"originators":{"Geoservices":{"attribution":"Géoservices","href":"https://geoservices.ign.fr/"}},"queryable":false,"style":"normal","tilematrix":"PM","title":"Carte Mondiale pour la mini-vue","legend":["https://data.geopf.fr/annexes/ressources/legendes/LEGEND.jpg"]},
};
/** List of theme with a regexp to filter layers by theme (with getcapabilities)
 * @API
 */
ol.layer.Geoportail.themes = [{
  theme: 'edugeo',
  rex: /EDUGEO|PVA_IGN_zone-marais|VERDUN|DOUAUMONT/
}, {
  theme: 'cartes',
  rex: /GEOGRAPHICALGRIDSYSTEMS|CARTES|SCAN/
}, {
  theme: 'energie',
  rex: /CHALEUR|CHAUD|FROID|METHAN|CONSO|ENR|ELECTRIQUE|EOLIEN|SOLAIRE|PRODUCTIBLE|GEOTHERM|PROD\.INSTALLATION|POTENTIEL\.HYDRO/
}, {
  theme: 'risque',
  rex: /SISMIC|RISKZONE/
}, {
  theme: 'agriculture',
  rex: /AGRICULTURE|PAC/
}, {
  theme: 'altimetrie',
  rex: /ELEVATION|SLOPE|ISOHYPSE/
}, {
  theme: 'parcellaire',
  rex: /PARCELS|Parcellaire|RPG|PCRS|PCI/
}, {
  theme: 'administratif',
  rex: /ADMIN/
}, {
  theme: 'ocsge',
  rex: /OCSGE|CoSIA/
}, {
  theme: 'clc',
  rex: /\.CLC|\.CHA/
}, {
  theme: 'environnement',
  rex: /PROTECTEDAREAS|FORETS|DEBROUSSAILLEMENT|LANDCOVER|PROTECTEDSITES|CHASSE|INPE|OFB/
}, {
  theme: 'topographie',
  rex: /BDCARTO|PARKING|RAILWAYS|BUILDINGS|RUNWAYS|UTILITYANDGOVERNMENTALSERVICES|GEOGRAPHICALNAMES\.NAMES|HYDROGRAPHY|TRANSPORTNETWORKS\.ROADS$|hedge.hedge/
}, {
  theme: 'transport',
  rex: /TRANSPORT|SECUROUTE|AUTOROUT/
}, {
  theme: 'economie',
  rex: /INSEE|AREAMANAGEMENT|PINEL|PRIORITYDISCTRICT/
}, {
  theme: 'agriculture',
  rex: /LANDUSE|PRAIRIES/
}, {
  theme: 'satellite',
  rex: /ORTHO-SAT|SPOT5|RAPIDEYE/
}, {
  theme: 'orthohisto',
  rex: /ORTHOPHOTOS\d|ORTHO-EXPRESS-\d/
}, {
  theme: 'ortho',
  rex: /ORTHOIMAGERY/
}];

/*	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: 'red'
      }))
    }
    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 = this._ratio = e.frameState.pixelRatio
    var ctx = e.context
    this.matrix_ = e.frameState.coordinateToPixelTransform
    this.inversePixelTransform_ = e.inversePixelTransform;
    // this.center_ = [ctx.canvas.width / 2 / ratio, ctx.canvas.height / ratio]
    this.center_ = [e.frameState.size[0] / 2, e.frameState.size[1]]
    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]
    ]
    var p1 = [
      p0[0] + h / this.res_ * (p0[0] - this.center_[0]),
      p0[1] + h / this.res_ * (p0[1] - this.center_[1])
    ]
    var version = parseFloat(ol.util.VERSION);
    // ol@v9.1+
    if (version > 9.0) {
      p0 = [
        p0[0] * this.inversePixelTransform_[0] - p0[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
        - p0[0] * this.inversePixelTransform_[2] + p0[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
      ]
      p1 = [
        p1[0] * this.inversePixelTransform_[0] - p1[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
        - p1[0] * this.inversePixelTransform_[2] + p1[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
      ]
      return {
        p0: [p0[0]/this._ratio, p0[1]/this._ratio],
        p1: [p1[0]/this._ratio, p1[1]/this._ratio]
      }
    }
    // Old versions
    return {
      p0: p0,
      p1: p1
    }
  }
  /** 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)));
    }
    this.setAnimation(!!map);
  }
  /** 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().getWidth()
          e.context.stroke()
        }
        e.context.restore()
      }
    }.bind(this))
    var update = function () {
      this.setPixelPosition()
    }.bind(this)
    this.on(['hide', 'show'], function () {
      setTimeout(update)
    }.bind(this))
    // Get events centroid
    function centroid(pevents) {
      var clientX = 0
      var clientY = 0
      var length = 0
      for (var i in pevents) {
        clientX += pevents[i].clientX
        clientY += pevents[i].clientY
        length++
      }
      return [clientX / length, clientY / length]
    }
    // Get events angle
    function angle() {
      var p1, p2, v = Object.keys(pointerEvents)
      if (v.length < 2)
        return false
      p1 = pointerEvents[v[0]]
      p2 = pointerEvents[v[1]]
      var v1 = [p2.clientX - p1.clientX, p2.clientY - p1.clientY]
      p1 = pointerEvents2[v[0]]
      p2 = pointerEvents2[v[1]]
      var v2 = [p2.clientX - p1.clientX, p2.clientY - p1.clientY]
      var d1 = Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1])
      var d2 = Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1])
      var a = Math.acos((v1[0] * v2[0] + v1[1] * v2[1]) / (d1 * d2)) * 360 / Math.PI
      if (v1[0] * v2[1] - v1[1] * v2[0] < 0)
        return -a
      else
        return a
    }
    // Get distance beetween events
    function distance(pevents) {
      var v = Object.keys(pevents)
      if (v.length < 2)
        return false
      return ol.coordinate.dist2d([pevents[v[0]].clientX, pevents[v[0]].clientY], [pevents[v[1]].clientX, pevents[v[1]].clientY])
    }
    // Handle popup move
    var pointerEvents = {}
    var pointerEvents2 = {}
    var pixelPosition = []
    var distIni, rotIni, scaleIni, move
    // down
    this.element.addEventListener('pointerdown', function (e) {
      e.preventDefault()
      e.stopPropagation()
      // Reset events to this position
      for (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.refresh()
  }
  /** 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) {
    this.set("active", active)
    this.refresh();
    return this.getActive()
  }
  /** Mouse move
   * @private
   */
  onMouseMove_(e) {
    if (!this.get("active")) {
      this.setPosition()
    } else {
      var isPosition = this.getPosition()
      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()
      }
      */
      if (!this.external_ && !isPosition) {
        this.refresh()
      }
    }
  }
  /** Refresh the view
   */
  refresh() {
    this.mgmap_.updateSize()
    this.setView_();
  }
  /** View has changed
   * @private
   */
  setView_(e) {
    // No map
    if (!this.getMap()) return
    // Not active
    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
 * @fires attribute
 * @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.positioning 
 *    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();
      Object.keys(atts).forEach(function(att) {
        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', {
              click: function(e) {
                this.dispatchEvent({ type: 'attribute', feature: feature, attribute: att, originalEvent: e })
              }.bind(this),
              parent: table 
            });
            ol.ext.element.create('TD', { 
              className: 'ol-label',
              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', {
              className: 'ol-value',
              html: content,
              parent: tr
            });
          }
        }
      }.bind(this))
    }
    // 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 {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 {number} [options.minZ=-99] min altitude (for undefined measures)
 *  @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 minZ = options.minZ === undefined ? -99 : options.minZ;
  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('|');
  param += '&resource=ign_rge_alti_wld'
  if (sampling) param += '&sampling='+sampling;
  ol.ext.Ajax.get({
    url: 'https://data.geopf.fr/altimetrie/1.0/calcul/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, Math.max(minZ, e.z)]);
        } else {
          pts.push([g[i][0], g[i][1], Math.max(minZ, 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 {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,
    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') {
      scale = scale.replace(':','/').split('/');
      fac = scale[1];
      if (!fac) fac = scale[0] || '';
      fac = fac.replace(/[^\d]/g,'');
      fac = parseInt(fac);
      if (scale[1]) {
        var num = parseInt(scale[0]);
        if (num) fac /= num;
      }
    }
    if (!fac) return;
    // Calculate new resolution
    var view = map.getView();
    var proj = view.getProjection();
    var center = view.getCenter();
    var px = map.getPixelFromCoordinate(center);
    px[1] += 1;
    var coord = map.getCoordinateFromPixel(px);
    var d = ol.sphere.getDistance(
      ol.proj.transform(center, proj, 'EPSG:4326'),
      ol.proj.transform(coord, proj, 'EPSG:4326'));
    d *= (dpi || 96) /.0254
    view.setResolution(view.getResolution()*fac/d);
    return fac;
  }
};

(function () {
/**
 * Visvalingam polyline simplification algorithm, adapted from http://bost.ocks.org/mike/simplify/simplify.js
 * This uses the [Visvalingam–Whyatt](https://en.wikipedia.org/wiki/Visvalingam%E2%80%93Whyatt_algorithm) algorithm.
 * @param {Object} options
 *  @param {number} [area] the tolerance area for simplification
 *  @param {number} [dist] a tolerance distance for simplification
 *  @param {number} [ratio=.8] a ratio of points to keep
 *  @param {number} [minPoints=2] minimum number of points to keep
 *  @param {boolean} [keepEnds] keep line ends
 * @return { LineString } A new, simplified version of the original geometry.
 * @api
 */
ol.geom.LineString.prototype.simplifyVisvalingam = function (options) {
  var points = this.getCoordinates();
  if (options.minPoints && options.minPoints >= points.length) {
    return new ol.geom.LineString(points);
  }
  var heap = minHeap(),
      maxArea = 0,
      triangle,
      triangles = [];
  points = points.map(function (d) { return d.slice(0,2); });
  for (var i = 1, n = points.length - 1; i < n; ++i) {
    triangle = points.slice(i - 1, i + 2);
    if (triangle[1][2] = area(triangle)) {
      triangles.push(triangle);
      heap.push(triangle);
    }
  }
  for (i = 0, n = triangles.length; i < n; ++i) {
    triangle = triangles[i];
    triangle.previous = triangles[i - 1];
    triangle.next = triangles[i + 1];
  }
  while (triangle = heap.pop()) {
    // If the area of the current point is less than that of the previous point
    // to be eliminated, use the latters area instead. This ensures that the
    // current point cannot be eliminated without eliminating previously-
    // eliminated points.
    if (triangle[1][2] < maxArea) triangle[1][2] = maxArea;
    else maxArea = triangle[1][2];
    if (triangle.previous) {
      triangle.previous.next = triangle.next;
      triangle.previous[2] = triangle[2];
      update(triangle.previous);
    } else {
      triangle[0][2] = triangle[1][2];
    }
    if (triangle.next) {
      triangle.next.previous = triangle.previous;
      triangle.next[0] = triangle[0];
      update(triangle.next);
    } else {
      triangle[2][2] = triangle[1][2];
    }
  }
  function update(triangle) {
    heap.remove(triangle);
    triangle[1][2] = area(triangle);
    heap.push(triangle);
  }
  // Get area to remove
  var w = options.area;
  if (options.dist) w = options.dist * options.dist / 2;
  // If no area
  if (w === undefined || options.minPoints) {
    // Get ordered weights 
    var weights = points.map(function (d) { return d.length < 3 ? Infinity : d[2] += Math.random(); /* break ties */ });
    weights.sort(function (a, b) {
      return b - a;
    });
    if (w) {
      // Check min points
      if (weights[options.minPoints] < w) {
        w = weights[options.minPoints]
      }
    } else {
      var pointsToKeep = options.minPoints;
      // Calculate ratio
      if (!pointsToKeep) {
        var ratio = options.ratio || .8
        pointsToKeep = Math.round(points.length * ratio);
      }
      pointsToKeep = Math.min(pointsToKeep, weights.length -1);
      w = weights[pointsToKeep]
    }
  }
  var result = points.filter(function (d) {
    return d[2] > w;
  });
  if (options.keepEnds) {
    if (!ol.coordinate.equal(result[0], points[0])) result.unshift(points[0]);
    if (!ol.coordinate.equal(result[result.length-1], points[points.length-1])) result.push(points[points.length-1]);
  }
  return new ol.geom.LineString(result);
};
function compare(a, b) {
  return a[1][2] - b[1][2];
}
function area(t) {
  return Math.abs((t[0][0] - t[2][0]) * (t[1][1] - t[0][1]) - (t[0][0] - t[1][0]) * (t[2][1] - t[0][1]));
}
function minHeap() {
  var heap = {},
      array = [];
  heap.push = function() {
    for (var i = 0, n = arguments.length; i < n; ++i) {
      var object = arguments[i];
      up(object.index = array.push(object) - 1);
    }
    return array.length;
  };
  heap.pop = function() {
    var removed = array[0],
        object = array.pop();
    if (array.length) {
      array[object.index = 0] = object;
      down(0);
    }
    return removed;
  };
  heap.size = function () {
    return array.length;
  };
  heap.remove = function(removed) {
    var i = removed.index,
        object = array.pop();
    if (i !== array.length) {
      array[object.index = i] = object;
      (compare(object, removed) < 0 ? up : down)(i);
    }
    return i;
  };
  function up(i) {
    var object = array[i];
    while (i > 0) {
      var up = ((i + 1) >> 1) - 1,
          parent = array[up];
      if (compare(object, parent) >= 0) break;
      array[parent.index = i] = parent;
      array[object.index = i = up] = object;
    }
  }
  function down(i) {
    var object = array[i];
    for (;;) {
      var right = (i + 1) * 2,
          left = right - 1,
          down = i,
          child = array[down];
      if (left < array.length && compare(array[left], child) < 0) child = array[down = left];
      if (right < array.length && compare(array[right], child) < 0) child = array[down = right];
      if (down === i) break;
      array[child.index = i] = child;
      array[object.index = i = down] = object;
    }
  }
  return heap;
}
})();

/*	Copyright (c) 2015 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** Pulse an extent on postcompose
*	@param {ol.coordinates} point to pulse
*	@param {ol.pulse.options} options pulse options param
*	  @param {ol.projectionLike|undefined} options.projection projection of coords, default no transform
*	  @param {Number} options.duration animation duration in ms, default 2000
*	  @param {ol.easing} options.easing easing function, default ol.easing.upAndDown
*	  @param {ol.style.Stroke} options.style stroke style, default 2px red
*/
ol.Map.prototype.animExtent = function(extent, options){
  var listenerKey;
  options = options || {};
  // Change to map's projection
  if (options.projection) {
    extent = ol.proj.transformExtent (extent, options.projection, this.getView().getProjection());
  }
  // options
  var start = new Date().getTime();
  var duration = options.duration || 1000;
  var easing = options.easing || ol.easing.upAndDown;
  var width = options.style ? options.style.getWidth() || 2 : 2;
  var color = options.style ? options.style.getColr() || 'red' : 'red';
  // Animate function
  function animate(event) {
    var frameState = event.frameState;
    var ratio = frameState.pixelRatio;
    var elapsed = frameState.time - start;
    if (elapsed > duration) {
      ol.Observable.unByKey(listenerKey);
    } else {
      var elapsedRatio = elapsed / duration;
      var p0 = this.getPixelFromCoordinate([extent[0],extent[1]]);
      var p1 = this.getPixelFromCoordinate([extent[2],extent[3]]);
      var context = event.context;
      context.save();
      context.scale(ratio,ratio);
      context.beginPath();
      // var e = easing(elapsedRatio)
      context.globalAlpha = easing(1 - elapsedRatio);
      context.lineWidth = width;
      context.strokeStyle = color;
      context.rect(p0[0], p0[1], p1[0]-p0[0], p1[1]-p0[1]);
      context.stroke();
      context.restore();
      // tell OL3 to continue postcompose animation
      frameState.animate = true;
    }
  }
  // Launch animation
  listenerKey = this.on('postcompose', animate.bind(this));
  try { this.renderSync(); } catch(e) { /* ok */ }
}

/** Create a cardinal spline version of this geometry.
 *	Original https://github.com/epistemex/cardinal-spline-js
 *	@see https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline
 *
 * @param {} options
 *	@param {Number} options.tension a [0,1] number / can be interpreted as the "length" of the tangent, default 0.5
 *  @param {Number} options.resolution size of segment to split
 *	@param {Integer} options.pointsPerSeg number of points per segment to add if no resolution is provided, default add 10 points per segment
 */
/** Cache cspline calculation on a geometry
 * @param {} options
 *	@param {Number} options.tension a [0,1] number / can be interpreted as the "length" of the tangent, default 0.5
 *  @param {Number} options.resolution size of segment to split
 *	@param {Integer} options.pointsPerSeg number of points per segment to add if no resolution is provided, default add 10 points per segment
 * @return {ol.geom.Geometry}
 */
ol.geom.Geometry.prototype.cspline = function(options){
  // Calculate cspline
  if (this.calcCSpline_){
    if (this.csplineGeometryRevision != this.getRevision() 
      || this.csplineOption != JSON.stringify(options)) {
      this.csplineGeometry_ = this.calcCSpline_(options)
      this.csplineGeometryRevision = this.getRevision();
      this.csplineOption = JSON.stringify(options);
    }
    return this.csplineGeometry_;
  } else {	
    // Default do nothing
    return this;
  }
};
ol.geom.GeometryCollection.prototype.calcCSpline_ = function(options) {
  var g=[], g0=this.getGeometries();
  for (var i=0; i<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 {string} [options.declutterMode] Declutter mode "declutter" | "obstacle" | "none" | undefined	
 *	@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,
      declutterMode: options.declutterMode,
    });
    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,
      declutterMode: this.getDeclutterMode ? this.getDeclutterMode() : null,
    });
    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 {string} [options.declutterMode] Declutter mode "declutter" | "obstacle" | "none" | undefined	
 *  @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,
      declutterMode: options.declutterMode,
    });
    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(),
      declutterMode: this.getDeclutterMode ? this.getDeclutterMode() : null,
    });
    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 {Number} [options.shadow=0] drop a shadow (the shadow width in pixel)
 *  @param {string} [options.declutterMode] Declutter mode "declutter" | "obstacle" | "none" | undefined	
 *  @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,
      declutterMode: options.declutterMode,
    })
    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(),
      declutterMode: this.getDeclutterMode ? this.getDeclutterMode() : null,
    })
    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 = parseFloat(ol.util.VERSION) > 9.1 ? 1 : Math.cos(e.rotation)
    var sin = parseFloat(ol.util.VERSION) > 9.1 ? 0 : 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 {string} [options.declutterMode] Declutter mode "declutter" | "obstacle" | "none" | undefined	
 *  @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,
      declutterMode: options.declutterMode,
    });
    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],
      declutterMode: this.getDeclutterMode ? this.getDeclutterMode() : null,
    });
    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));
})();
