/**
 * 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 v3.2.23
 * @author Jean-Marc Viglino
 * @see https://github.com/Viglino/ol-ext#,
 * @license BSD-3-Clause
 */
/** @namespace  ol.ext
 */
/*global ol*/
if (window.ol && !ol.ext) {
  ol.ext = {};
}
/** 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 = function(options) {
  options = options || {};
  ol.Object.call(this);
  this._auth = options.auth;
  this.set('dataType', options.dataType || 'JSON');
};
ol.ext.inherits(ol.ext.Ajax, ol.Object);
/** 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
 */
ol.ext.Ajax.get = function(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
 */
ol.ext.Ajax.getCORS = function(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
 */
ol.ext.Ajax.prototype.send = function (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 = function(options) {
  options = options || {};
  ol.Object.call(this);
  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 );
};
ol.ext.inherits(ol.ext.SVGFilter, ol.Object);
ol.ext.SVGFilter.prototype.NS = "http://www.w3.org/2000/svg";
ol.ext.SVGFilter.prototype.svg = null;
ol.ext.SVGFilter.prototype._id = 0;
/** Get filter ID
 * @return {string}
 */
ol.ext.SVGFilter.prototype.getId = function() {
  return this._id;
};
/** Remove from DOM
 */
ol.ext.SVGFilter.prototype.remove = function() {
  this.element.remove();
};
/** Add a new operation
 * @param {ol.ext.SVGOperation} operation
 */
ol.ext.SVGFilter.prototype.addOperation = function(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
 */
ol.ext.SVGFilter.prototype.grayscale = function(value) {
  this.addOperation({
    feoperation: 'feColorMatrix',
    type: 'saturate',
    values: value || 0
  });
};
/** Add a luminanceToAlpha operation
 * @param {*} options
 *  @param {number} options.gamma enhance gamma, default 0
 */
ol.ext.SVGFilter.prototype.luminanceToAlpha = function(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
      }]
    });
  }
};
ol.ext.SVGFilter.prototype.applyTo = function(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;
};

/** SVG filter 
 * @param {string | *} attributes a list of attributes or fe operation
 *  @param {string} attributes.feoperation filter primitive tag name
 */
ol.ext.SVGOperation = function(attributes) {
  if (typeof(attributes)==='string') attributes = { feoperation: attributes };
  if (!attributes || !attributes.feoperation) {
    console.error('[SVGOperation]: no operation defined.')
    return;
  }
  ol.Object.call(this);
  this._name = attributes.feoperation;
  this.element = document.createElementNS( this.NS, this._name );
  this.setProperties(attributes);
  if (attributes.operations instanceof Array) this.appendChild(attributes.operations);
};
ol.ext.inherits(ol.ext.SVGOperation, ol.Object);
ol.ext.SVGOperation.prototype.NS = "http://www.w3.org/2000/svg";
/** Get filter name
 * @return {string}
 */
ol.ext.SVGOperation.prototype.getName = function() {
  return this._name;
};
/** Set Filter attribute
 * @param {*} attributes
 */
ol.ext.SVGOperation.prototype.set = function(k, val) {
  if (!/^feoperation$|^operations$/.test(k)) {
    ol.Object.prototype.set.call(this, k, val);
    this.element.setAttribute( k, val );
  }
};
/** Set Filter attributes
 * @param {*} attributes
 */
ol.ext.SVGOperation.prototype.setProperties = function(attributes) {
  attributes = attributes || {};
  for (var k in attributes) {
    this.set(k, attributes[k])
  }
};
/** Get SVG  element
 * @return {Element}
 */
ol.ext.SVGOperation.prototype.geElement = function() {
  return this.element;
};
/** Append a new operation
 * @param {ol.ext.SVGOperation} operation
 */
ol.ext.SVGOperation.prototype.appendChild = function(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() );
  }
};

// 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);
};

/** 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': {
          this.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
  });
  console.log(input)
  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) {
    moving = true;
    if (pos !== false) {
      var delta = (isbar ? -1/scale : 1) * (pos - e[page]);
      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);
    }
  };
  // Animate scroll
  var animate = function(to) {
    var step = (to>0) ? Math.min(100, to/2) : Math.max(-100, to/2);
    to -= step;
    elt[scroll] += step;
    if (-1 < to && to < 1) {
      if (moving) setTimeout(function() { elt.classList.remove('ol-move'); });
      else elt.classList.remove('ol-move');
      moving = false;
      onmove(false);
    } else {
      setTimeout(function() {
        animate(to);
      }, 40);
    }
  }
  // Initialize scroll container for minibar
  var scrollContainer, scrollbar;
  if (options.vertical && options.minibar) {
    var init = function(b) {
      // only once
      elt.removeEventListener('pointermove', init);
      elt.parentNode.classList.add('ol-miniscroll');
      scrollbar = ol.ext.element.create('DIV');
      scrollContainer = ol.ext.element.create('DIV', {
        className: 'ol-scroll',
        html: scrollbar
      });
      elt.parentNode.insertBefore(scrollContainer, elt);
      // Move scrollbar
      scrollbar.addEventListener('pointerdown', function(e) {
        isbar = true;
        onPointerDown(e)
      });
      // Handle mousewheel
      if (options.mousewheel) {
        ol.ext.element.addListener(scrollContainer, 
          ['mousewheel', 'DOMMouseScroll', 'onmousewheel'], 
          function(e) { onMouseWheel(e) }
        );
        ol.ext.element.addListener(scrollbar, 
          ['mousewheel', 'DOMMouseScroll', 'onmousewheel'], 
          function(e) { onMouseWheel(e) }
        );
      }
      // Update on enter
      elt.parentNode.addEventListener('pointerenter', updateMinibar);
      // Update on resize
      window.addEventListener('resize', updateMinibar);
      // Update
      if (b!==false) updateMinibar();
    };
    // Allready inserted in the DOM
    if (elt.parentNode) init(false);
    // or wait when ready
    else elt.addEventListener('pointermove', init);
    // Update on scroll
    elt.addEventListener('scroll', function() {
      updateMinibar();
    });
  }
  // Enable scroll
  elt.style['touch-action'] = 'none';
  elt.style['overflow'] = 'hidden';
  elt.classList.add('ol-scrolldiv');
  // Start scrolling
  ol.ext.element.addListener(elt, ['pointerdown'], function(e) {
    isbar = false;
    onPointerDown(e)
  });
  // Prevet click when moving...
  elt.addEventListener('click', function(e) {
    if (elt.classList.contains('ol-move')) {
      e.preventDefault();
      e.stopPropagation();
    }
  }, true);
  // Stop scrolling
  var onPointerUp = function(e) {
    dt = new Date() - dt;
    if (dt>100 || isbar) {
      // User stop: no speed
      speed = 0;
    } else if (dt>0) {
      // Calculate new speed
      speed = ((speed||0) + (pos - e[page]) / dt) / 2;
    }
    animate(options.animate===false ? 0 : speed*200);
    pos = false;
    speed = 0;
    dt = 0;
    // Add class to handle click (on iframe / double-click)
    if (!elt.classList.contains('ol-move')) {
      elt.classList.add('ol-hasClick')
      setTimeout(function() { elt.classList.remove('ol-hasClick'); }, 500);
    } else {
      elt.classList.remove('ol-hasClick');
    }
    isbar = false;
    window.removeEventListener('pointermove', onPointerMove)
    ol.ext.element.removeListener(window, ['pointerup','pointercancel'], onPointerUp);
  };
  // Handle mousewheel
  var onMouseWheel = function(e) {
    var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
    elt.classList.add('ol-move');
    elt[scroll] -= delta*30;
    elt.classList.remove('ol-move');
    return false;
  }
  if (options.mousewheel) { // && !elt.classList.contains('ol-touch')) {
    ol.ext.element.addListener(elt, 
      ['mousewheel', 'DOMMouseScroll', 'onmousewheel'], 
      onMouseWheel
    );
  }
  return { 
    refresh: updateMinibar
  }
};
/** Dispatch an event to an Element 
 * @param {string} eventName
 * @param {Element} element
 */
ol.ext.element.dispatchEvent = function (eventName, element) {
  var event;
  try {
    event = new CustomEvent(eventName);
  } catch(e) {
    // Try customevent on IE
    event = document.createEvent("CustomEvent");
    event.initCustomEvent(eventName, true, true, {});
  }
  element.dispatchEvent(event);
};

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

/** @namespace ol.ext.imageLoader
 */
if (window.ol) window.ol.ext.imageLoader = {};
/** 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 = function(options) {
  options = options || {};
  ol.ext.SVGFilter.call(this, { 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 });
};
ol.ext.inherits(ol.ext.SVGFilter.Laplacian, ol.ext.SVGFilter);

/*	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 = function(options) {
  options = options || {};
  ol.ext.SVGFilter.call(this, { 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();
};
ol.ext.inherits(ol.ext.SVGFilter.Prewitt, ol.ext.SVGFilter);

/*	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 = function(options) {
  options = options || {};
  ol.ext.SVGFilter.call(this, { 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();
};
ol.ext.inherits(ol.ext.SVGFilter.Roberts, ol.ext.SVGFilter);

/*	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 = function(options) {
  options = options || {};
  ol.ext.SVGFilter.call(this, { 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 });
};
ol.ext.inherits(ol.ext.SVGFilter.Sobel, ol.ext.SVGFilter);

/** Vanilla JS geographic inputs
 * color, size, width, font, symboles, dash, arrow, pattern
 */
/** @namespace  ol.ext.input
 */
/*global ol*/
if (window.ol) {
  ol.ext.input = {};
}
/** 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 = function(options) {
  options = options || {};
  ol.Object.call(this);
  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))
};
ol.ext.inherits(ol.ext.input.Base, ol.Object);
/** Listen to drag event
 * @param {Element} elt 
 * @param {function} cback when draggin on the element
 * @private
 */
ol.ext.input.Base.prototype._listenDrag = function(elt, cback) {
  var handle = function(e) {
    this.moving = true;
    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;
        }.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
 */
 ol.ext.input.Base.prototype.setValue = function(v) {
  if (v !== undefined) this.input.value = v;
  this.input.dispatchEvent(new Event('change'));
};
/** Get the current getValue
 * @returns {string}
 */
ol.ext.input.Base.prototype.getValue = function() {
  return this.input.value;
};
/** Get the input element
 * @returns {Element}
 */
ol.ext.input.Base.prototype.getInputElement = function() {
  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 = function(options) {
  options = options || {};
  ol.ext.input.Base.call(this, 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;
  // 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) / step) * step;
    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();
};
ol.ext.inherits(ol.ext.input.Slider, ol.ext.input.Base);

/** 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 = function(options) {
  options = options || {};
  options.hidden = options.hidden!==false;
  ol.ext.input.Base.call(this, 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));
};
ol.ext.inherits(ol.ext.input.PopupBase, ol.ext.input.Base);
/** show/hide color picker
 * @param {boolean} [b=false]
 */
ol.ext.input.PopupBase.prototype.collapse = function(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}
 */
ol.ext.input.PopupBase.prototype.isCollapsed = function() {
  return !this._elt.popup.classList.contains('ol-visible');
};
/** Toggle the popup
 */
ol.ext.input.PopupBase.prototype.toggle = function() {
  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 = function(options) {
  options = options || {};
  ol.ext.input.Base.call(this, 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));
};
ol.ext.inherits(ol.ext.input.Checkbox, ol.ext.input.Base);
ol.ext.input.Checkbox.prototype.isChecked = function () {
  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 = function(options) {
  ol.Object.call(this);
  this.element = ol.ext.element.create('UL', {
    className: ('ol-collection-list '+(options.className||'')).trim(),
    parent: options.target
  })
  this.collection = options.collection;
  this._title = (typeof(options.getTitle) === 'function' ? options.getTitle : function(elt) { return elt.title });
  this.refresh();
  this.collection.on('change:length', 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));
};
ol.ext.inherits(ol.ext.input.Collection, ol.Object);
/** Select an item
 * @param {*} item
 */
ol.ext.input.Collection.prototype.select = function(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
 */
ol.ext.input.Collection.prototype.selectAt = function(n) {
  this.select(this.collection.item(n));
};
/** Get current selection
 * @returns {*}
 */
ol.ext.input.Collection.prototype.getSelect = function() {
  return this._currentItem;
};
/** Get current selection
 * @returns {number}
 */
ol.ext.input.Collection.prototype.getSelectPosition = function() {
  return this.collection.getArray().indexOf(this._currentItem);
};
/** Redraw the list
 */
ol.ext.input.Collection.prototype.refresh = function() {
  this.element.innerHTML = '';
  this._listElt = [];
  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 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 {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 = function(options) {
  options = options || {};
  options.hidden = options.hidden!==false;
  options.className = ('ol-ext-colorpicker ' + (options.hastab ? 'ol-tab ' : '') + (options.className || '')).trim();
  ol.ext.input.PopupBase.call(this, options);
  if (options.opacity===false) {
    this.element.classList.add('ol-nopacity');
  }
  this._cursor = {};
  var hsv = this._hsv = {};
  // Vignet
  this._elt.vignet = ol.ext.element.create('DIV', { className: 'ol-vignet', parent: this.element });
  // Bar 
  var bar = ol.ext.element.create('DIV', { className: 'ol-tabbar', parent: this._elt.popup });
  ol.ext.element.create('DIV', { 
    className: 'ol-tab', 
    html: options.paletteLabel || 'palette',
    click: function() {
      this.element.classList.remove('ol-picker-tab');
    }.bind(this),
    parent: bar
  });
  ol.ext.element.create('DIV', { 
    className: 'ol-tab', 
    html: options.pickerLabel || 'picker',
    click: function() {
      this.element.classList.add('ol-picker-tab');
    }.bind(this),
    parent: bar
  });
  // Popup container
  var container = ol.ext.element.create('DIV', { className: 'ol-container', parent: this._elt.popup });
  // Color picker
  var picker = this._elt.picker = ol.ext.element.create('DIV', { className: 'ol-picker', parent: container });
  var pickerCursor = this._cursor.picker = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: picker });
  this._listenDrag(picker, function(e) {
    var tx = Math.max(0, Math.min(e.offsetX / picker.clientWidth, 1));
    var ty = Math.max(0, Math.min(e.offsetY / picker.clientHeight, 1));
    pickerCursor.style.left = Math.round(tx*100) + '%';
    pickerCursor.style.top = Math.round(ty*100) + '%';
    hsv.s = tx * 100;
    hsv.v = 100 - ty * 100;
    this.setColor();
  }.bind(this));
  // Opacity cursor
  var slider = ol.ext.element.create('DIV', { className: 'ol-slider', parent: container });
  this._elt.slider = ol.ext.element.create('DIV', { parent: slider });
  var sliderCursor = this._cursor.slide = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: slider });
  this._listenDrag(slider, function(e) {
    var t = Math.max(0, Math.min(e.offsetX / slider.clientWidth, 1));
    hsv.a = t*100;
    sliderCursor.style.left = Math.round(t*100) + '%';
    this.setColor();
  }.bind(this));
  // Tint cursor
  var tint = ol.ext.element.create('DIV', { className: 'ol-tint', parent: container });
  var tintCursor = this._cursor.tint = ol.ext.element.create('DIV', { className: 'ol-cursor', parent: tint });
  this._listenDrag(tint, function(e) {
    var t = Math.max(0, Math.min(e.offsetY / tint.clientHeight, 1));
    hsv.h = t*360;
    tintCursor.style.top = Math.round(t*100) + '%';
    this.setColor();
  }.bind(this));
  // Clear button
  ol.ext.element.create('DIV', { 
    className: 'ol-clear', 
    click: function() {
      this.setColor([0,0,0,0]);
    }.bind(this),
    parent: container
  });
  // RVB input
  var rgb = ol.ext.element.create('DIV', { 
    className: 'ol-rgb', 
    parent: container
  });
  var changergb = function() {
    var r = Math.max(0, Math.min(255, parseInt(this._elt.r.value)));
    var g = Math.max(0, Math.min(255, parseInt(this._elt.g.value)));
    var b = Math.max(0, Math.min(255, parseInt(this._elt.b.value)));
    var a = Math.max(0, Math.min(1, parseFloat(this._elt.a.value)));
    this.setColor([r, g, b, a]);
  }.bind(this);
  this._elt.r = ol.ext.element.create('INPUT', { type: 'number', lang:'en-GB', change: changergb, min:0, max:255, parent: rgb });
  this._elt.g = ol.ext.element.create('INPUT', { type: 'number', lang:'en-GB', change: changergb, min:0, max:255, parent: rgb });
  this._elt.b = ol.ext.element.create('INPUT', { type: 'number', lang:'en-GB', change: changergb, min:0, max:255, parent: rgb });
  this._elt.a = ol.ext.element.create('INPUT', { type: 'number', lang:'en-GB', change: changergb, min:0, max:1, step:.1, parent: rgb });
  // Text color input
  this._elt.txtColor = ol.ext.element.create('INPUT', { 
    type: 'text', 
    className: 'ol-txt-color',
    change: function(){
      var color;
      this._elt.txtColor.classList.remove('ol-error')
      try {
        color = ol.color.asArray(this._elt.txtColor.value);
      } catch(e) {
        this._elt.txtColor.classList.add('ol-error');
      }
      if (color) this.setColor(color)
    }.bind(this), 
    parent: container
  });
  ol.ext.element.create('BUTTON', { 
    html: 'OK',
    click: function() {
      this._addCustomColor(this.getColor());
      this.collapse(true);
    }.bind(this),
    parent: container
  });
  var i;
  // Color palette
  this._paletteColor = {};
  this._elt.palette = ol.ext.element.create('DIV', {
    className: 'ol-palette',
    parent: this._elt.popup
  })
  for (i=0; i<8; i++) {
    var c = Math.round(255 - 255*i/7);
    this.addPaletteColor([c,c,c], c);//ol.color.toHexa([c,c,c]));
  }
  var colors = ['#f00', '#f90', '#ff0', '#0f0', '#0ff', '#48e', '#00f', '#f0f']
  colors.forEach(function(c){
    this.addPaletteColor(c, ol.color.toHexa(ol.color.asArray(c)));
  }.bind(this));
  for (i=0; i<5; i++) {
    colors.forEach(function(c){
      c = ol.color.toHSV(ol.color.asArray(c));
      c = [c[0], i/4*80+20, 100 - i/4*60];
      c = ol.color.fromHSV(c,1)
      this.addPaletteColor(c, ol.color.toHexa(c));
    }.bind(this));
  }
  // Custom colors
  ol.ext.element.create('HR', { parent: this._elt.palette });
  // Create custom color list
  if (!ol.ext.input.Color.customColorList) {
    ol.ext.input.Color.customColorList = new ol.Collection();
    var ccolor = JSON.parse(localStorage.getItem('ol-ext@colorpicker') || '[]');
    ccolor.forEach(function(c) {
      ol.ext.input.Color.customColorList.push(c);
    })
    ol.ext.input.Color.customColorList.on(['add','remove'], function(){
      localStorage.setItem('ol-ext@colorpicker', JSON.stringify(ol.ext.input.Color.customColorList.getArray()));
    });
  }
  // Handle custom color
  ol.ext.input.Color.customColorList.on('add', function(e) {
    this.addPaletteColor(this.getColorFromID(e.element));
  }.bind(this));
  ol.ext.input.Color.customColorList.on('remove', function(e) {
    if (this._paletteColor[e.element]) this._paletteColor[e.element].element.remove();
    delete this._paletteColor[e.element];
  }.bind(this));
  // Add new one
  ol.ext.input.Color.customColorList.forEach(function(c) {
    this._addCustomColor(this.getColorFromID(c));
  }.bind(this));
  // Current color
  this.setColor(options.color || [0,0,0,0]);
  this._currentColor = this.getColorID(this.getColor());
  // Add new palette color
  this.on('color', function() {
    this._addCustomColor(this.getColor());
    this._currentColor = this.getColorID(this.getColor());
    this.setColor();
  }.bind(this));
  // Update color on hide
  this.on('collapse', function(e) {
    if (!e.visible) {
      var c = this.getColor();
      if (this._currentColor !== this.getColorID(c)) {
        this.dispatchEvent({ type: 'color', color: c });
      }
    } else {
      this._currentColor = this.getColorID(this.getColor());
    }
  }.bind(this));
};
ol.ext.inherits(ol.ext.input.Color, ol.ext.input.PopupBase);
/** Custom color list
 * @private
 */
ol.ext.input.Color.customColorList = null;
/** Add color to palette
 * @param {ol.colorLike} color
 * @param {string} title
 * @param {boolean} select
 */
ol.ext.input.Color.prototype.addPaletteColor = function(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
 */
ol.ext.input.Color.prototype.showTab = function(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
 */
ol.ext.input.Color.prototype.getTab = function() {
  return this.element.classList.contains('ol-picker-tab') ? 'picker' : 'palette';
};
/** Select a color in the palette
 * @private
 */
ol.ext.input.Color.prototype._selectPalette = function(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> }
 */
ol.ext.input.Color.prototype.setColor = function(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>}
 */
ol.ext.input.Color.prototype.getColor = function(opacity) {
  return ol.color.fromHSV([this._hsv.h, this._hsv.s, this._hsv.v, (opacity !== false) ? this._hsv.a/100 : 1], 1);
}
/** 
 * @private
 */
ol.ext.input.Color.prototype._addCustomColor = function(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);
};
ol.ext.input.Color.prototype.clearCustomColor = function() {
  ol.ext.input.Color.customColorList.clear();
};
/** Convert color to id
 * @param {ol.colorLike} Color
 * @returns {number}
 */
ol.ext.input.Color.prototype.getColorID = function(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
 */
 ol.ext.input.Color.prototype.getColorFromID = function(id) {
  var c = id.split('-');
  return ([parseFloat(c[0]), parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3])]);
};

/** Checkbox input
 * @constructor
 * @extends {ol.ext.input.Base}
 * @param {*} options
 *  @param {string} [options.className]
 *  @param {Array<Object>} options.options an array of options to place in the popup { html:, title:, value: }
 *  @param {Element} [options.input] input element, if non create one
 *  @param {Element} [options.parent] parent element, if create an input
 *  @param {boolean} [options.hover=false] show popup on hover, default false or true if disabled or hidden
 *  @param {boolean} [options.hidden] the input is display:none
 *  @param {boolean} [options.disabled] disable input
 *  @param {boolean} [options.fixed=false] don't use a popup, default use a popup
 *  @param {string} [options.align=left] align popup left/right/middle
 */
ol.ext.input.List = function(options) {
  options = options || {};
  ol.ext.input.Base.call(this, 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));
};
ol.ext.inherits(ol.ext.input.List, ol.ext.input.Base);

/** 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 = function(options) {
  options = options || {};
  ol.ext.input.Checkbox.call(this, options);
  this.element.className = ('ol-ext-check ol-ext-radio ' + (options.className || '')).trim();
};
ol.ext.inherits(ol.ext.input.Radio, ol.ext.input.Checkbox);

/** 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 = function(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
        }
      })
    })
  })
  ol.ext.input.List.call(this, options);
  this._content.remove();
  this.element.classList.add('ol-size');
};
ol.ext.inherits(ol.ext.input.Size, ol.ext.input.List);
/** Get the current value
 * @returns {number}
 */
ol.ext.input.Size.prototype.getValue = function() {
  return parseFloat(ol.ext.input.List.prototype.getValue.call(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.Switch = function(options) {
  options = options || {};
  ol.ext.input.Checkbox.call(this, options);
  this.element.className = ('ol-ext-toggle-switch ' + (options.className || '')).trim();
};
ol.ext.inherits(ol.ext.input.Switch, ol.ext.input.Checkbox);

/** 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 = function(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
        }
      })
    })
  });
  ol.ext.input.List.call(this, options);
  this._content.remove();
  this.element.classList.add('ol-width');
};
ol.ext.inherits(ol.ext.input.Width, ol.ext.input.List);
/** Get the current value
 * @returns {number}
 */
ol.ext.input.Width.prototype.getValue = function() {
  return parseFloat(ol.ext.input.List.prototype.getValue.call(this));
}

/** @namespace  ol.legend
 */
/*global ol*/
if (window.ol && !ol.legend) {
  ol.legend = {};
}
/** Legend class to draw features in a legend element
 * @constructor
 * @fires select
 * @fires refresh
 * @param {*} options
 *  @param {String} options.title Legend title
 *  @param {ol.size | undefined} options.size Size of the symboles in the legend, default [40, 25]
 *  @param {number | undefined} options.margin Size of the symbole's margin, default 10
 *  @param { ol.style.Text | undefined } options.textStyle a text style for the legend, default 16px sans-serif
 *  @param { ol.style.Text | undefined } options.titleStyle 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 = function(options) {
  options = options || {};
  ol.Object.call(this);
  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));
  this._listElement = ol.ext.element.create('UL', {
    className: 'ol-legend'
  });
  this._canvas = document.createElement('canvas');
  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();
};
ol.ext.inherits(ol.legend.Legend, ol.Object);
/** Set legend title
 * @param {string} title
 */
ol.legend.Legend.prototype.setTitle = function(title) {
  this._title.setTitle(title);
  this.refresh();
};
/** Get legend title
 * @returns {string}
 */
ol.legend.Legend.prototype.getTitle = function() {
  return this._title.get('title');
};
/** Get text Style
 * @returns {ol.style.Text}
 */
ol.legend.Legend.prototype.getTextStyle = function() {
  return this._textStyle;
};
/** Set legend size
 * @param {ol.size} size
 */
 ol.legend.Legend.prototype.set = function(key, value, opt_silent) {
  ol.Object.prototype.set.call(this, key, value, opt_silent);
  if (!opt_silent) this.refresh();
};
/** Get legend list element
 * @returns {Element}
 */
ol.legend.Legend.prototype.getListElement = function() {
  return this._listElement;
};
/** Get legend canvas
 * @returns {HTMLCanvasElement}
 */
ol.legend.Legend.prototype.getCanvas = function() {
  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
 */
ol.legend.Legend.prototype.setStyle = function(style) {
  this._style = style;
  this.refresh();
};
/** Add a new item to the legend
 * @param {olLegendItemOptions|ol.legend.Item} item 
 */
ol.legend.Legend.prototype.addItem = function(item) {
  if (item instanceof ol.legend.Item) {
    this._items.push(item);
  } else {
    this._items.push(new ol.legend.Item(item));
  }
};
/** Get item collection
 * @param {ol.Collection} 
 */
ol.legend.Legend.prototype.getItems = function() {
  return this._items;
};
/** Draw legend text
 * @private
 */
ol.legend.Legend.prototype._drawText = function(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
 */
ol.legend.Legend.prototype._measureText = function(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
 */
ol.legend.Legend.prototype.refresh = function() {
  var table = this._listElement;
  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;
  // Calculate width
  ctx.font = this._titleStyle.getFont();
  var textWidth = this._measureText(ctx, this.getTitle('title')).width;
  this._items.forEach(function(r) {
    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));
  canvas.width = (textWidth + 2*margin) * ratio;
  canvas.height = (this._items.getLength()+1) * height * ratio;
  canvas.style.height = ((this._items.getLength()+1) * height) + '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
  this._items.forEach(function(r,i) {
    var index = i + (this.getTitle() ? 1 : 0);
    table.appendChild(r.getElement([width, height], function(b) {
      this.dispatchEvent({
        type: 'select', 
        index: i,
        symbol: b,
        item: r
      });
    }.bind(this)));
    var item = r.getProperties();
    ctx.textAlign = 'left';
    if (item.feature || item.typeGeom) {
      canvas = this.getLegendImage(item, canvas, index);
      ctx.font = r.get('textStyle') ? r.get('textStyle').getFont() : this._textStyle.getFont();
      this._drawText(ctx, r.get('title'), width + margin, (i+1.5)*height);
    } 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, (i+1.5)*height);
      } else {
        this._drawText(ctx, r.get('title'), margin, (i+1.5)*height);
      }
    }
  }.bind(this));
  // Done
  this.dispatchEvent({
    type: 'refresh',
    width: width,
    height: (this._items.length+1)*height
  });
};
/** Get the image for a style 
 * @param {olLegendItemOptions} item 
 * @param {Canvas|undefined} canvas a canvas to draw in, if none creat one
 * @param {int|undefined} row row number to draw in canvas, default 0
 * @return {CanvasElement}
 */
ol.legend.Legend.prototype.getLegendImage = function(options, canvas, row) {
  options = options || {};
  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.size || this.get('size'),
    lineHeight: options.lineHeight || this.get('lineHeight'),
    onload: function() {
      // Force refresh
      this.refresh();
    }.bind(this)
  }, canvas, row);
};
/** 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} row row number to draw in canvas, default 0
 */
ol.legend.Legend.getLegendImage = function(item, canvas, row) {
  item = item || {};
  if (typeof(item.margin) === 'undefined') item.margin = 10;
  var size = item.size || [40,25];
  item.onload = item.onload || function() {
    setTimeout(function() { 
      ol.legend.Legend.getLegendImage(item, canvas, row);
    }, 100);
  };
  var width = size[0] + 2 * item.margin;
  var height = item.lineHeight || (size[1] + 2 * item.margin);
  var ratio = ol.has.DEVICE_PIXEL_RATIO;
  if (!canvas) {
    row = 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.getImage();
        // Check image is loaded
        if (imgElt && imgElt.complete && !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 += (row*height) || 0;
  for (i=0; s= style[i]; i++) {
    vectorContext.setStyle(s);
    switch (typeGeom) {
      case ol.geom.Point:
      case 'Point':
      case 'MultiPoint':
        vectorContext.drawGeometry(new ol.geom.Point([cx, cy]));
        break;
      case ol.geom.LineString:
      case 'LineString':
      case 'MultiLineString': 
        ctx.save();
          ctx.rect(item.margin * ratio, 0, size[0] *  ratio, canvas.height);
          ctx.clip();
          vectorContext.drawGeometry(new ol.geom.LineString([[cx-sx, cy], [cx+sx, cy]]));
        ctx.restore();
        break;
      case ol.geom.Polygon:
      case 'Polygon':
      case 'MultiPolygon': 
        vectorContext.drawGeometry(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;
    }
  }
  ctx.restore();
  return canvas;
};

/** 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 {ol.size|undefined} size
 *  @property {number|undefined} margin
 */
/** A class for legend items
 * @constructor
 * @fires select
 * @param {olLegendItemOptions} options
 */
ol.legend.Item = function(options) {
  options = options || {};
  ol.Object.call(this, options);
  if (options.feature) this.set('feature', options.feature.clone());
};
ol.ext.inherits(ol.legend.Item, ol.Object);
/** Set the legend title
 * @param {string} title
 */
ol.legend.Item.prototype.setTitle = function(title) {
  this.set('title', title || '');
  this.changed();
};
/** Get element
 * @param {ol.size} size symbol size
 */
ol.legend.Item.prototype.getElement = function(size, onclick) {
  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 = function(options) {
  if (!options) options = {};
  // Define a style to draw on the canvas
  this.setStyle(options.style);
  ol.control.Control.call(this, options);
}
ol.ext.inherits(ol.control.CanvasBase, ol.control.Control);
/**
 * 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
 */
ol.control.CanvasBase.prototype.setMap = function (map) {
  this.getCanvas(map);
  var oldmap = this.getMap();
  if (this._listener) {
    ol.Observable.unByKey(this._listener);
    this._listener = null;
  }
  ol.control.Control.prototype.setMap.call(this, 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
 */
ol.control.CanvasBase.prototype.getCanvas = function(map) {
  return ol.ext.getMapCanvas(map);
};
/** Get map Canvas
 * @private
 */
ol.control.CanvasBase.prototype.getContext = function(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
 */
ol.control.CanvasBase.prototype.setStyle = function(style) {
  this._style = style ||  new ol.style.Style ({});
};
/** Get style
 * @api
 */
ol.control.CanvasBase.prototype.getStyle = function() {
  return this._style;
};
/** Get stroke
 * @api
 */
ol.control.CanvasBase.prototype.getStroke = function() {
  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
 */
ol.control.CanvasBase.prototype.getFill = function() {
  var t = this._style.getFill();
  if (!t) this._style.setFill(new ol.style.Fill ({ color:'#fff' }));
  return this._style.getFill();
};
/** Get stroke
 * @api
 */
ol.control.CanvasBase.prototype.getTextStroke = function() {
  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
 */
ol.control.CanvasBase.prototype.getTextFill = function() {
  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
 */
ol.control.CanvasBase.prototype.getTextFont = function() {
  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
 */
ol.control.CanvasBase.prototype._draw = function(/* 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 = function(options) {
  if (!options) options = {};
  this._features = this.setFeatures(options.features);
  var element;
  if (options.target) {
    element = document.createElement("div");
  } else {
    element = document.createElement("div");
    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
  });
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.setSources(options.source);
};
ol.ext.inherits(ol.control.SelectBase, ol.control.Control);
/** Set the current sources
 * @param {ol.source.Vector|Array<ol.source.Vector>|undefined} source
 */
ol.control.SelectBase.prototype.setSources = function (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
 */
ol.control.SelectBase.prototype.setFeatures = function (features) {
  if (features instanceof ol.Collection) this._features = features;
  else this._features = null;
};
/** Get feature collection to search in
 * @return {ol.Collection<ol.Feature>}
 */
ol.control.SelectBase.prototype.getFeatures = function () {
  return this._features;
};
/** List of operators / translation
 * @api
 */
ol.control.SelectBase.prototype.operationsList = {
  '=': '=',
  '!=': '≠',
  '<': '<',
  '<=': '≤',
  '>=': '≥',
  '>': '>',
  'contain': '⊂', // ∈
  '!contain': '⊄',	// ∉
  'regexp': '≃',
  '!regexp': '≄'
};
/** Escape string for regexp
 * @param {string} search
 * @return {string}
 */
ol.control.SelectBase.prototype._escape = function (s) {
  return 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
 */
ol.control.SelectBase.prototype._checkCondition = function (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
 */
ol.control.SelectBase.prototype._selectFeatures = function (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>}
 */
ol.control.SelectBase.prototype.getSources = function () {
  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
 */
ol.control.SelectBase.prototype.doSelect = function (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;
};

/*	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 = function(options){
  options = options || {};
  var element = document.createElement("div");
  element.className = (options.className || '') + " ol-button ol-unselectable ol-control";
  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;
  }
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  if (options.title) {
    this.set("title", options.title);
  }
  if (options.title) this.set("title", options.title);
  if (options.name) this.set("name", options.name);
};
ol.ext.inherits(ol.control.Button, ol.control.Control);
/** Set the control visibility
* @param {boolean} b 
*/
ol.control.Button.prototype.setVisible = function (val) {
  if (val) ol.ext.element.show(this.element);
  else ol.ext.element.hide(this.element);
};
/**
 * Set the button title
 * @param {string} title
 */
ol.control.Button.prototype.setTitle = function(title) {
  this.button_.setAttribute('title', title);
};
/**
 * Set the button html
 * @param {string} html
 */
ol.control.Button.prototype.setHtml = function(html) {
  ol.ext.element.setHTML (this.button_, html);
};
/**
 * Get the button element
 * @returns {Element}
 */
ol.control.Button.prototype.getButtonElement = function() {
  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 = function(options) {
  options = 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());
    });
  }
  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";
  ol.control.Button.call(this, options);
  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);
};
ol.ext.inherits(ol.control.Toggle, ol.control.Button);
/**
 * 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.
 */
ol.control.Toggle.prototype.setMap = function(map) {
  if (!map && this.getMap()) {
    if (this.interaction_) {
      this.getMap().removeInteraction (this.interaction_);
    }
    if (this.subbar_) this.getMap().removeControl (this.subbar_);
  }
  ol.control.Button.prototype.setMap.call(this, 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}
 */
ol.control.Toggle.prototype.getSubBar = function () {
  return this.subbar_;
};
/** Set the subbar associated with a control
 * @param {ol.control.Bar} [bar] a subbar if none remove the current subbar
 */
ol.control.Toggle.prototype.setSubBar = function (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
 */
ol.control.Toggle.prototype.getDisable = function() {
  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)
*/
ol.control.Toggle.prototype.setDisable = function(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
 */
ol.control.Toggle.prototype.getActive = function() {
  return this.element.classList.contains("ol-active");
};
/** Toggle control state active/deactive
 */
ol.control.Toggle.prototype.toggle = function() {
  if (this.getActive()) this.setActive(false);
  else this.setActive(true);
};
/** Change control state
 * @param {bool} b activate or deactivate the control, default false
 */
ol.control.Toggle.prototype.setActive = function(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
*/
ol.control.Toggle.prototype.setInteraction = function(i) {
  this.interaction_ = i;
};
/** Get the control interaction
* @return {_ol_interaction_} interaction associated with the control
*/
ol.control.Toggle.prototype.getInteraction = function() {
  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 = function(options) {
  var self = this;
  if (!options) options = {};
  if (options.typing == undefined) options.typing = 300;
  // Class name for history
  this._classname = options.className || 'search';
  var classNames = (options.className||'')+ ' ol-search'
    + (options.target ? '' : ' ol-unselectable ol-control');
  var element = ol.ext.element.create('DIV',{
    className: classNames 
  })
  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);
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  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_();
};
ol.ext.inherits(ol.control.Search, ol.control.Control);
/**
 * 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
 */
ol.control.Search.prototype.setMap = function (map) {
  if (this._listener) ol.Observable.unByKey(this._listener);
	this._listener = null;
  ol.control.Control.prototype.setMap.call(this, map);
  if (map) {
		this._listener = map.on('click', this._handleClick.bind(this));
	}
};
/** Collapse the search
 * @param {boolean} [b=true]
 * @api
 */
ol.control.Search.prototype.collapse = function (b) {
  if (b===false) this.element.classList.remove('ol-collapsed')
  else this.element.classList.add('ol-collapsed')
};
/** Get the input field
*	@return {Element} 
*	@api
*/
ol.control.Search.prototype.getInputField = function () {
  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
 */
ol.control.Search.prototype.getTitle = function (f) {
  return f.name || "No title";
};
/** Returns title as text
 *	@param {any} f feature to be displayed
 *	@return {string} 
 *	@api
 */
ol.control.Search.prototype._getTitleTxt = function (f) {
  return ol.ext.element.create('DIV', {
    html: this.getTitle(f)
  }).innerText;
};
/** Force search to refresh
 */
ol.control.Search.prototype.search = function () {
  var search = this.element.querySelector("input.search");
  this._triggerCustomEvent('search', search);
};
/** Reverse geocode
 * @param {Object} event
 *  @param {ol.coordinate} event.coordinate
 * @private
 */
ol.control.Search.prototype._handleClick = function (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
 */
ol.control.Search.prototype.reverseGeocode = function (/*coord, cback*/) {
  // this._handleSelect(f);
};
/** Trigger custom event on elemebt
 * @param {*} eventName 
 * @param {*} element 
 * @private
 */
ol.control.Search.prototype._triggerCustomEvent = function (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
*/
ol.control.Search.prototype.setInput = function (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
 */
ol.control.Search.prototype.select = function (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
 */
ol.control.Search.prototype._handleSelect = function (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);
  }
};
/** Current history */
ol.control.Search.prototype._history = {};
/** Save history (in the localstorage)
 */
ol.control.Search.prototype.saveHistory = function () {
  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) 
 */
ol.control.Search.prototype.restoreHistory = function () {
  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
 */
ol.control.Search.prototype.clearHistory = function () {
  this.set('history', []);
  this.saveHistory();
  this.drawList_();
};
/**
 * Get history table
 */
ol.control.Search.prototype.getHistory = function () {
  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
*/
ol.control.Search.prototype.autocomplete = function (s, cback) {
  cback ([]);
  return false;
  // or just return [];
};
/** Draw the list
* @param {Array} auto an array of search result
* @private
*/
ol.control.Search.prototype.drawList_ = function (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}
 */
ol.control.Search.prototype.equalFeatures = function (/* f1, f2 */) {
  return false;
};

/*	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 = function(options) {
  options = options || {};
  options.className = options.className || 'JSON';
  delete options.autocomplete;
  options.minLength = options.minLength || 3;
  options.typing = options.typing || 800;
  ol.control.Search.call(this, 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;
};
ol.ext.inherits(ol.control.SearchJSON, ol.control.Search);
/** 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
 */
ol.control.SearchJSON.prototype.ajax = function (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
 */
ol.control.SearchJSON.prototype.autocomplete = function (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
 */
ol.control.SearchJSON.prototype.requestData = function (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
 */
ol.control.SearchJSON.prototype.handleResponse = function (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 = function(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>';
  ol.control.SearchJSON.call(this, options);
  this.set('lang', options.lang);
  this.set('position', options.position);
};
ol.ext.inherits(ol.control.SearchPhoton, ol.control.SearchJSON);
/** 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
*/
ol.control.SearchPhoton.prototype.getTitle = function (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
 */
ol.control.SearchPhoton.prototype.requestData = function (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
 */
ol.control.SearchPhoton.prototype.handleResponse = function (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
 */
ol.control.SearchPhoton.prototype.equalFeatures = function (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
*/
ol.control.SearchPhoton.prototype.select = function (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
 */
ol.control.SearchPhoton.prototype.reverseData = function (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
 */
ol.control.SearchPhoton.prototype.reverseGeocode = function (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.authentication: basic authentication for the service API as btoa("login:pwd")
 *	@param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport.
 *	@param {string | undefined} options.label Text label to use for the search button, default "search"
 *	@param {boolean | undefined} options.reverse enable reverse geocoding, default false
 *	@param {string | undefined} options.placeholder placeholder, default "Search..."
 *	@param {number | undefined} options.typing a delay on each typing to start searching (ms), default 500.
 *	@param {integer | undefined} options.minLength minimum length to start searching, default 3
 *	@param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10
 *
 *	@param {StreetAddress|PositionOfInterest|CadastralParcel|Commune} options.type type of search. Using Commune will return the INSEE code, default StreetAddress,PositionOfInterest
 * @see {@link https://geoservices.ign.fr/documentation/geoservices/geocodage.html}
 */
ol.control.SearchGeoportail = function(options) {
  options = options || {};
  options.className = options.className || 'IGNF';
  options.typing = options.typing || 500;
  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>';
  ol.control.SearchJSON.call(this, options);
  this.set('type', options.type || 'StreetAddress,PositionOfInterest');
  this.set('timeout', options.timeout || 2000);
  // Authentication
  // this._auth = options.authentication;
};
ol.ext.inherits(ol.control.SearchGeoportail, ol.control.SearchJSON);
/** Reverse geocode
 * @param {ol.coordinate} coord
 * @param {function|*} options callback function called when revers located or options passed to the select event
 * @api
 */
ol.control.SearchGeoportail.prototype.reverseGeocode = function (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';
  // 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 (this.get('url').replace('ols/apis/completion','geoportail/ols'), 
    { 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'
    }
  );
};
/** 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
 */
ol.control.SearchGeoportail.prototype.getTitle = function (f) {
  return (f.fulltext);
};
/** 
 * @param {string} s the search string
 * @return {Object} request data (as key:value)
 * @api
 */
ol.control.SearchGeoportail.prototype.requestData = function (s) {
	return { 
    text: s, 
    type: this.get('type')==='Commune' ? 'PositionOfInterest' : this.get('type') || 'StreetAddress,PositionOfInterest', 
    maximumResponses: this.get('maxItems')
  };
};
/**
 * Handle server response to pass the features array to the display list
 * @param {any} response server response
 * @return {Array<any>} an array of feature
 * @api
 */
ol.control.SearchGeoportail.prototype.handleResponse = function (response) {
  var features = response.results;
  if (this.get('type') === 'Commune') {
    for (var i=features.length-1; i>=0; i--) {
      if ( features[i].kind 
        && (features[i].classification>5 || features[i].kind=="Département") ) {
          features.splice(i,1);
      }
    }
	}
	return features;
};
/** A ligne has been clicked in the menu > dispatch event
 * @param {any} f the feature, as passed in the autocomplete
 * @param {boolean} reverse true if reverse geocode
 * @param {ol.coordinate} coord
 * @param {*} options options passed to the event
 *	@api
 */
ol.control.SearchGeoportail.prototype.select = function (f, reverse, coord, options){
  if (f.x || f.y) {
    var c = [Number(f.x), Number(f.y)];
    // Add coordinate to the event
    try {
        c = ol.proj.transform (c, 'EPSG:4326', this.getMap().getView().getProjection());
    } catch(e) { /* ok */}
    // Get insee commune ?
    if (this.get('type')==='Commune') {
      this.searchCommune(f, function () {
        ol.control.Search.prototype.select.call(this, f, reverse, c, options);
        //this.dispatchEvent({ type:"select", search:f, coordinate: c, revers: reverse, options: options });
      });
    } else {
        ol.control.Search.prototype.select.call(this, f, reverse, c, options);
        //this.dispatchEvent({ type:"select", search:f, coordinate: c, revers: reverse, options: options });
    }
  } else {
    this.searchCommune(f);
  }
};
/** Search if no position and get the INSEE code
 * @param {string} s le nom de la commune
 */
ol.control.SearchGeoportail.prototype.searchCommune = function (f, cback) {
  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' }
  );
};

/*	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 = function(options) {
  options = options || {};
  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;
  }
  var element;
  if (options.target) {
    element = ol.ext.element.create('DIV', {
      className: options.switcherClass || "ol-layerswitcher"
    });
  } else {
    element = ol.ext.element.create('DIV', {
      className: (options.switcherClass || "ol-layerswitcher") +' ol-unselectable ol-control'
    });
    if (options.collapsed !== false) element.classList.add('ol-collapsed'); 
    else element.classList.add('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_
  });
  ol.control.Control.call (this, {
    element: element,
    target: options.target
  });
  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));
  }
};
ol.ext.inherits(ol.control.LayerSwitcher, ol.control.Control);
/** 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"
};
/** Test if a layer should be displayed in the switcher
 * @param {ol.layer} layer
 * @return {boolean} true if the layer is displayed
 */
ol.control.LayerSwitcher.prototype.displayInLayerSwitcher = function(layer) {
  return (layer.get('displayInLayerSwitcher')!==false);
};
/**
 * Set the map instance the control is associated with.
 * @param {_ol_Map_} map The map instance.
 */
ol.control.LayerSwitcher.prototype.setMap = function(map) {
  ol.control.Control.prototype.setMap.call(this, 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
 */
ol.control.LayerSwitcher.prototype.show = function() {
  this.element.classList.add('ol-forceopen');
  this.overflow();
  self.dispatchEvent({ type: 'toggle', collapsed: false });
};
/** Hide control
 */
ol.control.LayerSwitcher.prototype.hide = function() {
  this.element.classList.remove('ol-forceopen');
  this.overflow();
  self.dispatchEvent({ type: 'toggle', collapsed: true });
};
/** Toggle control
 */
ol.control.LayerSwitcher.prototype.toggle = function() {
  this.element.classList.toggle("ol-forceopen");
  this.overflow();
};
/** Is control open
 * @return {boolean}
 */
ol.control.LayerSwitcher.prototype.isOpen = function() {
  return this.element.classList.contains("ol-forceopen");
};
/** Add a custom header
 * @param {Element|string} html content html
 */
ol.control.LayerSwitcher.prototype.setHeader = function(html) {
  ol.ext.element.setHTML(this.header_, html);
};
/** Calculate overflow and add scrolls
 * @param {Number} dir scroll direction -1|0|1|'+50%'|'-50%'
 * @private
 */
ol.control.LayerSwitcher.prototype.overflow = function(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.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
 */
ol.control.LayerSwitcher.prototype._setLayerForLI = function(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') {
      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
 */
ol.control.LayerSwitcher.prototype.setLayerOpacity = function(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
 */
ol.control.LayerSwitcher.prototype.setLayerVisibility = function(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
 */
ol.control.LayerSwitcher.prototype._clearLayerForLI = function() {
  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
 */
ol.control.LayerSwitcher.prototype._getLayerForLI = function(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
 */
ol.control.LayerSwitcher.prototype.viewChange = function() {
  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
 */
ol.control.LayerSwitcher.prototype.getPanel = function() {
  return this.panelContainer_;
};
/** Draw the panel control (prevent multiple draw due to layers manipulation on the map with a delay function)
 * @api
 */
ol.control.LayerSwitcher.prototype.drawPanel = function() {
  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
 */
ol.control.LayerSwitcher.prototype.drawPanel_ = function() {
  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
 */
ol.control.LayerSwitcher.prototype.switchLayerVisibility = function(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
 */
ol.control.LayerSwitcher.prototype.testLayerVisibility = function(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
*/
ol.control.LayerSwitcher.prototype.dragOrdering_ = function(e) {
  e.stopPropagation();
  e.preventDefault();
  // Get params
  var self = this;
  var elt = e.currentTarget.parentNode.parentNode;
  var start = true;
  var panel = this.panel_; 
  var pageY;
  var pageY0 = e.pageY 
    || (e.touches && e.touches.length && e.touches[0].pageY) 
    || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY);
  var target, dragElt;
  var layer, group;
  elt.parentNode.classList.add('drag');
  // Stop ordering
  function stop() {
    if (target) {
      // Get drag on parent
      var drop = layer;
      var isSelected = self.getSelection() === drop;
      if (drop && target) {
        var collection ;
        if (group) collection = group.getLayers();
        else collection = self._layerGroup ?  self._layerGroup.getLayers() : self.getMap().getLayers();
        var layers = collection.getArray();
        // Switch layers
        for (var i=0; i<layers.length; i++) {
          if (layers[i]==drop) {
            collection.removeAt (i);
            break;
          }
        }
        for (var j=0; j<layers.length; j++) {
          if (layers[j] === target) {
            if (i>j) collection.insertAt (j,drop);
            else collection.insertAt (j+1,drop);
            break;
          }
        }
      }
      if (isSelected) self.selectLayer(drop);
      self.dispatchEvent({ type: "reorder-end", layer: drop, group: group });
    }
    elt.parentNode.querySelectorAll('li').forEach(function(li){
      li.classList.remove('dropover');
      li.classList.remove('dropover-after');
      li.classList.remove('dropover-before');
    });
    elt.classList.remove("drag");
    elt.parentNode.classList.remove("drag");
    self.element.classList.remove('drag');
    if (dragElt) dragElt.remove();
    ol.ext.element.removeListener(document, 'mousemove touchmove', move);
    ol.ext.element.removeListener(document, 'mouseup touchend touchcancel', stop);
  }
  // Ordering
  function move(e) {
    // First drag (more than 2 px) => show drag element (ghost)
    pageY = e.pageY 
        || (e.touches && e.touches.length && e.touches[0].pageY) 
        || (e.changedTouches && e.changedTouches.length && e.changedTouches[0].pageY);
    if (start && Math.abs(pageY0 - pageY) > 2) {
      start = false;
      elt.classList.add("drag");
      layer = self._getLayerForLI(elt);
      target = false;
      group = self._getLayerForLI(elt.parentNode.parentNode);
      // Ghost div
      dragElt = ol.ext.element.create('LI', { 
        className: 'ol-dragover',
        html: elt.innerHTML,
        style: { 
          position: "absolute", 
          "z-index": 10000, 
          left: elt.offsetLeft, 
          opacity: 0.5,
          width: ol.ext.element.outerWidth(elt),
          height: ol.ext.element.getStyle(elt,'height'),
        },
        parent: panel 
      });
      self.element.classList.add('drag');
      self.dispatchEvent({ type: "reorder-start", layer: layer, group: group });
    }
    // Start a new drag sequence
    if (!start) {
      e.preventDefault();
      e.stopPropagation();
      // Ghost div
      ol.ext.element.setStyle(dragElt, { top: pageY - ol.ext.element.offsetRect(panel).top + panel.scrollTop +5 });
      var li;
      if (!e.touches) {
        li = e.target;
      } else {
        li = document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY);
      }
      if (li.classList.contains("ol-switcherbottomdiv")) {
        self.overflow(-1);
      } else if (li.classList.contains("ol-switchertopdiv")) {
        self.overflow(1);
      }
      // Get associated li
      while (li && li.tagName!=='LI') {
        li = li.parentNode;
      }
      if (!li || !li.classList.contains('dropover')) {
        elt.parentNode.querySelectorAll('li').forEach(function(li) {
          li.classList.remove('dropover');
          li.classList.remove('dropover-after');
          li.classList.remove('dropover-before');
        });
      }
      if (li && li.parentNode.classList.contains('drag') && li !== elt) {
        target = self._getLayerForLI(li);
        // Don't mix layer level
        if (target && !target.get('allwaysOnTop') == !layer.get('allwaysOnTop')) {
          li.classList.add("dropover");
          li.classList.add((elt.offsetTop < li.offsetTop) ? 'dropover-after' : 'dropover-before');
        } else {
          target = false;
        }
        ol.ext.element.show(dragElt);
      } else {
        target = false;
        if (li===elt) ol.ext.element.hide(dragElt);
        else ol.ext.element.show(dragElt);
      }
      if (!target) dragElt.classList.add("forbidden");
      else dragElt.classList.remove("forbidden");
    }
  }
  // Start ordering
  ol.ext.element.addListener(document, 'mousemove touchmove', move);
  ol.ext.element.addListener(document, 'mouseup touchend touchcancel', stop);
};
/** Change opacity on drag 
*	@param {event} e drag event
*	@private
*/
ol.control.LayerSwitcher.prototype.dragOpacity_ = function(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
 */
ol.control.LayerSwitcher.prototype.drawList = function(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()?"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',// + (this.testLayerVisibility(layer) ? '' : ' ol-layer-hidden'),
      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,
      unselectable: 'on',
      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);
      },
      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
 */
ol.control.LayerSwitcher.prototype.getLayerClass = function(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
 */
ol.control.LayerSwitcher.prototype.selectLayer = function(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}
 */
ol.control.LayerSwitcher.prototype.getSelection = function() {
  return this._selectedLayer;
};
/** Handle progress bar for a layer
*	@private
*/
ol.control.LayerSwitcher.prototype.setprogress_ = function(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();
    });
  }
};

/*	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
 * @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 = function(options) {
  if (!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');
  ol.control.Control.call(this, {
    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]);
    }
  }
};
ol.ext.inherits(ol.control.Bar, ol.control.Control);
/** Set the control visibility
 * @param {boolean} val
 */
ol.control.Bar.prototype.setVisible = function (val) {
  if (val) this.element.style.display = '';
  else this.element.style.display = 'none';
}
/** Get the control visibility
 * @return {boolean} b
 */
ol.control.Bar.prototype.getVisible = function () {
  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.
 */
ol.control.Bar.prototype.setMap = function (map) {
  ol.control.Control.prototype.setMap.call(this, 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>}
 */
ol.control.Bar.prototype.getControls = function () {
  return this.controls_;
};
/** Set tool bar position
 * @param {string} pos a combinaison of top|left|bottom|right
 */
ol.control.Bar.prototype.setPosition = function (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
 */
ol.control.Bar.prototype.addControl = function (c) {
  this.controls_.push(c);
  c.setTarget(this.element);
  if (this.getMap()) {
    this.getMap().addControl(c);
  }
  // Activate and toogleOne
  c.on ('change:active', function(e) { this.onActivateControl_(e, c); }.bind(this));
  if (c.getActive) {
    // c.dispatchEvent({ type:'change:active', key:'active', oldValue:false, active:true });
    this.onActivateControl_({ target: c, active: c.getActive() }, c);
  }
};
/** Deativate all controls in a bar
 * @param {ol.control.Control} [except] a control
 */
ol.control.Bar.prototype.deactivateControls = function (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>}
 */
ol.control.Bar.prototype.getActiveControls = function () {
  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
 */
ol.control.Bar.prototype.setActive = function (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}
 */
ol.control.Bar.prototype.onActivateControl_ = function (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;
          }
        }
      }
    }
  }
};
/**
 * @param {string} name of the control to search
 * @return {ol.control.Control}
 */
ol.control.Bar.prototype.getControlsByName = function(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 = function(options) {
  if (!options) options = {};
  ol.control.Attribution.call(this, 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);
}
ol.ext.inherits(ol.control.CanvasAttribution, ol.control.Attribution);
/**
 * Draw attribution on canvas
 * @param {boolean} b draw the attribution on canvas.
 */
ol.control.CanvasAttribution.prototype.setCanvas = function (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 */ }
  }
};
/** Get map Canvas
 * @private
 */
ol.control.CanvasAttribution.prototype.getContext = ol.control.CanvasBase.prototype.getContext;
/**
 * Change the control style
 * @param {ol.style.Style} style
 */
ol.control.CanvasAttribution.prototype.setStyle = function (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
 */
ol.control.CanvasAttribution.prototype.setMap = function (map) {
  ol.control.CanvasBase.prototype.getCanvas.call(this, map);
  var oldmap = this.getMap();
  if (this._listener) ol.Observable.unByKey(this._listener);
  this._listener = null;
  ol.control.Attribution.prototype.setMap.call(this, 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
 */
ol.control.CanvasAttribution.prototype.drawAttribution_ = function(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.strokeText(text, left, h/2);
    ctx.fillText(text, 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).
*/
/**
 * @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 = function(options) {
  ol.control.ScaleLine.call(this, 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);
}
ol.ext.inherits(ol.control.CanvasScaleLine, ol.control.ScaleLine);
/** Get map Canvas
 * @private
 */
ol.control.CanvasScaleLine.prototype.getContext = ol.control.CanvasBase.prototype.getContext;
/**
 * 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
 */
ol.control.CanvasScaleLine.prototype.setMap = function (map) {	
  ol.control.CanvasBase.prototype.getCanvas.call(this, map);
  var oldmap = this.getMap();
  if (this._listener) ol.Observable.unByKey(this._listener);
  this._listener = null;
  ol.control.ScaleLine.prototype.setMap.call(this, 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
 */
ol.control.CanvasScaleLine.prototype.setStyle = function (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
 */
ol.control.CanvasScaleLine.prototype.drawScale_ = function(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.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();
}

/*	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 {ol.style.Style} options.style style used to draw the title.
 */
ol.control.CanvasTitle = function(options) {
  if (!options) options = {};
  var elt = ol.ext.element.create('DIV', {
    className: (options.className || '') + ' ol-control-title ol-unselectable',
    style: {
      display: 'block',
      visibility: 'hidden'
    }
  });
  ol.control.CanvasBase.call(this, {
    element: elt,
    style: options.style
  });
  this.setTitle(options.title || '');
  this.setVisible(options.visible);
  this.element.style.font = this.getTextFont();
};
ol.ext.inherits(ol.control.CanvasTitle, ol.control.CanvasBase);
/**
 * Change the control style
 * @param {ol.style.Style} style
 */
ol.control.CanvasTitle.prototype.setStyle = function (style) {
  ol.control.CanvasBase.prototype.setStyle.call(this, 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
 */
ol.control.CanvasTitle.prototype.setTitle = function (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
 */
ol.control.CanvasTitle.prototype.getTitle = function () {
  return this.get('title');
};
/**
 * Set control visibility
 * @param {bool} b
 * @api stable
 */
ol.control.CanvasTitle.prototype.setVisible = function (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
 */
ol.control.CanvasTitle.prototype.getVisible = function () {
  return this.element.style.display !== 'none';
};
/** Draw title in the final canvas
 * @private
*/
ol.control.CanvasTitle.prototype._draw = function(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();
  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 = function(options) {
  if (!options) options = {};
  var elt = ol.ext.element.create('DIV', {
    className: (options.className || '') + ' ol-center-position ol-unselectable',
    style: {
      display: 'block',
      visibility: 'hidden'
    }
  });
  ol.control.CanvasBase.call(this, {
    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; 
};
ol.ext.inherits(ol.control.CenterPosition, ol.control.CanvasBase);
/**
 * Change the control style
 * @param {ol.style.Style} style
 */
ol.control.CenterPosition.prototype.setStyle = function (style) {
  ol.control.CanvasBase.prototype.setStyle.call(this, 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.
 */
ol.control.CenterPosition.prototype.setCanvas = function (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
 */
ol.control.CenterPosition.prototype.setVisible = function (b) {
  this.element.style.display = (b ? '' : 'none');
  if (this.getMap()) {
    try { this.getMap().renderSync(); } catch(e) { /* ok */ }
  }
};
/**
 * Get control visibility
 * @return {bool} 
 * @api stable
 */
ol.control.CenterPosition.prototype.getVisible = function () {
  return this.element.style.display !== 'none';
};
/** Draw position in the final canvas
 * @private
*/
ol.control.CenterPosition.prototype._draw = function(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();
  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 = function(options) {
  if (!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}) });
  }
  ol.control.CanvasBase.call(this, { 
    element: elt,
    style: style
  });
  this.set('rotateVithView', options.rotateWithView!==false);
  this.setVisible(options.visible!==false);
  this.setImage(options.image || options.src);
};
ol.ext.inherits(ol.control.Compass, ol.control.CanvasBase);
/** Set compass image
 * @param {Image|string} [img=default] the image or an url or 'compact' or 'default'
 */
ol.control.Compass.prototype.setImage = function (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
 */
ol.control.Compass.prototype.compactCompass_ = function (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
 */
ol.control.Compass.prototype.defaultCompass_ = function (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}
 */
ol.control.Compass.prototype.getVisible = function() {
  return ol.ext.element.getStyle(this.element, 'display') === 'block';
};
/** Set visibility
 * @param {boolean} b
 */
ol.control.Compass.prototype.setVisible = function(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
*/
ol.control.Compass.prototype._draw = function(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 = function(options) {
  options = options || {};
  if (options.fullscreen) options.target = document.body;
  // Constructor
  var element = ol.ext.element.create('DIV', {
    className: ((options.className || '') + (options.zoom ? ' ol-zoom':'') + ' ol-ext-dialog').trim(),
    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
  });
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  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)
};
ol.ext.inherits(ol.control.Dialog, ol.control.Control);
/** 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
 */
ol.control.Dialog.prototype.show = function(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
 */
ol.control.Dialog.prototype.open = function() {
  this.show();
};
/** Set the dialog content
 * @param {Element | String} content dialog content
 */
 ol.control.Dialog.prototype.setContentMessage = function(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
 */
ol.control.Dialog.prototype.setTitle = function(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
 */
ol.control.Dialog.prototype.setContent = function(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}
 */
ol.control.Dialog.prototype.getContentElement = function() {
  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
 */
ol.control.Dialog.prototype.setProgress = function(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
 */
 ol.control.Dialog.prototype._onButton = function(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
 */
ol.control.Dialog.prototype.getInputs = function() {
  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 
 */
ol.control.Dialog.prototype.hide = function() {
  this.element.classList.remove('ol-visible');
  this.dispatchEvent ({ type: 'hide' });
};
/** Close the dialog 
 * @method Dialog.close
 * @return {bool} true if a dialog is closed
 */
ol.control.Dialog.prototype.close = ol.control.Dialog.prototype.hide;
/** The dialog is shown
 * @return {bool} true if a dialog is open
 */
ol.control.Dialog.prototype.isOpen = function() {
  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 = function(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];
	});
	ol.control.Control.call(this,
	{	element: element
	});
}
ol.ext.inherits(ol.control.Disable, ol.control.Control);
/** Test if the control is on
 * @return {bool}
 * @api stable
 */
ol.control.Disable.prototype.isOn = function()
{	return this.element.classList.contains("ol-disable");
}
/** Disable all action on the map
 * @param {bool} b, default false
 * @api stable
 */
ol.control.Disable.prototype.disableMap = function(b)
{	if (b) 
	{	this.element.classList.add("ol-enable").show();
	}
	else 
	{	this.element.classList.remove("ol-enable").hide();
	}
}

/** 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 = function(options) {
  options = options || {};
  options.interactions = options.interactions || {};
  // New bar
	ol.control.Bar.call(this, {
    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);
};
ol.ext.inherits(ol.control.EditBar, ol.control.Bar);
/**
 * Set the map instance the control is associated with
 * and add its controls associated to this map.
 * @param {_ol_Map_} map The map instance.
 */
ol.control.EditBar.prototype.setMap = function (map) {
  if (this.getMap()) {
    if (this._interactions.Delete) this.getMap().removeInteraction(this._interactions.Delete);
    if (this._interactions.ModifySelect) this.getMap().removeInteraction(this._interactions.ModifySelect);
  }
  ol.control.Bar.prototype.setMap.call(this, 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 
 */
ol.control.EditBar.prototype.getInteraction = function (name) {
  return this._interactions[name];
};
/** Get the option title */
ol.control.EditBar.prototype._getTitle = function (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
 */
ol.control.EditBar.prototype._setSelectInteraction = function (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() {
      sel.getFeatures().clear();
    });
  }
};
/** Add editing tools
 * @private
 */ 
ol.control.EditBar.prototype._setEditInteraction = function (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
 */
ol.control.EditBar.prototype._setDrawPolygon = function (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
 */ 
ol.control.EditBar.prototype._setModifyInteraction = function (options) {
  // Modify on selected features
  if (options.interactions.ModifySelect !== false && options.interactions.Select !== false) {
    if (options.interactions.ModifySelect instanceof ol.interaction.ModifyFeature) {
      this._interactions.ModifySelect = options.interactions.ModifySelect;
    } else {
      this._interactions.ModifySelect = new ol.interaction.ModifyFeature({
        features: this.getInteraction('Select').getFeatures()
      });
    }
    if (this.getMap()) this.getMap().addInteraction(this._interactions.ModifySelect);
    // Activate with select
    this._interactions.ModifySelect.setActive(this._interactions.Select.getActive());
    this._interactions.Select.on('change:active', function() {
      this._interactions.ModifySelect.setActive(this._interactions.Select.getActive());
    }.bind(this));
  }
  if (options.interactions.Transform !== false) {
    if (options.interactions.Transform instanceof ol.interaction.Transform) {
      this._interactions.Transform = options.interactions.Transform;
    } else {
      this._interactions.Transform = new ol.interaction.Transform ({
        addCondition: ol.events.condition.shiftKeyOnly
      });
    }
    var transform = new ol.control.Toggle ({
      html: '<i></i>',
      className: 'ol-transform',
      title: this._getTitle(options.interactions.Transform) || 'Transform',
      name: 'Transform',
      interaction: this._interactions.Transform
    });
    this.addControl (transform);
  }
  if (options.interactions.Split !== false) {
    if (options.interactions.Split instanceof ol.interaction.Split) {
      this._interactions.Split = options.interactions.Split;
    } else {
      this._interactions.Split = new ol.interaction.Split ({
          sources: this._source
      });
    }
    var split = new ol.control.Toggle ({
      className: 'ol-split',
      title: this._getTitle(options.interactions.Split) || 'Split',
      name: 'Split', 
      interaction: this._interactions.Split
    });
    this.addControl (split);
  }
  if (options.interactions.Offset !== false) {
    if (options.interactions.Offset instanceof ol.interaction.Offset) {
      this._interactions.Offset = options.interactions.Offset;
    } else {
      this._interactions.Offset = new ol.interaction.Offset ({
          source: this._source
      });
    }
    var offset = new ol.control.Toggle ({
      html: '<i></i>',
      className: 'ol-offset',
      title: this._getTitle(options.interactions.Offset) || 'Offset',
      name: 'Offset',
      interaction: this._interactions.Offset
    });
    this.addControl (offset);
  }
};

/*	Copyright (c) 2017 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** A simple gauge control to display level information on the map.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {Object=} options Control options.
 *  @param {String} options.className class of the control
 *  @param {String} options.title title of the control
 *  @param {number} options.max maximum value, default 100;
 *  @param {number} options.val the value, default 0
 */
ol.control.Gauge = function(options) {
  options = options || {};
  var element = ol.ext.element.create('DIV', {
    className: ((options.className||"") + ' ol-gauge ol-unselectable ol-control').trim()
  });
  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
  });
  /*
  var element = document.createElement("div");
  element.className = ((options.className||"") + ' ol-gauge ol-unselectable ol-control').trim();
  this.title_ = document.createElement("span");
  element.appendChild(this.title_);
  this.gauge_ = document.createElement("button");
  this.gauge_.setAttribute('type','button');
  element.appendChild(document.createElement("div").appendChild(this.gauge_))
  this.gauge_.style.width = '0px';
  */
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.setTitle(options.title);
  this.set("max", options.max||100);
  this.val(options.val);
};
ol.ext.inherits(ol.control.Gauge, ol.control.Control);
/** Set the control title
* @param {string} title
*/
ol.control.Gauge.prototype.setTitle = function(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
*/
ol.control.Gauge.prototype.val = function(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 = function(options) {
  options = options || {};
  var self = this;
  var element = document.createElement('div');
  if (options.target) {
    element.className = options.className || "ol-bookmark";
  } else {
    element.className = (options.className || "ol-bookmark") +
          " ol-unselectable ol-control ol-collapsed";
    element.addEventListener("mouseleave", function() {
      if (input !== document.activeElement) {
        menu.style.display = 'none';
      }
    });
    // Show bookmarks on click
    this.button = ol.ext.element.create('BUTTON', {
      type: 'button',
      title: options.title || 'Geobookmarks',
      click: function() {
        var show = (menu.style.display === '' || menu.style.display === 'none');
        menu.style.display = (show ? 'block': 'none');
        if (show) this.setBookmarks();
      }.bind(this)
    });
    element.appendChild(this.button);
  }
  // The menu
  var menu = document.createElement('div');
  element.appendChild(menu);
  var ul = document.createElement('ul');
  menu.appendChild(ul);
  var input = document.createElement('input');
  input.setAttribute("placeholder", options.placeholder || "Add a new geomark...")
  input.addEventListener("keydown", function(e) {
    e.stopPropagation();
    if (e.keyCode === 13) {
      e.preventDefault();
      var title = this.value;
      if (title) {
        self.addBookmark(title);
        this.value = '';
        self.dispatchEvent({
          type: "add",
          name: title
        });
      }
      menu.style.display = 'none';
    }
  });
  input.addEventListener("blur", function() {
    menu.style.display = 'none';
  });
  menu.appendChild(input);
  // Init
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  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);
};
ol.ext.inherits(ol.control.GeoBookmark, ol.control.Control);
/** 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}
});
 */
ol.control.GeoBookmark.prototype.setBookmarks = function(bmark) {
  if (!bmark) {
    bmark = {};
    try {
      bmark = JSON.parse(localStorage[this.get('namespace')+"@bookmark"] || "{}");
    } catch(e) { console.warn('Failed to access localStorage...'); }
  }
  var modify = this.get("editable");
  var ul = this.element.querySelector("ul");
  var menu = this.element.querySelector("div");
  var self = this;
  ul.innerHTML = '';
  for (var b in bmark) {
    var li = document.createElement('li');
    li.textContent = b;
    li.setAttribute('data-bookmark', JSON.stringify(bmark[b]));
    li.setAttribute('data-name', b);
    li.addEventListener('click', function() {
      var bm = JSON.parse(this.getAttribute("data-bookmark"));
      self.getMap().getView().setCenter(bm.pos);
      self.getMap().getView().setZoom(bm.zoom);
      self.getMap().getView().setRotation(bm.rot || 0);
      menu.style.display = 'none';
      self.dispatchEvent({ type: 'select', name: this.getAttribute("data-name"), bookmark: bm });
    });
    ul.appendChild(li);
    if (modify && !bmark[b].permanent) {
      var button = document.createElement('button');
      button.setAttribute('data-name', b);
      button.setAttribute('type', 'button');
      button.setAttribute('title', this.get('deleteTitle') ||'Suppr.');
      button.addEventListener('click', function(e) {
        self.removeBookmark(this.getAttribute("data-name"));
        self.dispatchEvent({ type: "remove", name: this.getAttribute("data-name") });
        e.stopPropagation();
      });
      li.appendChild(button);
    }
  }
  try {
    localStorage[this.get('namespace')+"@bookmark"] = JSON.stringify(bmark);
  } catch(e) { console.warn('Failed to access localStorage...'); }
};
/** Get Geo bookmarks
 * @return {any} a list of bookmarks : { BM1:{pos:ol.coordinates, zoom: integer}, BM2:{pos:ol.coordinates, zoom: integer} }
 */
ol.control.GeoBookmark.prototype.getBookmarks = function() {
  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
 */
ol.control.GeoBookmark.prototype.removeBookmark = function(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
 */
ol.control.GeoBookmark.prototype.addBookmark = function(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 = function(options) {
  if (!options) options = {};
  options.className = options.className || 'ol-geobar';
  ol.control.Bar.call(this, 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');
    }
  });
};
ol.ext.inherits(ol.control.GeolocationBar, ol.control.Bar);
/**
 * 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
 */
ol.control.GeolocationBar.prototype.setMap = function (map) {
  if (this._listener) ol.Observable.unByKey(this._listener);
  this._listener = null;
  ol.control.Bar.prototype.setMap.call(this, 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}
 */
ol.control.GeolocationBar.prototype.getInteraction = function () {
  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 = function(options) {
  if (!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);
  ol.control.Toggle.call (this, {
    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));
};
ol.ext.inherits(ol.control.GeolocationButton, ol.control.Toggle);

/*	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 = function(opt_options)
{	var options = opt_options || {};
	var self = this;
	// API
	var element;
	if (options.target)
	{	element = document.createElement("div");
		this.panel_ = options.target;
	}
	else
	{	element = document.createElement("div");
		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_);
	}
	ol.control.Control.call(this,
	{	element: element,
		target: options.target
	});
// 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);
};
ol.ext.inherits(ol.control.Globe, ol.control.Control);
/**
 * Set the map instance the control associated with.
 * @param {ol.Map} map The map instance.
 */
ol.control.Globe.prototype.setMap = function(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
*/
ol.control.Globe.prototype.setView = function()
{	if (this.getMap() && this.get('follow'))
	{	this.setCenter(this.getMap().getView().getCenter());
	}
}
/** Get globe map
*	@return {ol.Map}
*/
ol.control.Globe.prototype.getGlobe = function()
{	return this.ovmap_;
}
/** Show/hide the globe
*/
ol.control.Globe.prototype.show = function(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
*/
ol.control.Globe.prototype.setPosition = function(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
*/
ol.control.Globe.prototype.setCenter = function (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
 * @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.
 *  @param {number} options.step step beetween lines (in proj units), default 1
 *  @param {number} options.stepCoord show a coord every stepCoord, default 1
 *  @param {number} options.spacing spacing beetween lines (in px), default 40px 
 *  @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 = function(options) {
  if (!options) options = {};
  // Initialize parent
  var elt = document.createElement("div");
  elt.className = "ol-graticule ol-unselectable ol-hidden";
  ol.control.CanvasBase.call(this, { 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('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" }),
      }) 
    }));
  }
};
ol.ext.inherits(ol.control.Graticule, ol.control.CanvasBase);
ol.control.Graticule.prototype.setStyle = function (style) {
  this._style = style;
};
ol.control.Graticule.prototype._draw = function (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;
  }
  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+step; y<=ymax; y+=step)
      {	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+step; x<=xmax; x+=step)
      {	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 = function(options) {
  if (!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" }),
    })
  });
  ol.control.CanvasBase.call(this, {
    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');
};
ol.ext.inherits(ol.control.GridReference, ol.control.CanvasBase);
/**
 * Set the map instance the control is associated with.
 * @param {ol.Map} map The map instance.
 */
 ol.control.GridReference.prototype.setMap = function(map) {
  ol.control.CanvasBase.prototype.setMap.call(this, 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
 */
ol.control.GridReference.prototype.getFeatureName = function (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
 */
ol.control.GridReference.prototype.sortFeatures = function (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
 */
ol.control.GridReference.prototype.indexTitle = function (f) {
  return this.getFeatureName(f).charAt(0);
};
/** Display features in the index
 * @param { Array<ol.Feature> | ol.Collection<ol.Feature> } features
 */
ol.control.GridReference.prototype.setIndex = function (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
*/
ol.control.GridReference.prototype.getReference = function (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
 */
 ol.control.GridReference.prototype.getVIndex = function (index) {
  return index;
};
/** Get horizontal index (A,B,C...)
 * @param {number} index
 * @returns {string}
 * @api
 */
 ol.control.GridReference.prototype.getHIndex = function (index) {
  return String.fromCharCode(65 + index);
};
/** Draw the grid
* @param {ol.event} e postcompose event
* @private
*/
ol.control.GridReference.prototype._draw = function (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 = function(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' : '')
  });
  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);
  }
  // Initialize
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  // 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();
};
ol.ext.inherits(ol.control.Imageline, ol.control.Control);
/**
 * Remove the control from its current map and attach it to the new map.
 * @param {ol.Map} map Map.
 * @api stable
 */
ol.control.Imageline.prototype.setMap = function (map) {
	if (this._listener) {
    this._listener.forEach(function(l) {
      ol.Observable.unByKey(l);
    }.bind(this));
  }
	this._listener = null;
	ol.control.Control.prototype.setMap.call(this, 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
 */
ol.control.Imageline.prototype.setLayers = function (layers) {
  this._sources = this._getSources(layers);
};
/** Get source from a set of layers
 * @param {Array<ol.Layer} layers
 * @returns {Array<ol.source.Vector}
 * @private
 */
ol.control.Imageline.prototype._getSources = function (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
 */
ol.control.Imageline.prototype.useExtent = function(b) {
  this.set('useExtent', b);
  this.refresh();
};
/** Is the line collapsed
 * @return {boolean}
 */
ol.control.Imageline.prototype.isCollapsed = function() {
  return this.element.classList.contains('ol-collapsed');
};
/** Collapse the line
 * @param {boolean} b
 */
ol.control.Imageline.prototype.collapse = function(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
 */
ol.control.Imageline.prototype.toggle = function() {
  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
 */
ol.control.Imageline.prototype._getImage = function(f) {
  return f.get('img');
};
/** Default function to get an image title
 * @param {ol.Feature} f
 * @private
 */
ol.control.Imageline.prototype._getTitle = function(/* f */) {
  return '';
};
/**
 * Get features
 * @return {Array<ol.Feature>}
 */
ol.control.Imageline.prototype.getFeatures = function() {
  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
 */
ol.control.Imageline.prototype._setScrolling = function() {
  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
 */
ol.control.Imageline.prototype._updateScrollBounds = function() {
  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
 */
ol.control.Imageline.prototype.refresh = function() {
  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.dispatchEvent({type: 'select', feature: f });
          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;
            this._select.elt.classList.add('select');
          }
      }.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
 */
ol.control.Imageline.prototype.select = function(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
 */
ol.control.Imageline.prototype._drawLink = function(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 = function(options) {
  var self = this;
	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
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.set('iter', 1);
  var content = ol.ext.element.create('DIV', { className: 'content', parent: element } )
  // Search control
  this._addSearchCtrl(content, options);
  // Method buttons
  ol.ext.element.create('BUTTON', { className: 'ol-button ol-method-time selected', title:'isochrone', parent: content })
    .addEventListener('click', function(){
      this.setMethod('time');
    }.bind(this));
  ol.ext.element.create('I', { className: 'ol-button ol-method-distance', title:'isodistance', parent: content })
    .addEventListener('click', function(){
      this.setMethod('distance');
    }.bind(this));
  // Mode buttons
  ol.ext.element.create('I', { className: 'ol-button ol-car selected', title:'by car', parent: content })
    .addEventListener('click', function(){
      this.setMode('car');
    }.bind(this));
  ol.ext.element.create('I', { className: 'ol-button ol-pedestrian', title:'by foot', parent: content })
    .addEventListener('click', function(){
      this.setMode('pedestrian');
    }.bind(this));
  // Direction buttons
  ol.ext.element.create('I', { className: 'ol-button ol-direction-direct selected', title:'direct', parent: content })
    .addEventListener('click', function(){
      this.setDirection('direct');
    }.bind(this));
  ol.ext.element.create('I', { className: 'ol-button ol-direction-reverse', title:'reverse', parent: content })
    .addEventListener('click', function(){
      this.setDirection('reverse');
    }.bind(this));
  // Input 
  var div = ol.ext.element.create('DIV', { className: 'ol-time', parent: content })
  ol.ext.element.create('DIV', { html:'isochrone:', parent: div });
  ol.ext.element.create('INPUT', { type: 'number', parent: div, min: 0 })
    .addEventListener('change', function(){
      self.set('hour', Number(this.value));
    });
  ol.ext.element.create('TEXT', { parent: div, html: 'h' });
  ol.ext.element.create('INPUT', { type: 'number', parent: div, min: 0 })
    .addEventListener('change', function(){
      self.set('minute', Number(this.value));
    });
  ol.ext.element.create('TEXT', { parent: div, html: 'mn' });
  div = ol.ext.element.create('DIV', { className: 'ol-distance', parent: content });
  ol.ext.element.create('DIV', { html:'isodistance:', parent: div });
  ol.ext.element.create('INPUT', { type: 'number', step: 'any', parent: div, min: 0 })
    .addEventListener('change', function(){
      self.set('distance', Number(this.value));
    });
  ol.ext.element.create('TEXT', { parent: div, html: 'km' });
  div = ol.ext.element.create('DIV', { className: 'ol-iter', parent: content })
  ol.ext.element.create('DIV', { html:'Iteration:', parent: div });
  ol.ext.element.create('INPUT', { type: 'number', parent: div, value: 1, min: 1 })
    .addEventListener('change', function(){
      self.set('iter', Number(this.value));
    });
  // OK button
  ol.ext.element.create('I', { className:'ol-ok', html:'ok', parent: content })
    .addEventListener('click', function() {
      var val = 0;
      switch (this.get('method')) {
        case 'distance':  {
          val = this.get('distance')*1000;
          break;
        }
        default: {
          val = (this.get('hour')||0)*3600 + (this.get('minute')||0)*60;
          break;
        }
      }
      if (val && this.get('coordinate')) {
        this.search(this.get('coordinate'), val);
      }
    }.bind(this));
  this.set('url', 'https://wxs.ign.fr/'+(options.apiKey || 'essentiels')+'/isochrone/isochrone.json');
  this._ajax = new ol.ext.Ajax({ 
    dataType: 'JSON',
    auth: options.auth
  });
  this._ajax.on('success', this._success.bind(this));
  this._ajax.on('error', this._error.bind(this));
  // searching
  this._ajax.on('loadstart', function() {
    this.element.classList.add('ol-searching');
  }.bind(this));
  this._ajax.on('loadend', function() {
    this.element.classList.remove('ol-searching');
  }.bind(this));
  this.setMethod(options.method);
};
ol.ext.inherits(ol.control.IsochroneGeoportail, ol.control.Control);
/**
 * Set the map instance the control is associated with
 * and add its controls associated to this map.
 * @param {_ol_Map_} map The map instance.
 */
ol.control.IsochroneGeoportail.prototype.setMap = function (map) {
	ol.control.Control.prototype.setMap.call(this, map);
	this._search.setMap(map);
};
/** Add a new search input
 * @private
 */
ol.control.IsochroneGeoportail.prototype._addSearchCtrl = function (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)
 */
ol.control.IsochroneGeoportail.prototype.setMethod = function(method) {7
  method = (/distance/.test(method) ? 'distance' : 'time');
  this.element.querySelector(".ol-method-time").classList.remove("selected");
  this.element.querySelector(".ol-method-distance").classList.remove("selected");
  this.element.querySelector(".ol-method-"+method).classList.add("selected");
  this.element.querySelector("div.ol-time").classList.remove("selected");
  this.element.querySelector("div.ol-distance").classList.remove("selected");
  this.element.querySelector("div.ol-"+method).classList.add("selected");
  this.set('method', method);
};
/** Set mode
 * @param {string} mode The mode: 'car' or 'pedestrian', default 'car'
 */
ol.control.IsochroneGeoportail.prototype.setMode = function (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
 */
ol.control.IsochroneGeoportail.prototype.setDirection = function (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)
 */
ol.control.IsochroneGeoportail.prototype.search = function(coord, option, iter) {
  var proj = this.getMap() ? this.getMap().getView().getProjection() : 'EPSG:3857';
  var method = /distance/.test(this.get('method')) ? 'distance' : 'time';
  if (typeof(option)==='string') {
    var unit = option.replace(/([0-9|.]*)([a-z]*)$/,'$2');
    method = 'time';
    option = parseFloat(option);
    // convert unit
    switch (unit) {
      case 'mn': {
        option = option*60;
        break;
      }
      case 'h': {
        option = option*3600;
        break;
      }
      case 'm': {
        method = 'distance';
        break;
      }
      case 'km': {
        method = 'distance';
        option = option*1000;
        break;
      }
    }
  }
  var dt = Math.round(option * (this.get('iter')-(iter||0)) / this.get('iter'));
  if (typeof option === 'number') {
    // Send data
    var data = {
      'gp-access-lib': '2.1.0',
      location: ol.proj.toLonLat(coord, proj),
      graphName: (this.get('mode')==='pedestrian' ?  'Pieton' : 'Voiture'),
      exclusions: this.get('exclusions') || undefined,
      method: method,
      time: method==='time' ? dt : undefined,
      distance: method==='distance' ? dt : undefined,
      reverse: (this.get('direction') === 'reverse'),
      smoothing: this.get('smoothing') || true,
      holes: this.get('holes') || false
    };
    this._ajax.send(this.get('url'), data, { 
      coord: coord, 
      option: option,
      data: data,
      iteration: (iter||0)+1 
    });
  }
};
/** Trigger result
 * @private
 */
ol.control.IsochroneGeoportail.prototype._success = function(e) {
  var proj = this.getMap() ? this.getMap().getView().getProjection() : 'EPSG:3857';
  // Convert to features
  var format = new ol.format.WKT();
  var evt = e.response;
	evt.feature = format.readFeature(evt.wktGeometry, {
    dataProjection: 'EPSG:4326',
    featureProjection: proj
  });
  evt.feature.set('iteration', e.options.iteration);
  evt.feature.set('method', e.options.data.method);
  evt.feature.set(e.options.data.method, e.options.data[e.options.data.method]);
  delete evt.wktGeometry;
  evt.type = 'isochrone';
  evt.iteration = e.options.iteration-1;
  this.dispatchEvent (evt);
  if (e.options.iteration < this.get('iter')) {
    this.search(e.options.coord, e.options.option, e.options.iteration);
  }
};
/** Trigger error
 * @private
 */
ol.control.IsochroneGeoportail.prototype._error = function() {
  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 = function(options) {
  options = options || {};
	options.switcherClass="ol-layerswitcher-popup";
	if (options.mouseover!==false) options.mouseover=true;
	ol.control.LayerSwitcher.call(this, options);
};
ol.ext.inherits(ol.control.LayerPopup, ol.control.LayerSwitcher);
/** Disable overflow
*/
ol.control.LayerPopup.prototype.overflow = function(){};
/** Render a list of layer
 * @param {elt} element to render
 * @layers {Array{ol.layer}} list of layer to show
 * @api stable
 */
ol.control.LayerPopup.prototype.drawList = function(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 = function(options) {
  options = options || {};
  options.selection = true;
  options.noScroll = true;
  ol.control.LayerSwitcher.call (this, 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 = [];
};
ol.ext.inherits(ol.control.LayerShop, ol.control.LayerSwitcher);
/** Set the map instance the control is associated with.
 * @param {_ol_Map_} map The map instance.
 */
ol.control.LayerShop.prototype.setMap = function(map) {
  if (this.getMap()) {
    // Remove map controls
    this._controls.forEach(function(c) {
      this.getMap().removeControl(c)
    }.bind(this));
  }
  ol.control.LayerSwitcher.prototype.setMap.call(this, 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}
 */
ol.control.LayerShop.prototype.getBarElement = function(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
 */
ol.control.LayerShop.prototype.addControl = function(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 = function(options) {
  options = options || {};
	options.switcherClass = "ol-layerswitcher-image";
	if (options.mouseover!==false) options.mouseover=true;
	ol.control.LayerSwitcher.call(this, options);
};
ol.ext.inherits(ol.control.LayerSwitcherImage, ol.control.LayerSwitcher);
/** Render a list of layer
 * @param {elt} element to render
 * @layers {Array{ol.layer}} list of layer to show
 * @api stable
 */
ol.control.LayerSwitcherImage.prototype.drawList = function(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
*/
ol.control.LayerSwitcherImage.prototype.overflow = function(){};

// 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 {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 = function(options) {
  options = options || {};
  var element = document.createElement('div');
  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);
  }
  ol.control.CanvasBase.call(this, {
    element: element,
		target: options.target
	});
  // The legend
  this._legend = options.legend;
  this._legend.getCanvas().className = 'ol-legendImg';
  element.appendChild(this._legend.getCanvas());
  element.appendChild(this._legend.getListElement());
  if (options.collapsible!==false && options.collapsed===false) this.show();
  this._legend.on('select', function(e) {
    this.dispatchEvent(e);
  }.bind(this));
  this._legend.on('refresh', function() {
    if (this._onCanvas && this.getMap()) {
      try { this.getMap().renderSync(); } catch(e) { /* ok */ }
    }
  }.bind(this));
};
ol.ext.inherits(ol.control.Legend, ol.control.CanvasBase);
/** Get the legend associated with the control
 * @returns {ol.legend.Legend}
 */
ol.control.Legend.prototype.getLegend = function () {
  return this._legend;
};
/** Draw control on canvas
 * @param {boolean} b draw on canvas.
 */
ol.control.Legend.prototype.setCanvas = function (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}
 */
ol.control.Legend.prototype.onCanvas = function () {
  return !!this._onCanvas;
};
/** Draw legend on canvas
 * @private
 */
ol.control.Legend.prototype._draw = function (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
 */
ol.control.Legend.prototype.show = function() {
  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
 */
ol.control.Legend.prototype.hide = function() {
  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}
 */
ol.control.Legend.prototype.collapse = function(b) {
  if (b===false) this.show();
  else this.hide();
};
/** Is control collapsed
 * @returns {boolean}
 */
ol.control.Legend.prototype.isCollapsed = function() {
  return (this.element.classList.contains('ol-collapsed'));
};
/** Toggle control
 */
ol.control.Legend.prototype.toggle = function() {
  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 = function(options) {
  if (!options) options={};
  var element = document.createElement("div");
  if (options.target) {
    element = ol.ext.element.create('DIV', {
      className: options.className || "ol-mapzone"
    });
  } else {
    element = ol.ext.element.create('DIV', {
      className: (options.className || "ol-mapzone") +' ol-unselectable ol-control ol-collapsed'
    });
    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
    });
  }
  // Parent control
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  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();
    });
  });
};
ol.ext.inherits(ol.control.MapZone, ol.control.Control);
/** Collapse the control
 * @param {boolean} b
 */
ol.control.MapZone.prototype.setCollapsed = function (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');
  }
};
/** Get control collapsed
 * @return {boolean} 
 */
ol.control.MapZone.prototype.getCollapsed = function () {
  return this.element.classList.contains('ol-collapsed');
};
/** Set the control visibility (collapsed)
 * @param {boolean} b
 * @deprecated use setCollapsed instead
 */
ol.control.MapZone.prototype.setVisible = ol.control.MapZone.prototype.setCollapsed;
/** Get associated maps
 * @return {ol.Map}
 */
ol.control.MapZone.prototype.getMaps = function () {
  return this._maps;
};
/** Get nb zone */
ol.control.MapZone.prototype.getLength = function () {
  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
 */
ol.control.MapZone.prototype.addZone = function (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
 */
ol.control.MapZone.prototype.removeZone = function (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 = function(options) {
  options = options || {};
	var element = document.createElement('DIV');
  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);
	ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.set('closeBox', options.closeBox);
  this.set('hideOnClick', options.hideOnClick);
};
ol.ext.inherits(ol.control.Notification, ol.control.Control);
/**
 * 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
 */
ol.control.Notification.prototype.show = function(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
 */
ol.control.Notification.prototype.hide = function() {
  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
 */
ol.control.Notification.prototype.toggle = function(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 = function(options) {
  if (!options) options={};
/*
  var element = document.createElement("div");
  element.classList.add('ol-unselectable', 'ol-overlay');
  //if (options.className) element.classList.add(options.className);
*/
  var element = ol.ext.element.create('DIV', {
    className: 'ol-unselectable ol-overlay '+(options.className||''),
    html: options.content
  });
  ol.control.Control.call(this, {
    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);
};
ol.ext.inherits(ol.control.Overlay, ol.control.Control);
/** Set the content of the overlay
* @param {string|Element} html the html to display in the control
*/
ol.control.Overlay.prototype.setContent = function (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
*/
ol.control.Overlay.prototype.show = function (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
 */
ol.control.Overlay.prototype.showImage = function (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
*/
ol.control.Overlay.prototype.hide = function () {
  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
*/
ol.control.Overlay.prototype.toggle = function () {	
  if (this.getVisible()) this.hide();
  else this.show();
}
/** Get the control visibility
* @return {boolean} b
*/
ol.control.Overlay.prototype.getVisible = function () {
  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
*/
ol.control.Overlay.prototype.setClass = function (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 = function(options)
{	options = options || {};
  var self = this;
  // API
  this.minZoom = options.minZoom || 0;
  this.maxZoom = options.maxZoom || 18;
  this.rotation = options.rotation;
  var element;
  if (options.target) {
    element = document.createElement("div");
    this.panel_ = options.target;
  } else {
    element = document.createElement("div");
    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_);
  }
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  // 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;
    }
  }));
};
ol.ext.inherits(ol.control.Overview, ol.control.Control);
/** Get overview map
*	@return {ol.Map}
*/
ol.control.Overview.prototype.getOverviewMap = function(){
  return this.ovmap_;
};
/** Toggle overview map
*/
ol.control.Overview.prototype.toggleMap = function(){
  this.element.classList.toggle("ol-collapsed");
  this.ovmap_.updateSize();
  this.setView();
};
/** Set overview map position
*	@param {top|bottom-left|right}
*/
ol.control.Overview.prototype.setPosition = function(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.
 */
ol.control.Overview.prototype.setMap = function(map) {
  if (this._listener) {
    for (var i in this._listener) {
      ol.Observable.unByKey(this._listener[i]);
    }
  }
  this._listener = {};
  ol.control.Control.prototype.setMap.call(this, 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
*/
ol.control.Overview.prototype.calcExtent_ = function(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
*/
ol.control.Overview.prototype.setView = function(e){
  if (!e) {
    // refresh all
    this.setView({key:'rotation'});
    this.setView({key:'resolution'});
    this.setView({key:'center'});
    return;
  }
  // Set the view params
  switch (e.key){
    case 'rotation': {
      if (this.rotation) this.oview_.setRotation(this.getMap().getView().getRotation());
      else if (this.oview_.getRotation()) this.oview_.setRotation(0);
      break;
    }
    case 'center': {
      var mapExtent = this.getMap().getView().calculateExtent(this.getMap().getSize());
      var extent = this.oview_.calculateExtent(this.ovmap_.getSize());
      if (mapExtent[0]<extent[0] || mapExtent[1]<extent[1] 
      || mapExtent[2]>extent[2] || mapExtent[3]>extent[3]){
        this.oview_.setCenter(this.getMap().getView().getCenter()); 
      }
      break;
    }	
    case 'resolution': {
      //var z = Math.round(this.getMap().getView().getZoom()/2)*2-4;
      var z = Math.round(this.oview_.getZoomForResolution(this.getMap().getView().getResolution())/2)*2-4;
      z = Math.min ( this.maxZoom, Math.max(this.minZoom, z) );
      this.oview_.setZoom(z);
      break;
    }
    default: break;
  }
  this.calcExtent_();
};

/*	Copyright (c) 2015-2018 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/**
 * Set an hyperlink that will return the user to the current map view.
 * Just add a `permalink`property to layers to be handled by the control (and added in the url). 
 * The layer's permalink property is used to name the layer in the url.
 * The control must be added after all layer are inserted in the map to take them into acount.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {Object=} options
 *  @param {boolean} options.urlReplace replace url or not, default true
 *  @param {boolean|string} [options.localStorage=false] save current map view in localStorage, if 'position' only store map position
 *  @param {boolean} options.geohash use geohash instead of lonlat, default false
 *  @param {integer} options.fixed number of digit in coords, default 6
 *  @param {boolean} options.anchor use "#" instead of "?" in href
 *  @param {boolean} options.visible hide the button on the map, default true
 *  @param {boolean} options.hidden hide the button on the map, default false DEPRECATED: use visible instead
 *  @param {function} options.onclick a function called when control is clicked
*/
ol.control.Permalink = function(opt_options) {
  var options = opt_options || {};
  var self = this;
  var button = document.createElement('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);
  var element = document.createElement('div');
  element.className = (options.className || "ol-permalink") + " ol-unselectable ol-control";
  element.appendChild(button);
  if (options.hidden || options.visible===false) ol.ext.element.hide(element);
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.set('geohash', options.geohash);
  this.set('initial', false);
  this.on ('change', this.viewChange_.bind(this));
  // Save search params
  this.search_ = {};
  var init = {};
  var hash = document.location.hash || document.location.search || '';
//  console.log('hash', hash)
  if (this.replaceState_ && !hash && this._localStorage) {
    try {
      hash = localStorage['ol@permalink'];
    } catch(e) { console.warn('Failed to access localStorage...'); }
  }
  if (hash) {
    hash = hash.replace(/(^#|^\?)/,"").split("&");
    for (var i=0; i<hash.length;  i++) {
      var t = hash[i].split("=");
      switch(t[0]) {
        case 'lon':
        case 'lat':
        case 'z': 
        case 'r': {
          init[t[0]] = t[1];
          break
        }
        case 'gh': {
          var ghash = t[1].split('-');
          var lonlat = ol.geohash.toLonLat(ghash[0]);
          init.lon = lonlat[0];
          init.lat = lonlat[1];
          init.z = ghash[1];
          break;
        }
        case 'l': break;
        default: this.search_[t[0]] = t[1];
      }
    }
  }
  if (init.hasOwnProperty('lon')) {
    this.set('initial', init);
  }
  // Decode permalink
  if (this.replaceState_) this.setPosition();
};
ol.ext.inherits(ol.control.Permalink, ol.control.Control);
/**
 * Get the initial position passed by the url
 */
ol.control.Permalink.prototype.getInitialPosition = function() {
  return this.get('initial');
};
/**
 * Set the map instance the control associated with.
 * @param {ol.Map} map The map instance.
 */
ol.control.Permalink.prototype.setMap = function(map) {
  if (this._listener) {
    ol.Observable.unByKey(this._listener.change);
    ol.Observable.unByKey(this._listener.moveend);
  }
  this._listener = null;
  ol.control.Control.prototype.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}
*/
ol.control.Permalink.prototype.getLayerByLink =  function (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}
 */
ol.control.Permalink.prototype.setGeohash = function(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
 */
ol.control.Permalink.prototype.setPosition = function(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
 */
ol.control.Permalink.prototype.getUrlParams = function() {
  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
 */
ol.control.Permalink.prototype.setUrlParam = function(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
 */
ol.control.Permalink.prototype.getUrlParam = function(key) {
  return decodeURIComponent (this.search_[encodeURIComponent(key)] || '');
};
/**
 * Has a parameter url.
 * @param {string} key the key parameter
 * @return {boolean} 
 * @api stable
 */
ol.control.Permalink.prototype.hasUrlParam = function(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}
 */
ol.control.Permalink.prototype.getLink = function(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}
 */
ol.control.Permalink.prototype.getUrlReplace = function() {
  return this.replaceState_;
};
/** Enable / disable url replacement (replaceSate)
 *	@param {bool}
 */
ol.control.Permalink.prototype.setUrlReplace = function(replace) {
  try {
    this.replaceState_ = replace;
    if (!replace) {
      var s = "";
      for (var i in this.search_) {
        s += (s==""?"?":"&") + i + (typeof(this.search_[i]) !== 'undefined' ? "="+this.search_[i] : '');
      }
      window.history.replaceState (null,null, document.location.origin+document.location.pathname+s);
    }
    else window.history.replaceState (null,null, this.getLink());
  } catch(e) {/* ok */}
  /*
  if (this._localStorage) {
    localStorage['ol@permalink'] = this.getLink(true);
  }
  */
};
/**
 * On view change refresh link
 * @param {ol.event} The map instance.
 * @private
 */
ol.control.Permalink.prototype.viewChange_ = function() {
  try {
    if (this.replaceState_) window.history.replaceState (null,null, this.getLink());
  } catch(e) {/* ok */}
  if (this._localStorage) {
    try {
      localStorage['ol@permalink'] = this.getLink(this._localStorage);
    } catch(e) { console.warn('Failed to access localStorage...'); }
  }
};
/**
 * Layer change refresh link
 * @private
 */
ol.control.Permalink.prototype.layerChange_ = function() {
  // 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 = function(options) {
  if (!options) options = {};
  var element = ol.ext.element.create('DIV', {
    className: (options.className || 'ol-print')
  });
  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
    });
  }
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.set('immediate', options.immediate);
  this.set('imageType', options.imageType || 'image/jpeg');
  this.set('quality', options.quality || .8);
  this.set('orientation', options.orientation);
};
ol.ext.inherits(ol.control.Print, ol.control.Control);
/** Helper function to copy result to clipboard
 * @param {Event} e print event
 * @return {boolean}
 * @private
 */
ol.control.Print.prototype.toClipboard = function(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
 */
ol.control.Print.prototype.copyMap = function(options, callback) {
  this.once('print', function(e) {
    this.toClipboard(e, callback);
  }.bind(this));
  this.print(options);
};
/** Get map canvas
 * @private
 */
ol.control.Print.prototype._getCanvas = function(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 ?
        if (ol.ext.element.getStyle(c.parentNode, 'mix-blend-mode') === 'multiply') {
          ctx.globalCompositeOperation = 'multiply';
        }
        // 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
 */
ol.control.Print.prototype.fastPrint = function(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 {function} cback a callback function that take a string containing the requested data URI.
 * @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
 */
ol.control.Print.prototype.print = function(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 {string} options.imageType A string indicating the image format, default image/jpeg
 *	@param {number} options.quality Number between 0 and 1 indicating the image quality to use for image formats that use lossy compression such as image/jpeg and image/webp
 *	@param {string} options.orientation Page orientation (landscape/portrait), default guest the best one
 *	@param {boolean} options.immediate force print even if render is not complete,  default false
 *	@param {boolean} [options.openWindow=false] open the file in a new window on print
 *	@param {boolean} [options.copy=true] add a copy select option
 *	@param {boolean} [options.print=true] add a print select option
 *	@param {boolean} [options.pdf=true] add a pdf select option
 *	@param {function} [options.saveAs] a function to save the image as blob
 *	@param {*} [options.jsPDF] jsPDF object to save map as pdf
 */
ol.control.PrintDialog = function(options) {
  if (!options) options = {};
  this._lang = options.lang || 'en';
  var element = ol.ext.element.create('DIV', {
    className: (options.className || 'ol-print') + ' ol-unselectable ol-control'
  });
  ol.ext.element.create('BUTTON', {
    type: 'button',
    title: options.title || 'Print',
    click: function() { 
      this.print(); 
    }.bind(this),
    parent: element
  });
  ol.control.Control.call(this, {
    element: 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) {
    content.setAttribute('data-status', e.type);
    if (!e.clipboard) {
      this.dispatchEvent(e);
    }
  }.bind(this));
  // North arrow
  this._compass = new ol.control.Compass({ 
    src: options.northImage || 'compact', 
    visible: false, 
    className: 'olext-print-compass',
    style: new ol.style.Stroke({ color: '#333', width: 0 })
  });
  // Print dialog
  var printDialog = this._printDialog = new ol.control.Dialog({
    target: document.body,
    closeBox: true,
    className: 'ol-ext-print-dialog'
  });
  var content = printDialog.getContentElement();
  this._input = {};
  var param = ol.ext.element.create('DIV',{
    className: 'ol-print-param',
    parent: content
  });
  this._pages = [ ol.ext.element.create('DIV', { 
    className: 'ol-page'
  })];
  var printMap = ol.ext.element.create('DIV', {
    className: 'ol-map',
    parent: this._pages[0]
  });
  ol.ext.element.create('DIV', {
    html: this._pages[0],
    className: 'ol-print-map',
    parent: content
  });
  ol.ext.element.create('H2',{
    html: this.i18n('title'),
    parent: param
  });
  var ul = ol.ext.element.create('UL',{ parent: param });
  // Orientation
  var li = ol.ext.element.create('LI', { 
    /*
    html: ol.ext.element.create('LABEL', {
      html: this.18n('orientation')
    }),
    */
    className: 'ol-orientation',
    parent: ul 
  });
  this._input.orientation = { list: li };
  var label = ol.ext.element.create('LABEL', {
    className: 'portrait',
    parent: li
  });
  this._input.orientation.portrait = ol.ext.element.create('INPUT', {
    type: 'radio',
    name: 'ol-print-orientation',
    value: 'portrait',
    checked: true,
    on: { change: function(e) { 
      this.setOrientation(e.target.value);
    }.bind(this) },
    parent: label
  });
  ol.ext.element.create('SPAN', { 
    html: this.i18n('portrait'),
    parent: label
  });
  label = ol.ext.element.create('LABEL', {
    className: 'landscape',
    parent: li
  });
  this._input.orientation.landscape = ol.ext.element.create('INPUT',{
    type: 'radio',
    name: 'ol-print-orientation',
    value: 'landscape',
    on: { change: function(e) { 
      this.setOrientation(e.target.value);
    }.bind(this) },
    parent: label
  });
  ol.ext.element.create('SPAN', { 
    html: this.i18n('landscape'),
    parent: label 
  });
  // Page size
  var s; 
  li = ol.ext.element.create('LI',{ 
    html: ol.ext.element.create('LABEL', {
      html: this.i18n('size'),
    }),
    className: 'ol-size',
    parent: ul 
  });
  var size = this._input.size = ol.ext.element.create('SELECT', {
    on: { change: function(){
      this.setSize(size.value || originalSize);
    }.bind(this) },
    parent: li
  });
  for (s in this.paperSize) {
    ol.ext.element.create('OPTION', {
      html: s + (this.paperSize[s] ? ' - '+this.paperSize[s][0]+'x'+this.paperSize[s][1]+' mm' : this.i18n('custom')),
      value: s,
      parent: size
    });
  }
  // Margin
  li = ol.ext.element.create('LI',{ 
    html: ol.ext.element.create('LABEL', {
      html: this.i18n('margin'),
    }),
    className: 'ol-margin',
    parent: ul 
  });
  var margin = this._input.margin = ol.ext.element.create('SELECT', {
    on: { change: function(){
      this.setMargin(margin.value);
    }.bind(this) },
    parent: li
  });
  for (s in this.marginSize) {
    ol.ext.element.create('OPTION', {
      html: this.i18n(s) + ' - ' + this.marginSize[s] + ' mm',
      value: this.marginSize[s],
      parent: margin
    });
  }
  // Scale
  li = ol.ext.element.create('LI',{ 
    html: ol.ext.element.create('LABEL', {
      html: this.i18n('scale'),
    }),
    className: 'ol-scale',
    parent: ul 
  });
  var scale = this._input.scale = ol.ext.element.create('SELECT', {
    on: { change: function() {
      this.setScale(parseInt(scale.value))
    }.bind(this) },
    parent: li
  });
  Object.keys(this.scales).forEach(function(s) {
    ol.ext.element.create('OPTION', {
      html: this.scales[s],
      value: s,
      parent: scale
    });
  }.bind(this));
  // Legend
  li = ol.ext.element.create('LI',{ 
    className: 'ol-legend',
    parent: ul 
  });
  var legend = ol.ext.element.createSwitch({ 
    html: (this.i18n('legend')),
    checked: false,
    on: { change: function() {
      extraCtrl.legend.control.setCanvas(legend.checked);
    }.bind(this) },
    parent: li 
  });
  // North
  li = ol.ext.element.create('LI',{ 
    className: 'ol-print-north',
    parent: ul 
  });
  var north = this._input.north = ol.ext.element.createSwitch({ 
    html: this.i18n('north'),
    checked: 'checked',
    on:  { change: function() {
      if (north.checked) this._compass.element.classList.add('ol-print-compass');
      else this._compass.element.classList.remove('ol-print-compass');
      this.getMap().render();
    }.bind(this)},
    parent: li 
  });
  // Title
  li = ol.ext.element.create('LI',{ 
    className: 'ol-print-title',
    parent: ul 
  });
  var title = ol.ext.element.createSwitch({ 
    html: this.i18n('mapTitle'),
    checked: false,
    on: { change: function(e) {
      extraCtrl.title.control.setVisible(e.target.checked);
    }.bind(this) },
    parent: li 
  });
  var titleText = ol.ext.element.create('INPUT', {
    type: 'text',
    placeholder: this.i18n('mapTitle'),
    on: {
      keydown: function(e) { 
        if (e.keyCode === 13) e.preventDefault();
      },
      keyup: function() { 
        extraCtrl.title.control.setTitle(titleText.value);
      },
      change: function() {
        extraCtrl.title.control.setTitle(titleText.value);
      }.bind(this)
    },
    parent: li
  });
  // User div element
  var userElt = ol.ext.element.create('DIV', {
    className: 'ol-user-param',
    parent: param
  });
  // Save as
  li = ol.ext.element.create('LI',{ 
    className: 'ol-saveas',
    parent: ul 
  });
  var copied = ol.ext.element.create('DIV', {
    html: this.i18n('copied'),
    className: 'ol-clipboard-copy',
    parent: li
  });
  var save = ol.ext.element.create('SELECT', {
    on: { change: function() {
      // Copy to clipboard
      if (this.formats[save.value].clipboard) {
        printCtrl.copyMap(this.formats[save.value], function(isok) {
          if (isok) {
            copied.classList.add('visible');
            setTimeout(function() { copied.classList.remove('visible'); }, 1000);
          }
        });
      } else {
        // Print to file
        var format = (typeof(this.getSize())==='string' ? this.getSize() : null);
        var opt = Object.assign({
          format: format,
          size: format ? this.paperSize[format] : null,
          orient: this.getOrientation(),
          margin: this.getMargin(),
        }, this.formats[save.value]);
        printCtrl.print(opt);
      }
      save.value = '';
    }.bind(this) },
    parent: li
  });
  ol.ext.element.create('OPTION', {
    html: this.i18n('saveas'),
    style: { display: 'none' },
    value: '',
    parent: save
  });
  this.formats.forEach(function(format, i) {
    if (format.pdf) {
      if (options.pdf === false) return;
    } else if (format.clipboard) {
      if (options.copy === false) return;
    } else if (options.save === false) {
      return;
    }
    ol.ext.element.create('OPTION', {
      html: this.i18n(format.title),
      value: i,
      parent: save
    });
  }.bind(this));
  // Save Legend
  li = ol.ext.element.create('LI',{ 
    className: 'ol-savelegend',
    parent: ul 
  });
  var copylegend = ol.ext.element.create('DIV', {
    html: this.i18n('copied'),
    className: 'ol-clipboard-copy',
    parent: li
  });
  var saveLegend = ol.ext.element.create('SELECT', {
    on: { change: function() {
      // Print canvas (with white background)
      var clegend = extraCtrl.legend.control.getLegend().getCanvas();
      var canvas = document.createElement('CANVAS');
      canvas.width = clegend.width;
      canvas.height = clegend.height;
      var ctx = canvas.getContext('2d');
      ctx.fillStyle = '#fff';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(clegend, 0, 0);
      // Copy to clipboard
      if (this.formats[saveLegend.value].clipboard) {
        canvas.toBlob(function(blob) {
          try {
            navigator.clipboard.write([
              new window.ClipboardItem(
                Object.defineProperty({}, blob.type, {
                  value: blob,
                  enumerable: true
                })
              )
            ])
            copylegend.classList.add('visible');
            setTimeout(function() { copylegend.classList.remove('visible'); }, 1000);
          } catch (err) { /* errror */ }
        }, 'image/png');
      } else {
        var image;
        try {
          image = canvas.toDataURL(this.formats[saveLegend.value].imageType, this.formats[saveLegend.value].quality);
          var format = (typeof(this.getSize())==='string' ? this.getSize() : 'A4');
          var w = canvas.width / 96 * 25.4;
          var h = canvas.height / 96 * 25.4;
          var size = this.paperSize[format];
          if (this.getOrientation()==='landscape') size = [size[1], size[0]];
          var position = [
            (size[0] - w) /2,
            (size[1] - h) /2
          ]; 
          this.dispatchEvent({
            type: 'print',
            print: {
              legend: true,
              format: format,
              orientation: this.getOrientation(),
              unit: 'mm',
              size: this.paperSize[format],
              position: position,
              imageWidth: w,
              imageHeight: h
            },
            image: image,
            imageType: this.formats[saveLegend.value].imageType,
            pdf: this.formats[saveLegend.value].pdf,
            quality: this.formats[saveLegend.value].quality,
            canvas: canvas
          })
        } catch(err) { /* error */ }
      }
      saveLegend.value = '';
    }.bind(this) },
    parent: li
  });
  ol.ext.element.create('OPTION', {
    html: this.i18n('saveLegend'),
    style: { display: 'none' },
    value: '',
    parent: saveLegend
  });
  this.formats.forEach(function(format, i) {
    ol.ext.element.create('OPTION', {
      html: this.i18n(format.title),
      value: i,
      parent: saveLegend
    });
  }.bind(this));
  // Print
  var prButtons = ol.ext.element.create('DIV', {
    className: 'ol-ext-buttons',
    parent: param
  });
  ol.ext.element.create('BUTTON', {
    html: this.i18n('printBt'),
    type: 'submit',
    click: function(e) {
      e.preventDefault();
      window.print();
    },
    parent: prButtons
  });
  ol.ext.element.create('BUTTON', {
    html: this.i18n('cancel'),
    type: 'button',
    click: function() { printDialog.hide(); },
    parent: prButtons
  });
  ol.ext.element.create('DIV', {
    html: this.i18n('errorMsg'),
    className: 'ol-error',
    parent: param
  });
  // Handle dialog show/hide
  var originalTarget;
  var originalSize;
  var scalelistener;
  var extraCtrl = {};
  printDialog.on('show', function() {
    // Dialog is showing
    this.dispatchEvent({ type: 'show', userElement: userElt, dialog: this._printDialog, page: this.getPage() });
    //
    var map = this.getMap();
    if (!map) return;
    // Print document
    document.body.classList.add('ol-print-document');
    originalTarget = map.getTargetElement();
    originalSize = map.getSize();
    if (typeof(this.getSize()) === 'string') this.setSize(this.getSize());
    else this.setSize(originalSize);
    map.setTarget(printMap);
    // Refresh on move end
    if (scalelistener) ol.Observable.unByKey(scalelistener);
    scalelistener = map.on('moveend', function() {
      this.setScale(ol.sphere.getMapScale(map));
    }.bind(this));
    this.setScale(ol.sphere.getMapScale(map));
    // Get extra controls
    extraCtrl = {};
    this.getMap().getControls().forEach(function(c) {
      if (c instanceof ol.control.Legend) {
        extraCtrl.legend = { control: c };
      }
      if (c instanceof ol.control.CanvasTitle) {
        extraCtrl.title = { control: c };
      }
      if (c instanceof ol.control.Compass) {
        if (extraCtrl.compass) {
          c.element.classList.remove('ol-print-compass')
        } else {
          if (this._input.north.checked) c.element.classList.add('ol-print-compass')
          else c.element.classList.remove('ol-print-compass')
          this._compass = c;
          extraCtrl.compass = { control: c };
        }
      }
    }.bind(this));
    // Show hide title
    if (extraCtrl.title) {
      title.checked = extraCtrl.title.isVisible = extraCtrl.title.control.getVisible();
      titleText.value = extraCtrl.title.control.getTitle();
      title.parentNode.parentNode.classList.remove('hidden');
    } else {
      title.parentNode.parentNode.classList.add('hidden');
    }
    // Show hide legend
    if (extraCtrl.legend) {
      extraCtrl.legend.ison = extraCtrl.legend.control.onCanvas();
      extraCtrl.legend.collapsed = extraCtrl.legend.control.isCollapsed();
      extraCtrl.legend.control.collapse(false);
      saveLegend.parentNode.classList.remove('hidden');
      legend.parentNode.parentNode.classList.remove('hidden');
      legend.checked = !extraCtrl.legend.collapsed;
      extraCtrl.legend.control.setCanvas(!extraCtrl.legend.collapsed);
    } else {
      saveLegend.parentNode.classList.add('hidden');
      legend.parentNode.parentNode.classList.add('hidden');
    }
  }.bind(this));
  printDialog.on('hide', function() {
    // No print
    document.body.classList.remove('ol-print-document');
    if (!originalTarget) return;
    this.getMap().setTarget(originalTarget);
    originalTarget = null;
    if (scalelistener) ol.Observable.unByKey(scalelistener);
    // restore
    if (extraCtrl.title) {
      extraCtrl.title.control.setVisible(extraCtrl.title.isVisible);
    }
    if (extraCtrl.legend) {
      extraCtrl.legend.control.setCanvas(extraCtrl.legend.ison);
      extraCtrl.legend.control.collapse(extraCtrl.legend.collapsed);
    }
    this.dispatchEvent({ type: 'hide' });
  }.bind(this));
  // Update preview on resize
  window.addEventListener('resize', function() {
    this.setSize();
  }.bind(this));
  // Save or print
  if (options.saveAs) {
    this.on('print', function(e) {
      if (!e.pdf) {
        // Save image as file
        e.canvas.toBlob(function(blob) {
          var name = (e.print.legend ? 'legend.' : 'map.')+e.imageType.replace('image/','');
          options.saveAs(blob, name);
        }, e.imageType, e.quality);
      }
    });
  }
  // Save or print
  if (options.jsPDF) {
    this.on('print', function(e) {
      if (e.pdf) {
        // Export pdf using the print info
        var pdf = new options.jsPDF({
          orientation: e.print.orientation,
          unit: e.print.unit,
          format: e.print.size
        });
        pdf.addImage(e.image, 'JPEG', e.print.position[0], e.print.position[0], e.print.imageWidth, e.print.imageHeight);
        pdf.save(e.print.legend ? 'legend.pdf' : 'map.pdf');
      } 
    });
  }
};
ol.ext.inherits(ol.control.PrintDialog, ol.control.Control);
/** Check if the dialog is oprn
 * @return {boolean}
 */
 ol.control.PrintDialog.prototype.isOpen = function() {
  return this._printDialog.isOpen();
};
/** Add a new language
 * @param {string} lang lang id
 * @param {Objetct} labels
 */
ol.control.PrintDialog.addLang = function(lang, labels) {
  ol.control.PrintDialog.prototype._labels[lang] = labels;
};
/** Translate 
 * @param {string} what
 * @returns {string}
 */
ol.control.PrintDialog.prototype.i18n = function(what) {
  var rep = this._labels.en[what] || 'bad param';
  if (this._labels[this._lang] && this._labels[this._lang][what]) {
    rep = this._labels[this._lang][what];
  }
  return rep;
};
/** 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],
  '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'
};
/** Get print orientation
 * @returns {string}
 */
ol.control.PrintDialog.prototype.getOrientation = function () {
  return this._orientation || 'portrait';
};
/** Set print orientation
 * @param {string} ori landscape or portrait
 */
ol.control.PrintDialog.prototype.setOrientation = function (ori) {
  this._orientation = (ori==='landscape' ? 'landscape' : 'portrait');
  this._input.orientation[this._orientation].checked = true;
  this.setSize();
};
/** Get print margin
 * @returns {number}
 */
ol.control.PrintDialog.prototype.getMargin = function () {
  return this._margin || 0;
};
/** Set print margin
 * @param {number}
 */
ol.control.PrintDialog.prototype.setMargin = function (margin) {
  this._margin = margin;
  this._input.margin.value = margin;
  this.setSize();
};
/** Get print size
 * @returns {ol.size}
 */
ol.control.PrintDialog.prototype.getSize = function () {
  return this._size;
};
/** Set map print size
 * @param {ol/size|string} size map size as ol/size or A4, etc.
 */
ol.control.PrintDialog.prototype.setSize = function (size) {
  // reset status
  this._printDialog.getContentElement().setAttribute('data-status','');
  if (size) this._size = size;
  else size = this._size;
  if (!size) return;
  if (typeof(size) === 'string') {
    size = size.toLocaleUpperCase();
    if (!this.paperSize[size]) size = this._size = 'A4';
    this._input.size.value = size;
    size = [
      Math.trunc(this.paperSize[size][0]* 96/25.4),
      Math.trunc(this.paperSize[size][1]* 96/25.4)
    ]
    if (this.getOrientation() === 'landscape') {
      size = [size[1], size[0]];
    }
    this.getPage().classList.remove('margin');
  } else {
    this._input.size.value = '';
    this.getPage().classList.add('margin');
  }
  var printElement = this.getPage();
  var s = printElement.parentNode.getBoundingClientRect();
  var scx = (s.width - 40) / size[0];
  var scy = (s.height - 40) / size[1];
  var sc = Math.min(scx, scy, 1);
  printElement.style.width = size[0]+'px';
  printElement.style.height = size[1]+'px';
  printElement.style['-webkit-transform'] = 
  printElement.style.transform = 'translate(-50%,-50%) scale('+sc+')';
  var px = Math.round(5/sc);
  printElement.style['-webkit-box-shadow'] = 
  printElement.style['box-shadow'] = px+'px '+px+'px '+px+'px rgba(0,0,0,.6)';
  printElement.style['padding'] = (this.getMargin() * 96/25.4)+'px';
  if (this.getMap()) {
    this.getMap().updateSize();
  }
  this.dispatchEvent({ type: 'dialog:refresh' });
};
/** Get dialog content element 
 * @return {Element}
 */
ol.control.PrintDialog.prototype.getContentElement = function () {
  return this._printDialog.getContentElement();
};
/** Get dialog user element 
 * @return {Element}
 */
ol.control.PrintDialog.prototype.getUserElement = function () {
  return this._printDialog.getContentElement().querySelector('.ol-user-param');
};
/** Get page element
 * @return {Element}
 */
ol.control.PrintDialog.prototype.getPage = function () {
  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
 */
ol.control.PrintDialog.prototype.setMap = function (map) {
  if (this.getMap()) {
    this.getMap().removeControl(this._compass);
    this.getMap().removeControl(this._printCtrl);
    this.getMap().removeControl(this._printDialog);
  }
  ol.control.Control.prototype.setMap.call(this, 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
 */
ol.control.PrintDialog.prototype.setScale = function (value) {
  ol.sphere.setMapScale(this.getMap(), value);
  this._input.scale.value = ' '+(Math.round(value/100) * 100);
};
/** Get the current map scale factor
 * @return {number} 
 */
ol.control.PrintDialog.prototype.getScale = function () {
  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
 */
ol.control.PrintDialog.prototype.print = function(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}
 */
ol.control.PrintDialog.prototype.getrintControl = function() {
  return this._printCtrl;
}

/*	Copyright (c) 2016 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
/**
 * @classdesc OpenLayers 3 Profil Control.
 * Draw a profile of a feature (with a 3D geometry)
 *
 * @constructor
 * @extends {ol.control.Control}
 * @fires over
 * @fires out
 * @fires show
 * @fires dragstart
 * @fires dragging
 * @fires dragend
 * @fires dragcancel
 * @param {Object=} options
 *  @param {string} options.className
 *	@param {String} options.title button title
 *  @param {ol.style.Style} [options.style] style to draw the profil, default darkblue
 *  @param {ol.style.Style} [options.selectStyle] style for selection, default darkblue fill
 *  @param {*} options.info keys/values for i19n
 *  @param {number} [options.width=300]
 *  @param {number} [options.height=150]
 *  @param {ol.Feature} [options.feature] the feature to draw profil
 *  @param {boolean} [options.selectable=false] enable selection on the profil, default false
 *  @param {boolean} [options.zoomable=false] can zoom in the profil
 */
ol.control.Profil = function(options) {
  options = options || {};
  this.info = options.info || ol.control.Profil.prototype.info;
  var self = this;
  var element;
  if (options.target) {
    element = document.createElement("div");
    element.classList.add(options.className || "ol-profil");
  } else {
    element = document.createElement("div");
    element.className = ((options.className || 'ol-profil') +' ol-unselectable ol-control ol-collapsed').trim();
    this.button = document.createElement("button");
    this.button.title =  options.title || 'Profile',
    this.button.setAttribute('type','button');
    var click_touchstart_function = function(e) {
      self.toggle();
      e.preventDefault();
    };
    this.button.addEventListener("click", click_touchstart_function);
    this.button.addEventListener("touchstart", click_touchstart_function);
    element.appendChild(this.button);
  }
  // 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));
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.set('selectable', options.selectable);
  // Offset in px
  this.margin_ = { top:10*ratio, left:45*ratio, bottom:30*ratio, right:10*ratio };
  if (!this.info.ytitle) this.margin_.left -= 20*ratio;
  if (!this.info.xtitle) this.margin_.bottom -= 20*ratio;
  // Cursor
  this.bar_ = document.createElement("div");
  this.bar_.classList.add("ol-profilbar");
  this.bar_.style.top = (this.margin_.top/ratio)+"px";
  this.bar_.style.height = (this.canvas_.height-this.margin_.top-this.margin_.bottom)/ratio+"px";
  div.appendChild(this.bar_);
  this.cursor_ = document.createElement("div");
  this.cursor_.classList.add("ol-profilcursor");
  div.appendChild(this.cursor_);
  this.popup_ = document.createElement("div");
  this.popup_.classList.add("ol-profilpopup");
  this.cursor_.appendChild(this.popup_);
  // Track information
  var t = document.createElement("table");
      t.cellPadding = '0';
      t.cellSpacing = '0';
      t.style.clientWidth = this.canvas_.width/ratio + "px";
    div.appendChild(t);
  var firstTr = document.createElement("tr");
      firstTr.classList.add("track-info");
      t.appendChild(firstTr);
  var div_zmin = document.createElement("td");
  div_zmin.innerHTML = (this.info.zmin||"Zmin")+': <span class="zmin">';
  firstTr.appendChild(div_zmin);
  var div_zmax = document.createElement("td");
  div_zmax.innerHTML = (this.info.zmax||"Zmax")+': <span class="zmax">';
  firstTr.appendChild(div_zmax);
  var div_distance = document.createElement("td");
  div_distance.innerHTML = (this.info.distance||"Distance")+': <span class="dist">';
  firstTr.appendChild(div_distance);
  var div_time = document.createElement("td");
  div_time.innerHTML = (this.info.time||"Time")+': <span class="time">';
  firstTr.appendChild(div_time);
  var secondTr = document.createElement("tr");
      secondTr.classList.add("point-info")
      t.appendChild(secondTr);
  var div_altitude = document.createElement("td");
  div_altitude.innerHTML = (this.info.altitude||"Altitude")+': <span class="z">';
  secondTr.appendChild(div_altitude);
  var div_distance2 = document.createElement("td");
  div_distance2.innerHTML = (this.info.distance||"Distance")+': <span class="dist">';
  secondTr.appendChild(div_distance2);
  var div_time2 = document.createElement("td");
  div_time2.innerHTML = (this.info.time||"Time")+': <span class="time">';
  secondTr.appendChild(div_time2);
  // Array of data
  this.tab_ = [];
  // Show feature
  if (options.feature) {
    this.setGeometry (options.feature);
  }
  // Zoom on profile
  if (options.zoomable) {
    this.set('selectable', true);
    var start, geom;
    this.on('change:geometry', function() {
      geom = null;
    });
    this.on('dragstart', function(e) {
      start = e.index;
    })
    this.on('dragend', function(e) {
      if (Math.abs(start - e.index) > 10) {
        if (!geom) {
          var bt = ol.ext.element.create('BUTTON', {
            parent: element,
            className: 'ol-zoom-out',
            click: function(e) {
              e.stopPropagation();
              e.preventDefault();
              if (geom) {
                this.dispatchEvent({ type:'zoom' });
                this.setGeometry(geom, this._geometry[1]);
              }
              element.removeChild(bt);
            }.bind(this)
          })
        }
        var saved = geom || this._geometry[0];
        var g = new ol.geom.LineString(this.getSelection(start, e.index));
        this.setGeometry(g, this._geometry[1]);
        geom = saved;
        this.dispatchEvent({ type:'zoom', geometry: g, start: start, end: e.index });
      }
    }.bind(this));
  }
};
ol.ext.inherits(ol.control.Profil, ol.control.Control);
/** Custom infos list
* @api stable
*/
ol.control.Profil.prototype.info = {
  "zmin": "Zmin",
  "zmax": "Zmax",
  "ytitle": "Altitude (m)",
  "xtitle": "Distance (km)",
  "time": "Time",
  "altitude": "Altitude",
  "distance": "Distance",
  "altitudeUnits": "m",
  "distanceUnitsM": "m",
  "distanceUnitsKM": "km",
};
/** Show popup info
* @param {string} info to display as a popup
* @api stable
*/
ol.control.Profil.prototype.popup = function(info) {
  this.popup_.innerHTML = info;
};
/** Show point on profil
 * @param {*} p 
 * @param {number} dx 
 * @private
 */
ol.control.Profil.prototype._drawAt = function(p, dx) {
  if (p) {
    this.cursor_.style.left = dx+"px";
    this.cursor_.style.top = (this.canvas_.height-this.margin_.bottom+p[1]*this.scale_[1]+this.dy_)/this.ratio+"px";
    this.cursor_.style.display = "block";
    this.bar_.parentElement.classList.add("over");
    this.bar_.style.left = dx+"px";
    this.bar_.style.display = "block";
    this.element.querySelector(".point-info .z").textContent = p[1]+this.info.altitudeUnits;
    this.element.querySelector(".point-info .dist").textContent = (p[0]/1000).toFixed(1)+this.info.distanceUnitsKM;
    this.element.querySelector(".point-info .time").textContent = p[2];
    if (dx>this.canvas_.width/this.ratio/2) this.popup_.classList.add('ol-left');
    else this.popup_.classList.remove('ol-left');
  } else {
    this.cursor_.style.display = "none";
    this.bar_.style.display = 'none';
    this.cursor_.style.display = 'none';  
    this.bar_.parentElement.classList.remove("over");
  }
};
/** Show point at coordinate or a distance on the profil
 * @param { ol.coordinates|number } where a coordinate or a distance from begining, if none it will hide the point
 * @return { ol.coordinates } current point
 */
ol.control.Profil.prototype.showAt = function(where) {
  var i, p, p0, d0 = Infinity;
  if (typeof(where) === 'undefined') {
    if (this.bar_.parentElement.classList.contains("over")) {
      // Remove it
      this._drawAt();
    }
  } else if (where.length) {
    // Look for closest the point
    for (i=1; p=this.tab_[i]; i++) {
      var d = ol.coordinate.dist2d(p[3], where);
      if (d<d0) {
        p0 = p;
        d0 = d;
      } 
    }
  } else {
    for (i=0; p=this.tab_[i]; i++) {
      p0 = p;
      if (p[0] >= where) {
        break;
      } 
    }
  }
  if (p0) {
    var dx = (p0[0] * this.scale_[0] + this.margin_.left) / this.ratio;
    this._drawAt(p0, dx);
    return p0[3];
  }
  return null;
};
/** Show point at a time on the profil
 * @param { Date|number } time a Date or a DateTime (in s) to show the profile on, if none it will hide the point
 * @param { booelan } delta true if time is a delta from the start, default false
 * @return { ol.coordinates } current point
 */
ol.control.Profil.prototype.showAtTime = function(time, delta) {
  var i, p, p0;
  if (time instanceof Date) {
    time = time.getTime()/1000;
  } else if (delta) {
    time += this.tab_[0][3][3];
  }
  if (typeof(time) === 'undefined') {
    if (this.bar_.parentElement.classList.contains("over")) {
      // Remove it
      this._drawAt();
    }
  } else {
    for (i=0; p=this.tab_[i]; i++) {
      p0 = p;
      if (p[3][3] >= time) {
        break;
      } 
    }
  }
  if (p0) {
    var dx = (p0[0] * this.scale_[0] + this.margin_.left) / this.ratio;
    this._drawAt(p0, dx);
    return p0[3];
  }
  return null;
};
/** Get the point at a given time on the profil
 * @param { number } time time at which to show the point
 * @return { ol.coordinates } current point
 */
ol.control.Profil.prototype.pointAtTime = function(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
 */
ol.control.Profil.prototype.onMove = function(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
*/
ol.control.Profil.prototype.show = function() {
  this.element.classList.remove("ol-collapsed");
  this.dispatchEvent({ type:'show', show: true });
};
/** Hide panel
* @api stable
*/
ol.control.Profil.prototype.hide = function() {
  this.element.classList.add("ol-collapsed");
  this.dispatchEvent({ type:'show', show: false });
};
/** Toggle panel
* @api stable
*/
ol.control.Profil.prototype.toggle = function() {
  this.element.classList.toggle("ol-collapsed");
  var b = this.element.classList.contains("ol-collapsed");
  this.dispatchEvent({ type:'show', show: !b });
}
/** Is panel visible
*/
ol.control.Profil.prototype.isShown = function() {
  return (!this.element.classList.contains("ol-collapsed"));
};
/** Get selection
 * @param {number} starting point
 * @param {number} ending point
 * @return {Array<ol.coordinate>}
 */
ol.control.Profil.prototype.getSelection = function(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
 */
ol.control.Profil.prototype._drawGraph = function(t, style) {
  if (!t.length) return;
  var ctx = this.canvas_.getContext('2d');
  var scx = this.scale_[0];
  var scy = this.scale_[1];
  var dy = this.dy_;
  var ratio = this.ratio;
  var i, p;
  // Draw Path
  ctx.beginPath();
  for (i=0; p=t[i]; i++) {
    if (i==0) ctx.moveTo(p[0]*scx,p[1]*scy+dy);
    else ctx.lineTo(p[0]*scx,p[1]*scy+dy);
  }
  if (style.getStroke()) {
    ctx.strokeStyle = style.getStroke().getColor() || '#000';
    ctx.lineWidth = style.getStroke().getWidth() * ratio;
    ctx.setLineDash([]);
    ctx.stroke();
  }
  // Fill path
  if (style.getFill()) {
    ctx.fillStyle = style.getFill().getColor() || '#000';
    ctx.Style = style.getFill().getColor() || '#000';
    ctx.lineTo(t[t.length-1][0]*scx, 0);
    ctx.lineTo(t[0][0]*scx, 0);
    ctx.fill();
  }
};
/**
 * Set the geometry to draw the profil.
 * @param {ol.Feature|ol.geom.Geometry} f the feature.
 * @param {Object=} options
 *  @param {ol.ProjectionLike} [options.projection] feature projection, default projection of the map
 *  @param {string} [options.zunit='m'] 'm' or 'km', default m
 *  @param {string} [options.unit='km'] 'm' or 'km', default km
 *  @param {Number|undefined} [options.zmin=0] default 0
 *  @param {Number|undefined} options.zmax default max Z of the feature
 *  @param {integer|undefined} [options.zDigits=0] number of digits for z graduation, default 0
 *  @param {integer|undefined} [options.zMaxChars] maximum number of chars to be used for z graduation before switching to scientific notation
 *  @param {Number|undefined} [options.graduation=100] z graduation default 100
 *  @param {integer|undefined} [options.amplitude] amplitude of the altitude, default zmax-zmin
 * @api stable
 */
ol.control.Profil.prototype.setGeometry = function(g, options) {
  if (!options) options = {};
  if (g instanceof ol.Feature) g = g.getGeometry();
  this._geometry = [g, options];
  // No Z
  if (!/Z/.test(g.getLayout())) return;
  // No time
  if(/M/.test(g.getLayout())) this.element.querySelector(".time").parentElement.style.display = 'block';
  else this.element.querySelector(".time").parentElement.style.display = 'none';
  // Coords
  var c = g.getCoordinates();
  switch (g.getType()) {
    case "LineString": break;
    case "MultiLineString": c = c[0]; break;
    default: return;
  }
  // Distance beetween 2 coords
  var proj = options.projection || this.getMap().getView().getProjection();
  function dist2d(p1,p2) {
    return ol.sphere.getDistance(
      ol.proj.transform(p1, proj, 'EPSG:4326'),
      ol.proj.transform(p2, proj, 'EPSG:4326')
    );
  }
  function getTime(t0, t1) {
    if (!t0 || !t1) return "-"
    var dt = (t1-t0) / 60; // mn
    var ti = Math.trunc(dt/60);
    var mn = Math.trunc(dt-ti*60);
    return ti+"h"+(mn<10?"0":"")+mn+"mn";
  }
  // Calculate [distance, altitude, time, point] for each points
  var zmin=Infinity, zmax=-Infinity;
  var i, p, d, z, ti, t = this.tab_ = [];
  for (i=0, p; p=c[i]; i++) {
    z = p[2];
    if (z<zmin) zmin=z;
    if (z>zmax) zmax=z;
    if (i==0) d = 0;
    else d += dist2d(c[i-1], p);
    ti = getTime(c[0][3],p[3]);
    t.push ([d, z, ti, p]);
  }
  this._z = [zmin,zmax];
  this.set('graduation', options.graduation || 100);
  this.set('zmin', options.zmin);
  this.set('zmax', options.zmax);
  this.set('amplitude', options.amplitude);
  this.set('unit', options.unit);
  this.set('zunit', options.zunit);
  this.set('zDigits', options.zDigits);
  this.set('zMaxChars', options.zMaxChars);
  this.dispatchEvent({ type: 'change:geometry', geometry: g })
  this.refresh();
};
/** Refresh the profil
 */
ol.control.Profil.prototype.refresh = function() {
  var canvas = this.canvas_;
  var ctx = canvas.getContext('2d');
  var w = canvas.width;
  var h = canvas.height;
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0,0, w, h);
  var zmin = this._z[0];
  var zmax = this._z[1];
  var t = this.tab_;
  var d = t[t.length-1][0];
  var ti = t[t.length-1][2];
  var i;
  if (!d) {
    console.error('[ol/control/Profil] no data...', t);
    return;
  }
  // Margin
  ctx.setTransform(1, 0, 0, 1, this.margin_.left, h-this.margin_.bottom);
  var ratio = this.ratio;
  w -= this.margin_.right + this.margin_.left;
  h -= this.margin_.top + this.margin_.bottom;
  // Draw axes
  ctx.strokeStyle = this._style.getText().getFill().getColor() || '#000';
  ctx.lineWidth = 0.5*ratio;
  ctx.beginPath();
  ctx.moveTo(0,0); ctx.lineTo(0,-h);
  ctx.moveTo(0,0); ctx.lineTo(w, 0);
  ctx.stroke();
  // Info
  this.element.querySelector(".track-info .zmin").textContent = zmin.toFixed(2)+this.info.altitudeUnits;
  this.element.querySelector(".track-info .zmax").textContent = zmax.toFixed(2)+this.info.altitudeUnits;
  if (d>1000) {
    this.element.querySelector(".track-info .dist").textContent = (d/1000).toFixed(1)+this.info.distanceUnitsKM;
  } else {
    this.element.querySelector(".track-info .dist").textContent= (d).toFixed(1)+this.info.distanceUnitsM;
  }
  this.element.querySelector(".track-info .time").textContent = ti;
  // Set graduation
  var grad = this.get('graduation');
  while (true) {
    zmax = Math.ceil(zmax/grad)*grad;
    zmin = Math.floor(zmin/grad)*grad;
    var nbgrad = (zmax-zmin)/grad;
    if (h/nbgrad < 15*ratio) {
      grad *= 2;
    }
    else break;
  }
  // Set amplitude
  if (typeof(this.get('zmin'))=='number' && zmin > this.get('zmin')) zmin = this.get('zmin');
  if (typeof(this.get('zmax'))=='number' && zmax < this.get('zmax')) zmax = this.get('zmax');
  var amplitude = this.get('amplitude');
  if (amplitude) {
    zmax = Math.max (zmin + amplitude, zmax);
  }
  // Scales lines
  var scx = w/d;
  var scy = -h/(zmax-zmin);
  var dy = this.dy_ = -zmin*scy;
  this.scale_ = [scx,scy];
  this._drawGraph(t, this._style);
  // Draw 
  ctx.textAlign = 'right';
  ctx.textBaseline = 'top';
  ctx.fillStyle = this._style.getText().getFill().getColor() || '#000';
  // Scale Z
  ctx.beginPath();
  var fix = this.get('zDigits') || 0;
  var exp = null;
  if (typeof(this.get('zMaxChars'))=='number') {
    var usedChars;
    if (this.get('zunit') != 'km') usedChars = Math.max(zmin.toFixed(fix).length, zmax.toFixed(fix).length);
    else usedChars = Math.max((zmin/1000).toFixed(1).length, (zmax/1000).toFixed(1).length);
    if (this.get('zMaxChars') < usedChars) {
      exp = Math.floor(Math.log10(Math.max(Math.abs(zmin), Math.abs(zmax),Number.MIN_VALUE)));
      ctx.font = 'bold '+(9*ratio)+'px arial';
      ctx.fillText(exp.toString(), -8*ratio, 8*ratio);
      var expMetrics = ctx.measureText(exp.toString());
      var expWidth = expMetrics.width;
      var expHeight = expMetrics.actualBoundingBoxAscent + expMetrics.actualBoundingBoxDescent;
      ctx.font = 'bold '+(12*ratio)+'px arial';
      ctx.fillText("10", -8*ratio-expWidth, 8*ratio+0.5*expHeight);
    }
  }
  ctx.font = (10*ratio)+'px arial';
  ctx.textBaseline = 'middle';
  for (i=zmin; i<=zmax; i+=grad) {
    if (exp !== null) {
        var baseNumber = i / Math.pow(10, exp);
        if (this.get('zunit') == 'km')
            baseNumber /= 1000;
        var nbDigits = this.get('zMaxChars') - Math.floor(Math.log10(Math.max(Math.abs(baseNumber),1))+1) - 1;
        if (baseNumber < 0) nbDigits -= 1
        if (this.get('zunit') != 'km') ctx.fillText(baseNumber.toFixed(Math.max(nbDigits, 0)), -4*ratio, i*scy+dy);
        else ctx.fillText(baseNumber.toFixed(Math.max(nbDigits,0)), -4*ratio, i*scy+dy);
    } else {
        if (this.get('zunit') != 'km') ctx.fillText(i.toFixed(fix), -4*ratio, i*scy+dy);
        else ctx.fillText((i/1000).toFixed(1), -4*ratio, i*scy+dy);
    }
    ctx.moveTo (-2*ratio, i*scy+dy);
    if (i!=0) ctx.lineTo (d*scx, i*scy+dy);
    else ctx.lineTo (0, i*scy+dy);
  }
  // Scale X
  ctx.textAlign = "center";
  ctx.textBaseline = "top";
  ctx.setLineDash([ratio,3*ratio]);
  var unit = this.get('unit') ||"km";
  var step;
  if (d>1000) {
    step = Math.round(d/1000)*100;
    if (step > 1000) step = Math.ceil(step/1000)*1000;
  } else {
    unit = "m";
    if (d>100) step = Math.round(d/100)*10;
    else if (d>10) step = Math.round(d/10);
    else if (d>1) step = Math.round(d)/10;
    else step = d;
  }
  for (i=0; i<=d; i+=step) {
    var txt = (unit=="m") ? i : (i/1000);
    //if (i+step>d) txt += " "+ (options.zunits || "km");
    ctx.fillText(Math.round(txt*10)/10, i*scx, 4*ratio);
    ctx.moveTo (i*scx, 2*ratio); ctx.lineTo (i*scx, 0);
  }
  ctx.font = (12*ratio)+"px arial";
  ctx.fillText(this.info.xtitle.replace("(km)","("+unit+")"), w/2, 18*ratio);
  ctx.save();
  ctx.rotate(-Math.PI/2);
  ctx.fillText(this.info.ytitle, h/2, -this.margin_.left);
  ctx.restore();
  ctx.stroke();
};
/** Get profil image
* @param {string|undefined} type image format or 'canvas' to get the canvas image, default image/png.
* @param {Number|undefined} encoderOptions between 0 and 1 indicating image quality image/jpeg or image/webp, default 0.92.
* @return {string} requested data uri
* @api stable
*/
ol.control.Profil.prototype.getImage = function(type, encoderOptions) {
  if (type==="canvas") return this.canvas_;
  return this.canvas_.toDataURL(type, encoderOptions);
};

/*	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 = function(options) {
  options = options || {};
  var element = ol.ext.element.create('DIV', {
    className: ((options.className || '') + ' ol-progress-bar ol-unselectable ol-control').trim()
  });
  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
  });
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this._layerlistener = [];
  this.setLayers(options.layers);
};
ol.ext.inherits(ol.control.ProgressBar, ol.control.Control);
/** Set the control visibility
 * @param {Number} [n] progress percentage, a number beetween 0,1, default hide progress bar
 */
ol.control.ProgressBar.prototype.setPercent = function (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
 */
ol.control.ProgressBar.prototype.setLabel = function (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
 */
ol.control.ProgressBar.prototype.setLayers = function (layers) {
  // reset
  this._layerlistener.forEach(function (l) {
    ol.Observable.unByKey(l);
  });
  this._layerlistener = [];
  this.setPercent();
  var loading=0, loaded=0;
  if (layers instanceof ol.layer.Layer) layers = [layers];
  if (!layers || !layers.forEach) return;
  var tout;
  // Listeners
  layers.forEach(function(layer) {
    if (layer instanceof ol.layer.Layer) {
      this._layerlistener.push(layer.getSource().on('tileloadstart', function () {
        loading++;
        this.setPercent(loaded/loading);
        clearTimeout(tout);
      }.bind(this)));
      this._layerlistener.push(layer.getSource().on(['tileloadend', 'tileloaderror'], function () {
        loaded++;
        if (loaded === loading) {
          loading = loaded = 0;
          this.setPercent(1);
          tout = setTimeout(this.setPercent.bind(this), 300);
        } else {
          this.setPercent(loaded/loading);
        }
      }.bind(this)));
    }
  }.bind(this));
}

/*	Copyright (c) 2018 Jean-Marc VIGLINO,
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** Geoportail routing Control.
 * @constructor
 * @extends {ol.control.Control}
 * @fires select
 * @fires change:input
 * @fires routing:start
 * @fires routing
 * @fires step:select
 * @fires step:hover
 * @fires error
 * @fires abort
 * @param {Object=} options
 *	@param {string} options.className control class name
 *	@param {string | undefined} [options.apiKey] the service api key.
 *	@param {string | undefined} options.authentication: basic authentication for the service API as btoa("login:pwd")
 *	@param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport.
 *	@param {string | undefined} options.label Text label to use for the search button, default "search"
 *	@param {string | undefined} options.placeholder placeholder, default "Search..."
 *	@param {string | undefined} options.inputLabel label for the input, default none
 *	@param {string | undefined} options.noCollapse prevent collapsing on input blur, default false
 *	@param {number | undefined} options.typing a delay on each typing to start searching (ms) use -1 to prevent autocompletion, default 300.
 *	@param {integer | undefined} options.minLength minimum length to start searching, default 1
 *	@param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10
 *	@param {integer | undefined} options.maxHistory maximum number of items to display in history. Set -1 if you don't want history, default maxItems
 *	@param {function} options.getTitle a function that takes a feature and return the name to display in the index.
 *	@param {function} options.autocomplete a function that take a search string and callback function to send an array
 *	@param {number} options.timeout default 20s
 */
ol.control.RoutingGeoportail = function(options) {
  var self = this;
  if (!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';
  // Class name for history
  this._classname = options.className || 'search';
  this._source = new ol.source.Vector();
  // Authentication
  this._auth = options.authentication;
  var element = document.createElement("DIV");
  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');
    });
  }
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.set('url', 'https://wxs.ign.fr/calcul/geoportail/'+options.apiKey+'/rest/1.0.0/route');
  var content = ol.ext.element.create('DIV', { className: 'content', parent: element } )
  var listElt = ol.ext.element.create('DIV', { className: 'search-input', parent: content });
  this._search = [];
  this.addSearch(listElt, options);
  this.addSearch(listElt, options);
  ol.ext.element.create('I', { className: 'ol-car', title: options.carlabel||'by car', parent: content })
    .addEventListener("click", function() {
      self.setMode('car');
    });
  ol.ext.element.create('I', { className: 'ol-pedestrian', title: options.pedlabel||'pedestrian', parent: content })
    .addEventListener("click", function() {
      self.setMode('pedestrian');
    });
  ol.ext.element.create('I', { className: 'ol-ok', title: options.runlabel||'search', html:'OK', parent: content })
    .addEventListener("click", function() {
      self.calculate();
    });
  ol.ext.element.create('I', { className: 'ol-cancel', html:'cancel', parent: content })
    .addEventListener("click", function() {
      this.resultElement.innerHTML = '';
    }.bind(this));
  this.resultElement = document.createElement("DIV");
  this.resultElement.setAttribute('class', 'ol-result');
  element.appendChild(this.resultElement);
  this.setMode(options.mode || 'car');
  this.set('timeout', options.timeout || 20000);
};
ol.ext.inherits(ol.control.RoutingGeoportail, ol.control.Control);
ol.control.RoutingGeoportail.prototype.setMode = function (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();
};
ol.control.RoutingGeoportail.prototype.setMethod = function (method, silent) {
  this.set('method', method);
  if (!silent) this.calculate();
};
ol.control.RoutingGeoportail.prototype.addButton = function (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 }
 */
ol.control.RoutingGeoportail.prototype.getSource = function () {
  return this._source;
};
ol.control.RoutingGeoportail.prototype._resetArray = function (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
 */
ol.control.RoutingGeoportail.prototype.removeSearch = function (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
 */
ol.control.RoutingGeoportail.prototype.addSearch = function (element, options, after) {
  var self = this;
  var div = ol.ext.element.create('DIV');
  if (after) element.insertBefore(div, after.nextSibling);
  else element.appendChild(div);
  ol.ext.element.create ('BUTTON', { title: options.startlabel||"add/remove", parent: div})
    .addEventListener('click', function(e) {
      if (e.ctrlKey) {
        if (this._search.length>2) this.removeSearch(element, options, div);
      } else if (e.shiftKey) {
        this.addSearch(element, options, div);
      }
    }.bind(this));
  var search = div.olsearch = new ol.control.SearchGeoportail({
    className: 'IGNF ol-collapsed',
    apiKey: options.search.apiKey,
    authentication: options.search.authentication,
    target: div,
    reverse: true
  });
  search._changeCounter = 0;
  this._resetArray(element);
  search.on('select', function(e){
    search.setInput(e.search.fulltext);
    var f = search.get('feature');
    if (!f) {
      f = new ol.Feature(new ol.geom.Point(e.coordinate));
      search.set('feature', f);
      this._source.addFeature(f);
      // Check geometry change
      search.checkgeom = true;
      f.getGeometry().on('change', function() {
        if (search.checkgeom) this.onGeometryChange(search, f);
      }.bind(this));
    } else {
      search.checkgeom = false;
      if (!e.silent) f.getGeometry().setCoordinates(e.coordinate);
      search.checkgeom = true;
    }
    f.set('name', search.getTitle(e.search));
    f.set('step', this._search.indexOf(search));
    if (f.get('step') === 0) f.set('pos','start');
    else if (f.get('step') === this._search.length-1) f.set('pos','end');
    search.set('selection', e.search);
  }.bind(this));
  search.element.querySelector('input').addEventListener('change', function(){
    search.set('selection', null);
    self.resultElement.innerHTML = '';
  });
  if (this.getMap()) this.getMap().addControl(search);
};
/** Geometry has changed
 * @private
 */
ol.control.RoutingGeoportail.prototype.onGeometryChange = function (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.
 */
ol.control.RoutingGeoportail.prototype.setMap = function (map) {
  ol.control.Control.prototype.setMap.call(this, map);
  for (var i=0; i<this._search.length; i++) {
    var c = this._search[i];
    c.setMap(map);
  }
};
/** Get request data
 * @private
 */
ol.control.RoutingGeoportail.prototype.requestData = function (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', // 'bdtopo-pgr',
    profile: this.get('mode')==='pedestrian' ? 'pedestrian' : 'car',
    optimization: this.get('method') || 'fastest', // 'distance'
    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
 */
ol.control.RoutingGeoportail.prototype.getTimeString = function (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
 */
ol.control.RoutingGeoportail.prototype.getDistanceString = function (d) {
  return (d<1000) ? d.toFixed(0)+' m' : (d/1000).toFixed(2)+' km';
};
/** Show routing as a list
 * @private
 */
ol.control.RoutingGeoportail.prototype.listRouting = function (routing) {
  this.resultElement.innerHTML = '';
  var t = this.getTimeString(routing.duration);
  t += ' ('+this.getDistanceString(routing.distance)+')';
  var iElement = document.createElement('i');
  iElement.textContent = t;
  this.resultElement.appendChild(iElement)
  var ul = document.createElement('ul');
  this.resultElement.appendChild(ul);
  var info = {
    'none': 'Prendre sur ',
    'R': 'Tourner à droite sur ',
    'FR': 'Tourner légèrement à droite sur ',
    'L': 'Tourner à gauche sur ',
    'FL': 'Tourner légèrement à gauche sur ',
    'F': 'Continuer tout droit sur ',
  }
  routing.features.forEach(function(f, i) {
    var d = this.getDistanceString(f.get('distance'));
    t = this.getTimeString(f.get('durationT'));
    ol.ext.element.create('LI', {
      className: f.get('instruction'),
      html: (info[f.get('instruction')||'none']||'#')
        + ' ' + f.get('name')
        + '<i>' + d + (t ? ' - ' + t : '') +'</i>',
      on: {
        pointerenter: function() {
          this.dispatchEvent({ type: 'step:hover', hover: false, index: i, feature: f });
        }.bind(this),
        pointerleave: function() {
          this.dispatchEvent({ type: 'step:hover', hover: false, index: i, feature: f });
        }.bind(this)
      },
      click: function() {
        this.dispatchEvent({ type: 'step:select', index: i, feature: f });
      }.bind(this),
      parent: ul
    });
  }.bind(this));
};
/** Handle routing response
 * @private
 */
ol.control.RoutingGeoportail.prototype.handleResponse = function (data, start, end) {
  if (data.status === 'ERROR') {
    this.dispatchEvent({
      type: 'errror',
      status: '200',
      statusText: data.message
    })
    return;
  }
  // console.log(data)
  var routing = { type:'routing' };
  routing.features = [];
  var distance = 0;
  var duration = 0;
  var f;
  var parser = new ol.format.GeoJSON();
  var lastPt;
  for (var i=0, l; l=data.portions[i]; i++) {
    for (var j=0, s; s=l.steps[j]; j++) {
      /*
      var options = {
        geometry: geom.transform('EPSG:4326',this.getMap().getView().getProjection()),
        name: s.name,
        instruction: s.navInstruction,
        distance: parseFloat(s.distanceMeters),
        duration: parseFloat(s.durationSeconds)
      }
      //console.log(duration, options.duration, s)
      distance += options.distance;
      duration += options.duration;
      options.distanceT = distance;
      options.durationT = duration;
      f = new ol.Feature(options);
      */
      s.type = 'Feature'; 
      s.properties = s.attributes.name || s.attributes;
      s.properties.distance = s.distance;
      s.properties.duration = Math.round(s.duration * 60);
      // Route info
      if (s.instruction) {
        s.properties.instruction_type = s.instruction.type;
        s.properties.instruction_modifier = s.instruction.modifier;
      }
      // Distance / time
      distance += s.distance;
      duration += s.duration;
      s.properties.distanceT = Math.round(distance * 100) / 100;
      s.properties.durationT = Math.round(duration * 60);
      s.properties.name = s.properties.cpx_toponyme_route_nommee || s.properties.cpx_toponyme || s.properties.cpx_numero || s.properties.nom_1_droite || s.properties.nom_1_gauche || ''; 
      // TODO: BUG ?
      var lp = s.geometry.coordinates[s.geometry.coordinates.length-1]
      if (lastPt && !ol.coordinate.equal(lp, s.geometry.coordinates[s.geometry.coordinates.length-1])) {
        s.geometry.coordinates.unshift(lastPt);
      }
      lastPt = s.geometry.coordinates[s.geometry.coordinates.length-1];
      //
      f = parser.readFeature(s, {
        featureProjection: this.getMap().getView().getProjection()
      });
      routing.features.push(f);
    }
  }
  routing.distance = parseFloat(data.distance);
  routing.duration = parseFloat(data.duration) / 60;
  // Full route
  var route = parser.readGeometry(data.geometry, {
    featureProjection: this.getMap().getView().getProjection()
  });
  routing.feature = new ol.Feature ({
    geometry: route,
    start: this._search[0].getTitle(start),
    end: this._search[0].getTitle(end), 
    distance: routing.distance,
    duration: routing.duration
  });
  // console.log(data, routing);
  this.dispatchEvent(routing);
  this.path = routing;
  return routing;
};
/** Abort request
 */
ol.control.RoutingGeoportail.prototype.abort = function () {
  // 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)
 */
ol.control.RoutingGeoportail.prototype.calculate = function (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
 */
ol.control.RoutingGeoportail.prototype.ajax = function (url, onsuccess, onerror){
  var self = this;
  // Abort previous request
  if (this._request) {
    this._request.abort();
  }
  // New request
  var ajax = this._request = new XMLHttpRequest();
  ajax.open('GET', url, true);
  ajax.timeout = this.get('timeout') || 20000;
  if (this._auth) {
    ajax.setRequestHeader("Authorization", "Basic " + this._auth);
  }
  this.element.classList.add('ol-searching');
  // Load complete
  ajax.onload = function() {
    self._request = null;
    self.element.classList.remove('ol-searching');
    onsuccess.call(self, this);
  };
  // Timeout
  ajax.ontimeout = function () {
    self._request = null;
    self.element.classList.remove('ol-searching');
    if (onerror) onerror.call(self, this);
  };
  // Oops, TODO do something ?
  ajax.onerror = function() {
    self._request = null;
    self.element.classList.remove('ol-searching');
    if (onerror) onerror.call(self, this);
  };
  // GO!
  ajax.send();
};

/*	Copyright (c) 2017 Jean-Marc VIGLINO,
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/**
 * Scale Control.
 * A control to display the scale of the center on the map
 *
 * @constructor
 * @extends {ol.control.Control}
 * @fires select
 * @fires change:input
 * @param {Object=} options
 *  @param {string} options.className control class name
 *  @param {string} options.ppi screen ppi, default 96
 * 	@param {string} options.editable make the control editable, default true
 */
ol.control.Scale = function(options) {
  if (!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";
  }
  this._input = document.createElement("INPUT");
  this._input.value = '-';
  element.setAttribute('class', classNames);
  if (options.editable===false) this._input.readOnly = true;
  element.appendChild(this._input);
  ol.control.Control.call(this, {
  element: element,
  target: options.target
  });
  this._input.addEventListener("change", this.setScale.bind(this));
  this.set('ppi', options.ppi || 96)
};
ol.ext.inherits(ol.control.Scale, ol.control.Control);
/**
 * 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
 */
ol.control.Scale.prototype.setMap = function (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.on('moveend', this.getScale.bind(this));
  }
};
/** Display the scale
 */
ol.control.Scale.prototype.getScale = function () {
  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
 */
ol.control.Scale.prototype.formatScale = function (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
 */
ol.control.Scale.prototype.setScale = function (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 = function(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>';
   ol.control.SearchPhoton.call(this, options);
   this.set("postcode", options.postcode);
   this.set("citycode", options.citycode);
   this.set("type", options.type);
 };
ol.ext.inherits(ol.control.SearchBAN, ol.control.SearchPhoton);
/** 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
 */
ol.control.SearchBAN.prototype.getTitle = function (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
 */
ol.control.SearchBAN.prototype.select = function (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 });
};
ol.control.SearchBAN.prototype.requestData = function (s) {
  var data = ol.control.SearchPhoton.prototype.requestData.call(this, s);
  data.postcode = this.get('postcode'),
  data.citycode = this.get('citycode'),
  data.type = this.get('type')
  return data;
};

/*	Copyright (c) 2017 Jean-Marc VIGLINO, 
	released under the CeCILL-B license (French BSD license)
	(http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/**
 * Search on DFCI grid.
 *
 * @constructor
 * @extends {ol.control.Search}
 * @fires select
 * @param {Object=} Control options. 
 *	@param {string} options.className control class name
 *	@param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport.
 *	@param {string | undefined} options.label Text label to use for the search button, default "search"
 *	@param {string | undefined} options.placeholder placeholder, default "Search..."
 *	@param {number | undefined} options.typing a delay on each typing to start searching (ms), default 300.
 *	@param {integer | undefined} options.minLength minimum length to start searching, default 1
 *	@param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10
 *
 *	@param {string | undefined} options.property a property to display in the index, default 'name'.
 *	@param {function} options.getTitle a function that takes a feature and return the name to display in the index, default return the property 
 *	@param {function | undefined} options.getSearchString a function that take a feature and return a text to be used as search string, default geTitle() is used as search string
 */
ol.control.SearchDFCI = function(options) {
  if (!options) options = {};
  options.className = options.className || 'dfci';
  options.placeholder = options.placeholder || 'Code DFCI';
  ol.control.Search.call(this, options);
};
ol.ext.inherits(ol.control.SearchDFCI, ol.control.Search);
/** 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
*/
ol.control.SearchDFCI.prototype.autocomplete = function (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
 */
ol.control.SearchFeature = function(options) {
  if (!options) options = {};
  options.className = options.className || 'feature';
  ol.control.Search.call(this, options);
  if (typeof(options.getSearchString)=="function") this.getSearchString = options.getSearchString;
  this.set('property', options.property || 'name');
  this.source_ = options.source;
};
ol.ext.inherits(ol.control.SearchFeature, ol.control.Search);
/** No history avaliable on features
 */
ol.control.SearchFeature.prototype.restoreHistory = function () {
  this.set('history', []);
};
/** No history avaliable on features
 */
ol.control.SearchFeature.prototype.saveHistory = function () {
  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
*/
ol.control.SearchFeature.prototype.getTitle = function (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
*/
ol.control.SearchFeature.prototype.getSearchString = function (f) {
  return this.getTitle(f);
};
/** Get the source
*	@return {ol.source.Vector}
*	@api
*/
ol.control.SearchFeature.prototype.getSource = function () {
  return this.source_;
}
/** Get the source
*	@param {ol.source.Vector} source
*	@api
*/
ol.control.SearchFeature.prototype.setSource = function (source) {
  this.source_ =  source;
};
/** 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
*/
ol.control.SearchFeature.prototype.autocomplete = function (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;
      }
    }
  }
  return result;
};

/*	Copyright (c) 2019 Jean-Marc VIGLINO, 
	released under the CeCILL-B license (French BSD license)
	(http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/**
 * Search on GPS coordinate.
 *
 * @constructor
 * @extends {ol.control.Search}
 * @fires select
 * @param {Object=} Control options. 
 *  @param {string} options.className control class name
 *  @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport.
 *  @param {string | undefined} options.label Text label to use for the search button, default "search"
 *  @param {string | undefined} options.placeholder placeholder, default "Search..."
 *  @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 300.
 *  @param {integer | undefined} options.minLength minimum length to start searching, default 1
 *  @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10
 */
ol.control.SearchGPS = function(options) {
  if (!options) options = {};
  options.className = (options.className || '') + ' ol-searchgps';
  options.placeholder = options.placeholder || 'lon,lat';
  ol.control.Search.call(this, options);
  // Geolocation
  this.geolocation = new ol.Geolocation({
    projection: "EPSG:4326",
    trackingOptions: {
      maximumAge: 10000,
      enableHighAccuracy: true,
      timeout: 600000
    }
  });
  ol.ext.element.create ('BUTTON', {
    className: 'ol-geoloc',
    title: 'Locate with GPS',
    parent: this.element,
    click: function(){
      this.geolocation.setTracking(true);
    }.bind(this)
  })
  // DMS switcher
  ol.ext.element.createSwitch({
    html: 'decimal',
    after: 'DMS',
    change: function(e) {
      if (e.target.checked) this.element.classList.add('ol-dms');
      else this.element.classList.remove('ol-dms');
    }.bind(this),
    parent: this.element
  });
  this._createForm();
  // Move list to the end
  var ul = this.element.querySelector("ul.autocomplete");
  this.element.appendChild(ul);
};
ol.ext.inherits(ol.control.SearchGPS, ol.control.Search);
/** Create input form
 * @private
 */
ol.control.SearchGPS.prototype._createForm = function () {
  // Value has change
  var onchange = function(e) {
    if (e.target.classList.contains('ol-dms')) {
      lon.value = (lond.value<0 ? -1:1) * Number(lond.value) + Number(lonm.value)/60 + Number(lons.value)/3600;
      lon.value = (lond.value<0 ? -1:1) * Math.round(lon.value*10000000)/10000000;
      lat.value = (latd.value<0 ? -1:1) * Number(latd.value) + Number(latm.value)/60 + Number(lats.value)/3600;
      lat.value = (latd.value<0 ? -1:1) * Math.round(lat.value*10000000)/10000000;
    }
    if (lon.value||lat.value) {
      this._input.value = lon.value+','+lat.value;
    } else {
      this._input.value = '';
    }
    if (!e.target.classList.contains('ol-dms')) {
      var s = ol.coordinate.toStringHDMS([Number(lon.value), Number(lat.value)]);
      var c = s.replace(/(N|S|E|W)/g,'').split('″');
      c[1] = c[1].trim().split(' ');
      lond.value = (/W/.test(s) ? -1 : 1) * parseInt(c[1][0]);
      lonm.value = parseInt(c[1][1]);
      lons.value = parseInt(c[1][2]);
      c[0] = c[0].trim().split(' ');
      latd.value = (/W/.test(s) ? -1 : 1) * parseInt(c[0][0]);
      latm.value = parseInt(c[0][1]);
      lats.value = parseInt(c[0][2]);
    }
    this.search();
  }.bind(this);
  function createInput(className, unit) {
    var input = ol.ext.element.create('INPUT', {
      className: className,
      type:'number',
      step:'any',
      lang: 'en',
      parent: div,
      on: {
        'change keyup': onchange
      }
    });
    if (unit) {
      ol.ext.element.create('SPAN', {
        className: 'ol-dms',
        html: unit,
        parent: div,
      });
    }
    return input;
  }
  // Longitude
  var div = ol.ext.element.create('DIV', {
    className: 'ol-longitude',
    parent: this.element
  });
  ol.ext.element.create('LABEL', {
    html: 'Longitude',
    parent: div
  });
  var lon = createInput('ol-decimal');
  var lond = createInput('ol-dms','°');
  var lonm = createInput('ol-dms','\'');
  var lons = createInput('ol-dms','"');
  // Latitude
  div = ol.ext.element.create('DIV', {
    className: 'ol-latitude',
    parent: this.element
  })
  ol.ext.element.create('LABEL', {
    html: 'Latitude',
    parent: div
  });
  var lat = createInput('ol-decimal');
  var latd = createInput('ol-dms','°');
  var latm = createInput('ol-dms','\'');
  var lats = createInput('ol-dms','"');
  // Focus on open
  if (this.button) {
    this.button.addEventListener("click", function() {
      lon.focus();
    });
  }
  // Change value on click
  this.on('select', function(e){
    lon.value = e.search.gps[0];
    lat.value = e.search.gps[1];
  }.bind(this));
  // Change value on geolocation
  this.geolocation.on('change', function(){
    this.geolocation.setTracking(false);
    var coord = this.geolocation.getPosition();
    lon.value = coord[0];
    lat.value = coord[1];
    this._triggerCustomEvent('keyup', lon);
  }.bind(this));
};
/** Autocomplete function
* @param {string} s search string
* @return {Array<any>|false} an array of search solutions
* @api
*/
ol.control.SearchGPS.prototype.autocomplete = function (s) {
  var result = [];
  var c = s.split(',');
  c[0] = Number(c[0]);
  c[1] = Number(c[1]);
  // Name
  s = ol.coordinate.toStringHDMS(c)
  if (s) s= s.replace(/(°|′|″) /g,'$1');
  // 
  var coord = ol.proj.transform ([c[0], c[1]], 'EPSG:4326', this.getMap().getView().getProjection());
  result.push({ gps: c, coordinate: coord, name: s });
  return result;
};

/*	Copyright (c) 2017 Jean-Marc VIGLINO,
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/**
 * Search places using the French National Base Address (BAN) API.
 *
 * @constructor
 * @extends {ol.control.SearchJSON}
 * @fires select
 * @param {any} options extend ol.control.SearchJSON options
 *	@param {string} options.className control class name
 *	@param {boolean | undefined} [options.apiKey] the service api key.
 *	@param {string | undefined} options.authentication: basic authentication for the service API as btoa("login:pwd")
 *	@param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport.
 *	@param {string | undefined} options.label Text label to use for the search button, default "search"
 *	@param {string | undefined} options.placeholder placeholder, default "Search..."
 *	@param {number | undefined} options.typing a delay on each typing to start searching (ms), default 500.
 *	@param {integer | undefined} options.minLength minimum length to start searching, default 3
 *	@param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10
 *
 *	@param {Number} options.pageSize item per page for parcelle list paging, use -1 for no paging, default 5
 * @see {@link https://geoservices.ign.fr/documentation/geoservices/geocodage.html}
 */
ol.control.SearchGeoportailParcelle = function(options) {
  var self = this;
  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...";
  ol.control.SearchGeoportail.call(this, options);
  this.set('copy', null);
  var element = this.element;
  // Add parcel form
  var div = document.createElement("DIV");
  element.appendChild(div);
  var label = document.createElement("LABEL");
  label.innerText = 'Préfixe'
  div.appendChild(label);
  label = document.createElement("LABEL");
  label.innerText = 'Section'
  div.appendChild(label);
  label = document.createElement("LABEL");
  label.innerText = 'Numéro'
  div.appendChild(label);
  div.appendChild(document.createElement("BR"));
  // Input
  this._inputParcelle = {
    prefix: document.createElement("INPUT"),
    section: document.createElement("INPUT"),
    numero: document.createElement("INPUT")
  };
  this._inputParcelle.prefix.setAttribute('maxlength',3);
  this._inputParcelle.section.setAttribute('maxlength',2);
  this._inputParcelle.numero.setAttribute('maxlength',4);
  // Delay search
  var tout;
  var doSearch = function() {
    if (tout) clearTimeout(tout);
    tout = setTimeout(function() {
        self.autocompleteParcelle();
    }, options.typing || 0);
  }
  // Add inputs
  for (var i in this._inputParcelle) {
    div.appendChild(this._inputParcelle[i]);
    this._inputParcelle[i].addEventListener("keyup", doSearch);
    this._inputParcelle[i].addEventListener('blur', function() {
      tout = setTimeout(function(){ element.classList.add('ol-collapsed-num'); }, 200);
    });
    this._inputParcelle[i].addEventListener('focus', function() {
      clearTimeout(tout);
      element.classList.remove('ol-collapsed-num');
    });
  }
  this.activateParcelle(false);
  // Autocomplete list
  var auto = document.createElement('DIV');
  auto.className = 'autocomplete-parcelle';
  element.appendChild(auto);
  var ul = document.createElement('UL');
  ul.classList.add('autocomplete-parcelle');
  auto.appendChild(ul);
  ul = document.createElement('UL');
  ul.classList.add('autocomplete-page');
  auto.appendChild(ul);
  // Show/hide list on fcus/blur	
  this._input.addEventListener('blur', function() {
    setTimeout(function(){ element.classList.add('ol-collapsed-list') }, 200);
  });
  this._input.addEventListener('focus', function() {
    element.classList.remove('ol-collapsed-list');
    self._listParcelle([]);
    if (self._commune) {
      self._commune = null;
      self._input.value = '';
      self.drawList_();
    }
    self.activateParcelle(false);
  });
  this.on('select', this.selectCommune.bind(this));
  this.set('pageSize', options.pageSize || 5);
};
ol.ext.inherits(ol.control.SearchGeoportailParcelle, ol.control.SearchGeoportail);
/** Select a commune => start searching parcelle  
 * @param {any} e 
 * @private
 */
ol.control.SearchGeoportailParcelle.prototype.selectCommune = function(e) {
  this._commune = e.search.insee;
  this._input.value = e.search.insee + ' - ' + e.search.fulltext;
  this.activateParcelle(true);
  this._inputParcelle.numero.focus();
  this.autocompleteParcelle();
};
/** Set the input parcelle
 * @param {*} p parcel
 * 	@param {string} p.Commune
 * 	@param {string} p.CommuneAbsorbee
 * 	@param {string} p.Section
 * 	@param {string} p.Numero
 * @param {boolean} search start a search
 */
ol.control.SearchGeoportailParcelle.prototype.setParcelle = function(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
 */
ol.control.SearchGeoportailParcelle.prototype.activateParcelle = function(b) {
  for (var i in this._inputParcelle) {
    this._inputParcelle[i].readOnly = !b;
  }
  if (b) {
    this._inputParcelle.section.parentElement.classList.add('ol-active');
  } else {
    this._inputParcelle.section.parentElement.classList.remove('ol-active');		
  }
};
/** Send search request for the parcelle  
 * @private
 */
ol.control.SearchGeoportailParcelle.prototype.autocompleteParcelle = function() {
  // Add 0 to fit the format
  function complete (s, n, c) {
    if (!s) return s;
    c = c || "0";
    while (s.length < n) s = c+s;
    return s.replace(/\*/g,'_');
  }
  // The selected commune
  var commune = this._commune;
  var prefix = complete (this._inputParcelle.prefix.value, 3);
  if (prefix === '000') {
    prefix = '___';
  }
  // Get parcelle number
  var section = complete (this._inputParcelle.section.value, 2);
  var numero = complete (this._inputParcelle.numero.value, 4, "0");
  var search = commune + (prefix||'___') + (section||"__") + (numero ?  numero : section ? "____":"0001");
  this.searchParcelle(search, 
    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
 */
ol.control.SearchGeoportailParcelle.prototype.searchParcelle = function(search, success /*, error */) {
  // 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);
      }
      success(jsonResp);
    }, 
    { dataType: 'XML' }
  );
};
/**
 * Draw the autocomplete list
 * @param {*} resp 
 * @private
 */
ol.control.SearchGeoportailParcelle.prototype._listParcelle = function(resp) {
  var self = this;
  var ul = this.element.querySelector("ul.autocomplete-parcelle");
  ul.innerHTML='';
  var page = this.element.querySelector("ul.autocomplete-page");
  page.innerHTML='';
  this._listParc = [];
  // Show page i
  function showPage(i) {
    var l = ul.children;
    var visible = "ol-list-"+i;
    var k;
    for (k=0; k<l.length; k++) {
      l[k].style.display = (l[k].className===visible) ? '' : 'none';
    }
    l = page.children;
    for (k=0; k<l.length; k++) {
      l[k].className = (l[k].innerText==i) ? 'selected' : '';
    }
    page.style.display = l.length>1 ? '' : 'none';
  }
  // Sort table
  resp.sort(function(a,b) {
    var na = a.INSEE+a.CommuneAbsorbee+a.Section+a.Numero;
    var nb = b.INSEE+b.CommuneAbsorbee+b.Section+b.Numero;
    return na===nb ? 0 : na<nb ? -1 : 1;
  });
  // Show list
  var n = this.get('pageSize');
  for (var i=0, r; r = resp[i]; i++) {
    var li = document.createElement("LI");
    li.setAttribute("data-search", i);
    if (n>0) li.classList.add("ol-list-"+Math.floor(i/n));
    this._listParc.push(r);
    li.addEventListener("click", function(e) {
      self._handleParcelle(self._listParc[e.currentTarget.getAttribute("data-search")]);
    });
    li.innerHTML = r.INSEE+r.CommuneAbsorbee+r.Section+r.Numero;
    ul.appendChild(li);
    //
    if (n>0 && !(i%n)) {
      li = document.createElement("LI");
      li.innerText = Math.floor(i/n);
      li.addEventListener("click", function(e) {
        showPage(e.currentTarget.innerText);
      });
      page.appendChild(li);
    }
  }
  if (n>0) showPage(0);
};
/**
 * Handle parcelle section
 * @param {*} parc 
 * @private
 */
ol.control.SearchGeoportailParcelle.prototype._handleParcelle = function(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 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 Nominatim API, default "https://nominatim.openstreetmap.org/search"
 * @see {@link https://wiki.openstreetmap.org/wiki/Nominatim}
 */
ol.control.SearchNominatim = function(options) {
  options = options || {};
  options.className = options.className || 'nominatim';
  options.typing = options.typing || 500;
  options.url = options.url || 'https://nominatim.openstreetmap.org/search';
  options.copy = '<a href="http://www.openstreetmap.org/copyright" target="new">&copy; OpenStreetMap contributors</a>';
  ol.control.SearchJSON.call(this, options);
  this.set('polygon', options.polygon);
  this.set('viewbox', options.viewbox);
  this.set('bounded', options.bounded);
};
ol.ext.inherits(ol.control.SearchNominatim, ol.control.SearchJSON);
/** 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
*/
ol.control.SearchNominatim.prototype.getTitle = function (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
 */
ol.control.SearchNominatim.prototype.requestData = function (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
*/
ol.control.SearchNominatim.prototype.select = function (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 });
};
/**
 * 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
 */
ol.control.SearchJSON.prototype.handleResponse = function (response) {
  return response.results || response;
};
/** Reverse geocode
 * @param {ol.coordinate} coord
 * @api
 */
ol.control.SearchNominatim.prototype.reverseGeocode = function (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)
  );
};

/*	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 = function(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>';
  ol.control.SearchJSON.call(this, options);
  this.set('lang', options.lang);
};
ol.ext.inherits(ol.control.SearchWikipedia, ol.control.SearchJSON);
/** 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
*/
ol.control.SearchWikipedia.prototype.getTitle = function (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, ...)
 */
ol.control.SearchWikipedia.prototype.setLang = function (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
 */
ol.control.SearchWikipedia.prototype.requestData = function (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
 */
ol.control.SearchWikipedia.prototype.handleResponse = function (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
*/
ol.control.SearchWikipedia.prototype.select = function (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 = function(options) {
  var self = this;
  if (!options) options = {};
  // Container
  var div = options.content = document.createElement("div");
  // 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');
  ol.control.SelectBase.call(this, options);
  // Add button
  ol.ext.element.create('BUTTON', {
    className: 'ol-append',
    html: options.addLabel	|| 'add rule',
    click: function(){
      self.addCondition();
    },
    parent: div
  });
  this._conditions = [];
  this.set('attrPlaceHolder', options.attrPlaceHolder || 'attribute');
  this.set('valuePlaceHolder', options.valuePlaceHolder || 'value');
  this.addCondition();
};
ol.ext.inherits(ol.control.Select, ol.control.SelectBase);
/** Add a new condition
 * @param {*} options
 * 	@param {string} options.attr attribute name
 * 	@param {string} options.op	operator
 * 	@param {string} options.val attribute value
 */
ol.control.Select.prototype.addCondition = function (options) {
  options = options || {};
  this._conditions.push({
    attr: options.attr || '',
    op: options.op || '=',
    val: options.val || ''
  });
  this._drawlist();
};
/** Get the condition list
 */
ol.control.Select.prototype.getConditions = function () {
  return {
    usecase: this._useCase.checked,
    all: this._all.checked,
    conditions: this._conditions
  }
};
/** Set the condition list
 */
ol.control.Select.prototype.setConditions = function (cond) {
  this._useCase.checked = cond.usecase;
  this._all.checked = cond.all;
  this._conditions = cond.conditions;
  this._drawlist();
};
/** Get the conditions as string
 */
ol.control.Select.prototype.getConditionsString = function (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
 */
ol.control.Select.prototype._drawlist = function () {
  this._ul.innerHTML = '';
  for (var i=0; i < this._conditions.length; i++) {
    this._ul.appendChild(this._getLiCondition(i));
  }
};
/** Get a line
 * @return {*}
 * @private
 */
ol.control.Select.prototype._autocomplete = function (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
 */
ol.control.Select.prototype._getLiCondition = function (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
 */
ol.control.Select.prototype.removeCondition = function (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
 */
ol.control.Select.prototype.doSelect = function (options) {
  options = options || {};
  options.useCase = options.useCase || this._useCase.checked;
  options.matchAll = options.matchAll || this._all.checked;
  options.conditions = options.conditions || this._conditions
  return ol.control.SelectBase.prototype.doSelect.call(this, 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 = function(options) {
  if (!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
    });
  }
  // Input div
  this._input = ol.ext.element.create('DIV', {
    parent: div
  });
  options.className = options.className || 'ol-select-check';
  ol.control.SelectBase.call(this, options);
  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();
  }
};
ol.ext.inherits(ol.control.SelectCheck, ol.control.SelectBase);
/**
* Set the map instance the control associated with.
* @param {o.Map} map The map instance.
*/
ol.control.SelectCheck.prototype.setMap = function(map) {
  ol.control.SelectBase.prototype.setMap.call(this, map);
  this.setValues();
};
/** Select features by attributes
 */
ol.control.SelectCheck.prototype.doSelect = function(options) {
  console.log('select')
  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 ol.control.SelectBase.prototype.doSelect.call(this, { 
      features: options.features, 
      matchAll: this._selectAll 
    });
  } else {
    return ol.control.SelectBase.prototype.doSelect.call(this, {
      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
 */
ol.control.SelectCheck.prototype.setValues = function(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 = function(options) {
  if (!options) options = {};
  // Container
  var div = options.content = ol.ext.element.create('DIV');
  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
  });
  options.className = options.className || 'ol-select-condition';
  ol.control.SelectBase.call(this, options);
  this.setCondition(options.condition);
  this._selectAll = options.selectAll;
  this._onchoice = options.onchoice;
};
ol.ext.inherits(ol.control.SelectCondition, ol.control.SelectBase);
/** 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
 */
ol.control.SelectCondition.prototype.setCondition = function(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
 */
ol.control.SelectCondition.prototype.addCondition = function(condition) {
  this._conditions.push(condition);
};
/** Select features by condition
 */
ol.control.SelectCondition.prototype.doSelect = function(options) {
  options = options || {};
  var conditions = this._conditions;
  if (!this._check.checked) {
    return ol.control.SelectBase.prototype.doSelect.call(this, { features: options.features, matchAll: this._selectAll });
  } else {
    return ol.control.SelectBase.prototype.doSelect.call(this, {
      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 = function(options) {
  if (!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
    });
  }
  this._input = ol.ext.element.create('INPUT', {
    placeHolder: options.placeHolder || 'search...',
    change: function() {
      if (this._onchoice) this._onchoice();
    }.bind(this),
    parent: div
  });
  ol.control.SelectBase.call(this, options);
  this._onchoice = options.onchoice;
  this.set('property', options.property || 'name');
};
ol.ext.inherits(ol.control.SelectFulltext, ol.control.SelectBase);
/** Select features by condition
 */
ol.control.SelectFulltext.prototype.doSelect= function(options) {
  options = options || {};
  return ol.control.SelectBase.prototype.doSelect.call(this, {
    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 = function(options) {
  if (!options) options = {};
  // Container
  options.content = ol.ext.element.create('DIV');
  this._container = ol.ext.element.create('UL', {
    parent: options.content
  });
  options.className = options.className || 'ol-select-multi';
  ol.control.SelectBase.call(this, options);
  this._controls = [];
  options.controls.forEach(this.addControl.bind(this));
};
ol.ext.inherits(ol.control.SelectMulti, ol.control.SelectBase);
/**
* Set the map instance the control associated with.
* @param {o.Map} map The map instance.
*/
ol.control.SelectMulti.prototype.setMap = function(map) {
  if (this.getMap()) {
    this._controls.forEach(function(c) {
      this.getMap().remveControl(c);
    }.bind(this));
  }
  ol.control.SelectBase.prototype.setMap.call(this, map);
  if (this.getMap()) {
    this._controls.forEach(function(c) {
      this.getMap().addControl(c);
    }.bind(this));
  }
};
/** Add a new control
 * @param {ol.control.SelectBase} c
 */
ol.control.SelectMulti.prototype.addControl = function(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>}
 */
ol.control.SelectMulti.prototype.getControls = function() {
  return this._controls;
};
/** Select features by condition
 */
ol.control.SelectMulti.prototype.doSelect = function() {
  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 = function(options) {
  if (!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
    });
  }
  this._input = ol.ext.element.create('SELECT', {
    on: { change: function () { 
      if (this._onchoice) this._onchoice();
      else this.doSelect();
    }.bind(this) },
    parent: div
  });
  options.className = options.className || 'ol-select-popup';
  ol.control.SelectBase.call(this, options);
  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();
};
ol.ext.inherits(ol.control.SelectPopup, ol.control.SelectBase);
/**
* Set the map instance the control associated with.
* @param {o.Map} map The map instance.
*/
ol.control.SelectPopup.prototype.setMap = function(map) {
  ol.control.SelectBase.prototype.setMap.call(this, map);
  this.setValues();
};
/** Select features by attributes
 */
ol.control.SelectPopup.prototype.doSelect = function(options) {
  options = options || {};
  if (!this._input.value) {
    return ol.control.SelectBase.prototype.doSelect.call(this, { features: options.features, matchAll: this._selectAll });
  } else {
    return ol.control.SelectBase.prototype.doSelect.call(this, {
      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
 */
ol.control.SelectPopup.prototype.setValues = function(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 = function(options) {
  options = options || {};
  // New element
  var element = ol.ext.element.create('DIV', {
    className: (options.className || '') + ' ol-status'
      + (options.target ? '': ' ol-unselectable ol-control')
  });
  // Initialize
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  this.setVisible(options.visible!==false);
  if (options.position) this.setPosition(options.position);
  this.status(options.status || '');
};
ol.ext.inherits(ol.control.Status, ol.control.Control);
/** Set visiblitity 
 * @param {boolean} visible
 */
ol.control.Status.prototype.setVisible = function(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
 */
ol.control.Status.prototype.status = function(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
 */
ol.control.Status.prototype.setPosition = function(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
 */
ol.control.Status.prototype.show = function(show) {
  if (show===false) ol.ext.element.hide(this.element);
  else ol.ext.element.show(this.element);
};
/** Hide the status
 */
ol.control.Status.prototype.hide = function() {
  ol.ext.element.hide(this.element);
};
/** Toggle the status
 */
ol.control.Status.prototype.toggle = function() {
  ol.ext.element.toggle(this.element);
};
/** Is status visible
 */
ol.control.Status.prototype.isShown = function() {
  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
 *	@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 = function(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'),
  });
  this.content = ol.ext.element.create('DIV', {
    parent: element
  });
  // Initialize
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  // Make a scroll div
  ol.ext.element.scrollDiv (this.content, {
    vertical: true,
    mousewheel: true,
    minibar: options.minibar
  });
  this.setStory(options.html);
};
ol.ext.inherits(ol.control.Storymap, ol.control.Control);
/** Scroll to a chapter
 * @param {string} name Name of the chapter to scroll to
 */
ol.control.Storymap.prototype.setChapter = function (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
 */
ol.control.Storymap.prototype.setStory = function (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} options.layers layer to swipe
 *  @param {ol.layer} options.rightLayer layer to swipe on right side
 *  @param {string} options.className control class name
 *  @param {number} options.position position propertie of the swipe [0,1], default 0.5
 *  @param {string} options.orientation orientation propertie (vertical|horizontal), default vertical
 */
ol.control.Swipe = function(options) {
  options = options || {};
  var button = document.createElement('button');
  var element = document.createElement('div');
  element.className = (options.className || "ol-swipe") + " ol-unselectable ol-control";
  element.appendChild(button);
  element.addEventListener("mousedown", this.move.bind(this));
  element.addEventListener("touchstart", this.move.bind(this));
  ol.control.Control.call(this, {
    element: element
  });
  // 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');
};
ol.ext.inherits(ol.control.Swipe, ol.control.Control);
/**
 * Set the map instance the control associated with.
 * @param {_ol_Map_} map The map instance.
 */
ol.control.Swipe.prototype.setMap = function(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 */ }
  }
  ol.control.Control.prototype.setMap.call(this, 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
*/
ol.control.Swipe.prototype.isLayer_ = function(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.
 */
ol.control.Swipe.prototype.addLayer = function(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
 */
ol.control.Swipe.prototype.removeLayers = function() {
  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
 */
ol.control.Swipe.prototype.removeLayer = function(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}
 */
ol.control.Swipe.prototype.getRectangle = function() {
  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
 */
ol.control.Swipe.prototype.move = function(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
 */
ol.control.Swipe.prototype._transformPt = function(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
 */
ol.control.Swipe.prototype._drawRect = function(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
*/
ol.control.Swipe.prototype.precomposeLeft = function(e) {
  var ctx = e.context;
  if (ctx instanceof WebGLRenderingContext) {
    if (e.type === 'prerender') {
      // Clear
      ctx.clearColor(0, 0, 0, 0);
      ctx.clear(ctx.COLOR_BUFFER_BIT);
      // Clip
      ctx.enable(ctx.SCISSOR_TEST);
      var mapSize = this.getMap().getSize(); // [width, height] in CSS pixels
      // get render coordinates and dimensions given CSS coordinates
      var bottomLeft = this._transformPt(e, [0, mapSize[1]]);
      var topRight = this._transformPt(e, [mapSize[0], 0]);
      var width = topRight[0] - bottomLeft[0];
      var height = topRight[1] - bottomLeft[1];
      if (this.get('orientation') === "vertical") {
        width = Math.round(width * this.get('position'));
      } else {
        height = Math.round(height * this.get('position'));
        bottomLeft[1] += mapSize[1] - height;
      }
      ctx.scissor(bottomLeft[0], bottomLeft[1], width, height); 
    }
  } else {
    var size = e.frameState.size;
    ctx.save();
    ctx.beginPath();
    var pts = [[0,0],[size[0],size[1]]];
    if (this.get('orientation') === "vertical") {
      pts[1] = [
        size[0]*.5 + this.getMap().getSize()[0] * (this.get('position') - .5), 
        size[1]
      ];
    } else {
      pts[1] = [
        size[0],
        size[1]*.5 + this.getMap().getSize()[1] * (this.get('position') - .5)
      ];
    }
    this._drawRect(e, pts);
    ctx.clip();
  }
};
/** @private
*/
ol.control.Swipe.prototype.precomposeRight = function(e) {
  var ctx = e.context;
  if (ctx instanceof WebGLRenderingContext) {
    if (e.type === 'prerender') {
      // Clear
      ctx.clearColor(0, 0, 0, 0);
      ctx.clear(ctx.COLOR_BUFFER_BIT);
      // Clip
      ctx.enable(ctx.SCISSOR_TEST);
      var mapSize = this.getMap().getSize(); // [width, height] in CSS pixels
      // get render coordinates and dimensions given CSS coordinates
      var bottomLeft = this._transformPt(e, [0, mapSize[1]]);
      var topRight = this._transformPt(e, [mapSize[0], 0]);
      var width = topRight[0] - bottomLeft[0];
      var height = topRight[1] - bottomLeft[1];
      if (this.get('orientation') === "vertical") {
        width = Math.round(width * (1-this.get('position')));
        bottomLeft[0] += mapSize[0] - width;
      } else {
        height = Math.round(height * (1-this.get('position')));
        }
      ctx.scissor(bottomLeft[0], bottomLeft[1], width, height); 
    }
  } else {
    var size = e.frameState.size;
    ctx.save();
    ctx.beginPath();
    var pts = [[0,0],[size[0],size[1]]];
    if (this.get('orientation') === "vertical") {
      pts[0] = [
        size[0]*.5 + this.getMap().getSize()[0] * (this.get('position') - .5), 
        0
      ];
    } else {
      pts[0] = [
        0,
        size[1]*.5 + this.getMap().getSize()[1] * (this.get('position') - .5)
      ]
    }
    this._drawRect(e, pts);
    ctx.clip();
  }
};
/** @private
*/
ol.control.Swipe.prototype.postcompose = function(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 {ol.layer} options.layers layer to swipe
 *  @param {ol.layer} options.rightLayer layer to swipe on right side
 *  @param {string} options.className control class name
 *  @param {number} options.position position propertie of the swipe [0,1], default 0.5
 *  @param {string} options.orientation orientation propertie (vertical|horizontal), default vertical
 */
ol.control.SwipeMap = function(options) {
  options = options || {};
  var button = document.createElement('button');
  var element = document.createElement('div');
  element.className = (options.className || "ol-swipe") + " ol-unselectable ol-control";
  element.appendChild(button);
  element.addEventListener("mousedown", this.move.bind(this));
  element.addEventListener("touchstart", this.move.bind(this));
  ol.control.Control.call(this, {
    element: element
  });
  this.on('propertychange', function(e) {
    if (this.get('orientation') === "horizontal") {
      this.element.style.top = this.get('position')*100+"%";
      this.element.style.left = "";
    } else {
      if (this.get('orientation') !== "vertical") this.set('orientation', "vertical");
      this.element.style.left = this.get('position')*100+"%";
      this.element.style.top = "";
    }
    if (e.key === 'orientation') {
      this.element.classList.remove("horizontal", "vertical");
      this.element.classList.add(this.get('orientation'));
    }
    this._clip();
  }.bind(this));
  this.on('change:active', this._clip.bind(this));
  this.set('position', options.position || 0.5);
  this.set('orientation', options.orientation || 'vertical');
  this.set('right', options.right);
};
ol.ext.inherits(ol.control.SwipeMap, ol.control.Control);
/** Set the map instance the control associated with.
 * @param {ol.Map} map The map instance.
 */
ol.control.SwipeMap.prototype.setMap = function(map) {
  if (this.getMap()) {
    if (this._listener) ol.Observable.unByKey(this._listener);
    var layerDiv = this.getMap().getViewport().querySelector('.ol-layers');
    layerDiv.style.clip = ''; 
  }
  ol.control.Control.prototype.setMap.call(this, map);
  if (map) {
    this._listener = map.on('change:size', this._clip.bind(this));
  }
};
/** Clip
 * @private
 */
ol.control.SwipeMap.prototype._clip = function() {
  if (this.getMap()) {
    var layerDiv = this.getMap().getViewport().querySelector('.ol-layers');
    var rect = this.getRectangle();
    layerDiv.style.clip = 'rect(' 
      + rect[1]+'px,' // top
      + rect[2]+'px,' // right
      + rect[3]+'px,' // bottom
      + rect[0]+'px'  //left
      + ')';
  }
};
/** Get visible rectangle
 * @returns {ol.extent}
 */
ol.control.SwipeMap.prototype.getRectangle = function() {
  var s = this.getMap().getSize();
  if (this.get('orientation') === 'vertical') {
    if (this.get('right')) {
      return [ s[0]*this.get('position'), 0, s[0], s[1]];
    } else {
      return [ 0, 0, s[0]*this.get('position'), s[1]];
    }
  } else {
    if (this.get('right')) {
      return [ 0, s[1]*this.get('position'), s[0], s[1]];
    } else {
      return [ 0, 0, s[0], s[1]*this.get('position')];
    }
  }
};
/** @private
*/
ol.control.SwipeMap.prototype.move = function(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 = function(options) {
  options = options || {};
	this.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 (!(this.style instanceof Array)) this.style = [this.style];
	this.composite = options.composite || '';
	var div = document.createElement('div');
	div.className = "ol-target ol-unselectable ol-control";
	ol.control.CanvasBase.call(this, {
    element: div,
		target: options.target
	});
	this.setVisible(options.visible!==false);
};
ol.ext.inherits(ol.control.Target, ol.control.CanvasBase);
/** Set the control visibility
 * @paraam {boolean} b 
 */
ol.control.Target.prototype.setVisible = function (b) {
  this.set("visible",b);
	if (this.getMap()) {
    try { this.getMap().renderSync(); } catch(e) { /* ok */ }
  }
};
/** Get the control visibility
 * @return {boolean} b 
 */
ol.control.Target.prototype.getVisible = function () {
  return this.get("visible");
};
/** Draw the target
 * @private
 */
ol.control.Target.prototype._draw = function (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: [ratio,0,0,ratio,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 = function(options)
{	options = options || {};
    options.className = (options.className||"") + " ol-text-button";
    ol.control.Button.call(this, options);
};
ol.ext.inherits(ol.control.TextButton, ol.control.Button);

/*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 = function(options) {
  var element = ol.ext.element.create('DIV', {
    className: (options.className || '') + ' ol-timeline'
      + (options.target ? '': ' ol-unselectable ol-control')
      + (options.zoomButton ? ' ol-hasbutton':'')
  });
  // Initialize
  ol.control.Control.call(this, {
    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);
};
ol.ext.inherits(ol.control.Timeline, ol.control.Control);
/**
 * 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.
 */
ol.control.Timeline.prototype.setMap = function(map) {
  ol.control.Control.prototype.setMap.call(this, 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
 */
ol.control.Timeline.prototype.addButton = function(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
 */
ol.control.Timeline.prototype.setInterval = function(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
 */
ol.control.Timeline.prototype._getHTML = function(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
 */
ol.control.Timeline.prototype._getFeatureDate = function(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
 */
ol.control.Timeline.prototype._endFeatureDate = function(/* feature */) {
  return undefined;
};
/** Is the line collapsed
 * @return {boolean}
 */
ol.control.Timeline.prototype.isCollapsed = function() {
  return this.element.classList.contains('ol-collapsed');
};
/** Collapse the line
 * @param {boolean} b
 */
ol.control.Timeline.prototype.collapse = function(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
 */
ol.control.Timeline.prototype.toggle = function() {
  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
 */
ol.control.Timeline.prototype.setFeatures = function(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>}
 */
ol.control.Timeline.prototype.getFeatures = function() {
  return this._features || this._source.getFeatures();
}
/**
 * Refresh the timeline with new data
 * @param {Number} zoom Zoom factor from 0.25 to 10, default 1
 */
ol.control.Timeline.prototype.refresh = function(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
 */
ol.control.Timeline.prototype._getOffsetFromDate = function(date) {
  return (date - this._minDate) * this._scale;
};
/** Get date given an offset
 * @param {Date} date
 * @return {number}
 * @private
 */
ol.control.Timeline.prototype._getDateFromOffset = function(offset) {
  return offset / this._scale + this._minDate;
};
/** Set the current position 
 * @param {number} scrollLeft current position (undefined when scrolling)
 * @returns {number}
 * @private
 */
ol.control.Timeline.prototype._setScrollLeft = function(scrollLeft) {
  this._scrollLeft = scrollLeft;
  if (scrollLeft !== undefined) {
    this._scrollDiv.scrollLeft = scrollLeft;
  }
};
/** Get the current position 
 * @returns {number}
 * @private
 */
ol.control.Timeline.prototype._getScrollLeft = function() {
  // Unset when scrolling
  if (this._scrollLeft === undefined) {
    return this._scrollDiv.scrollLeft;
  } else {
    // St by user
    return this._scrollLeft;
  }
};
/**
 * Draw dates on line
 * @private
 */
ol.control.Timeline.prototype._drawTime = function(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
 */
ol.control.Timeline.prototype.setDate = function(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}
 */
ol.control.Timeline.prototype.roundDate = function(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}
 */
ol.control.Timeline.prototype.getDate = function(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
 */
ol.control.Timeline.prototype._roundTo = function(d, r) {
  return Math.round(d/r) * r;
};
/** Get the start date of the control
 * @return {Date}
 */
ol.control.Timeline.prototype.getStartDate = function() {
  return new Date(this.get('minDate'));
}
/** Get the end date of the control
 * @return {Date}
 */
ol.control.Timeline.prototype.getEndDate = function() {
  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 = function(options) {
  if (!options) options = {};
  var element = ol.ext.element.create('DIV', {
    className: (options.className || 'ol-videorec') + ' ol-unselectable ol-control'
  });
  // buttons
  ol.ext.element.create('BUTTON', {
    type: 'button',
    className: 'ol-start',
    title: 'start',
    click: function() { 
      this.start();
    }.bind(this),
    parent: element
  });
  ol.ext.element.create('BUTTON', {
    type: 'button',
    className: 'ol-stop',
    title: 'stop',
    click: function() { 
      this.stop();
    }.bind(this),
    parent: element
  });
  ol.ext.element.create('BUTTON', {
    type: 'button',
    className: 'ol-pause',
    title: 'pause',
    click: function() { 
      this.pause();
    }.bind(this),
    parent: element
  });
  ol.ext.element.create('BUTTON', {
    type: 'button',
    className: 'ol-resume',
    title: 'resume',
    click: function() {
      this.resume();
    }.bind(this),
    parent: element
  });
  // Start
  ol.control.Control.call(this, {
    element: element,
    target: options.target
  });
  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')
  });
}
ol.ext.inherits(ol.control.VideoRecorder, ol.control.Control);
/**
 * 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
 */
ol.control.VideoRecorder.prototype.setMap = function (map) {
  if (this.getMap()) {
    this.getMap().removeControl(this._printCtrl);
    if (this._dialog) this.getMap().removeControl(this._dialog);
  }
  ol.control.Control.prototype.setMap.call(this, map);
  if (this.getMap()) {
    this.getMap().addControl(this._printCtrl);
    if (this._dialog) this.getMap().addControl(this._dialog);
  }
};
/** Start recording */
ol.control.VideoRecorder.prototype.start = function () {
  var print = this._printCtrl;
  var stop = false;
  function capture(canvas) {
    if (stop) return;
    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 */
ol.control.VideoRecorder.prototype.stop = function () {
  if (this._mediaRecorder) {
    this._mediaRecorder.stop();
    this._mediaRecorder = null;
    this.element.setAttribute('data-state', 'inactive');
  }
};
/** Pause recording */
ol.control.VideoRecorder.prototype.pause = function () {
  if (this._mediaRecorder) {
    this._mediaRecorder.pause();
    this.element.setAttribute('data-state', 'pause');
  }
};
/** Resume recording after pause */
ol.control.VideoRecorder.prototype.resume = function () {
  if (this._mediaRecorder) {
    this._mediaRecorder.resume();
    this.element.setAttribute('data-state', 'rec');
  }
};

/* 
  Using WMS Layer with EPSG:4326 projection.
  The tiles will be reprojected to map pojection (EPSG:3857).
  NB: reduce tileSize to minimize deformations on small scales.
*/
/** WMSCapabilities
 * @constructor
 * @fires load
 * @fires capabilities
 * @extends {ol.control.Button}
 * @param {*} options
 *  @param {string|Element} [options.target] the target to set the dialog, use document.body to have fullwindow dialog
 *  @param {string} [options.proxy] proxy to use when requesting Getcapabilites, default none (suppose the service use CORS)
 *  @param {string} [options.placeholder='service url...'] input placeholder, default 'service url...'
 *  @param {string} [options.title=WMS] dialog title, default 'WMS'
 *  @param {string} [options.searchLabel='search'] Label for search button, default 'search'
 *  @param {string} [options.loadLabel='load'] Label for load button, default 'load'
 *  @param {Array<string>} [options.srs] an array of supported srs, default map projection code or 'EPSG:3857'
 *  @param {number} [options.timeout=1000] Timeout for getCapabilities request, default 1000
 *  @param {boolean} [options.cors=false] Use CORS, default false
 *  @param {string} [options.optional] a list of optional url properties (when set in the request url), separated with ','
 *  @param {boolean} [options.trace=false] Log layer info, default false
 *  @param {*} [options.services] a key/url object of services for quick access in a menu
 */
ol.control.WMSCapabilities = function (options) {
  options = options || {};
  var buttonOptions = Object.assign({}, options || {});
  this._proxy = options.proxy;
  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 () {
      this.showDialog();
    }.bind(this)
  }
  ol.control.Button.call(this, buttonOptions);
  // WMS options
  this.set('srs', options.srs || []);
  this.set('cors', options.cors);
  this.set('trace', options.trace);
  this.set('title', options.title);
  this.set('loadLabel', options.loadLabel);
  this.set('optional', options.optional);
  // Dialog
  this.createDialog(options);
  // Default version
  this._elements.formVersion.value = '1.0.0';
  // Ajax request
  var parser = this._getParser();
  this._ajax = new ol.ext.Ajax({ dataType:'text', auth: options.authentication });
  this._ajax.on('success', function (evt) {
    var caps;
    try {
      caps = parser.read(evt.response);
    } catch (e) {
      this.showError({ type: 'load', error: e });
    }
    if (caps) {
      if (!caps.Capability.Layer.Layer) {
        this.showError({ type: 'noLayer' });
      } else {
        this.showCapabilities(caps);
      }
    } 
    this.dispatchEvent({ type: 'capabilities', capabilities: caps });
    if (typeof(evt.options.callback) === 'function') evt.options.callback(caps);
  }.bind(this));
  this._ajax.on('error', function(evt) {
    this.showError({ type: 'load', error: evt });
    this.dispatchEvent({ type: 'capabilities' });
    if (typeof(evt.options.callback) === 'function') false;
  }.bind(this));
  // Handle waiting
  this._ajax.on('loadstart', function() {
    this._elements.element.classList.add('ol-searching');
  }.bind(this));
  this._ajax.on('loadend', function() {
    this._elements.element.classList.remove('ol-searching');
  }.bind(this));
  // Load a layer
  if (options.onselect) {
    this.on('load', function(e) { 
      options.onselect(e.layer, e.options); 
    });
  }
};
ol.ext.inherits(ol.control.WMSCapabilities, ol.control.Button);
/** Get service parser
 */
ol.control.WMSCapabilities.prototype._getParser = function() {
  return  new ol.format.WMSCapabilities();
};
/** 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:',
};
/** Create dialog
 * @private
 */
ol.control.WMSCapabilities.prototype.createDialog = function (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),
        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
 */
ol.control.WMSCapabilities.prototype.getLayerFromOptions = function (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.
 */
ol.control.WMSCapabilities.prototype.setMap = function (map) {
  ol.control.Button.prototype.setMap.call(this, map);
  if (this._dialog) this._dialog.setMap(map);
};
/** Get the dialog
 * @returns {ol.control.Dialog}
 */
ol.control.WMSCapabilities.prototype.getDialog = function() {
  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
 */
ol.control.WMSCapabilities.prototype.showDialog = function(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
 */
ol.control.WMSCapabilities.prototype.testUrl = function(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
 */
ol.control.WMSCapabilities.prototype.getRequestParam = function(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
 */
ol.control.WMSCapabilities.prototype.getCapabilities = function(url, options) {
  if (!url) return;
  if (!this.testUrl(url)) {
    this.showError({
      type: 'badUrl'
    })
    return;
  }
  options = options || {};
  // Extract map attributes
  url = url.split('?');
  var search = url[1];
  url = url[0];
  // reset
  this._elements.formMap.value = '';
  this._elements.formLayer.value = '';
  this._elements.formStyle.value = '';
  this._elements.formTitle.value = '';
  this._elements.formProjection.value = this.getMap().getView().getProjection().getCode();
  this._elements.formFormat.selectedIndex = 0;
  var map = options.map || '';
  var optional = {};
  if (search) {
    search = search.replace(/^\?/,'').split('&');
    search.forEach(function(s) {
      s = s.split('=');
      s[1] = decodeURIComponent(s[1] || '');
      if (/^map$/i.test(s[0])) {
        map = s[1];
        this._elements.formMap.value = map;
      }
      if (/^layers$/i.test(s[0])) {
        this._elements.formLayer.value = s[1];
        this._elements.formTitle.value = s[1].split(',')[0];
      }
      if (/^style$/i.test(s[0])) {
        this._elements.formStyle.value = s[1];
      }
      if (/^crs$/i.test(s[0])) {
        this._elements.formProjection.value = s[1];
      }
      if (/^format$/i.test(s[0])) {
        for (var o,i=0; o=this._elements.formFormat.options[i]; i++) {
          if (o.value===s[1]) {
            this._elements.formFormat.selectedIndex = i;
            break;
          }
        }
      }
      // Check optionals
      if (this.get('optional')) {
        this.get('optional').split(',').forEach(function(o) {
          if (o === s[0]) {
            optional[o] = s[1];
          }
        }.bind(this))
      }
    }.bind(this))
  }
  // Get request params
  var request = this.getRequestParam(options);
  var opt = [];
  if (map) {
    request.MAP = map;
    opt.push('map='+map);
  }
  for (var o in optional) {
    request[o] = optional[o];
    opt.push(o+'='+optional[o]);
  }
  // Fill form
  this._elements.input.value = (url || '') + (opt ? '?'+opt.join('&') : '');
  this.clearForm();
  // Sen drequest
  if (this._proxy) {
    var q = '';
    for (var r in request) q += (q?'&':'')+r+'='+request[r];
    this._ajax.send(this._proxy, {
      url: q
    }, {
      timeout: options.timeout || 10000,
      callback: options.onload,
      abort: false
    });
  } else {
    this._ajax.send(url, request, {
      timeout: options.timeout || 10000,
      callback: options.onload,
      abort: false
    });
  }
};
/** Display error
 * @param {*} error event
 */
ol.control.WMSCapabilities.prototype.showError = function(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
 */
ol.control.WMSCapabilities.prototype.clearForm = function() {
  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
 */
ol.control.WMSCapabilities.prototype.showCapabilities = function(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.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
 */
ol.control.WMSCapabilities.prototype.getLayerResolution = function(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
 */
ol.control.WMSCapabilities.prototype.getOptionsFromCap = function(caps, parent) {
  var formats = parent.Capability.Request.GetMap.Format;
  var format, i;
  // Look for prefered format first
  var pref = [/png/,/jpeg/,/gif/];
  for (i=0; i<3; i++) {
    for (var f=0; f<formats.length; f++) {
      if (pref[i].test(formats[f])) {
        format = formats[f];
        break;
      }
    }
    if (format) break;
  }
  if (!format) format = formats[0];  
  // Check srs
  var srs = this.getMap().getView().getProjection().getCode();
  this.showError();
  var crs = false;
  if (!caps.CRS) {
    crs = false;
  } else if (caps.CRS.indexOf(srs)>=0) {
    crs = true;
  } else if (caps.CRS.indexOf('EPSG:4326')>=0) {
    // try to set EPSG:4326 instead
    srs = 'EPSG:4326';
    crs = true;
  } else {
    this.get('srs').forEach(function(s) {
      if (caps.CRS.indexOf(s)>=0) {
        srs = s;
        crs = true;
      }
    })
  }
  if (!crs) {
    this.showError({ type:'srs' });
    if (this.get('trace')) console.log('BAD srs: ', caps.CRS);
  }
  var bbox = caps.EX_GeographicBoundingBox;
  //bbox = ol.proj.transformExtent(bbox, 'EPSG:4326', srs);
  if (bbox) bbox = ol.proj.transformExtent(bbox, 'EPSG:4326', this.getMap().getView().getProjection());
  var attributions = [];
  if (caps.Attribution) {
    attributions.push('<a href="'+encodeURI(caps.Attribution.OnlineResource)+'">&copy; '+caps.Attribution.Title.replace(/</g,'&lt;')+'</a>');
  }
  var layer_opt = {
    title: caps.Title,
    extent: bbox,
    queryable: caps.queryable,
    abstract: caps.Abstract,
    minResolution: this.getLayerResolution('min', caps),
    maxResolution: this.getLayerResolution('max', caps) || 156543.03392804097
  };
  var source_opt = {
    url: parent.Capability.Request.GetMap.DCPType[0].HTTP.Get.OnlineResource, //parent.Service.OnlineResource,
    projection: srs,
    attributions: attributions,
    crossOrigin: this.get('cors') ? 'anonymous' : null,
    params: {
      'LAYERS': caps.Name,
      'FORMAT': format,
      'VERSION': parent.version || '1.3.0'
    }
  }
  // Resolution to zoom
  var view = new ol.View({
    projection: this.getMap().getView().getProjection()
  })
  view.setResolution(layer_opt.minResolution);
  var maxZoom = Math.round(view.getZoom());
  view.setResolution(layer_opt.maxResolution);
  var minZoom = Math.round(view.getZoom());
  // Fill form
  this._fillForm({
    title: layer_opt.title,
    layers: source_opt.params.LAYERS,
    format: source_opt.params.FORMAT,
    minZoom: minZoom,
    maxZoom: maxZoom,
    extent: bbox ? bbox.join(',') : '',
    projection: source_opt.projection,
    attribution: source_opt.attributions[0] || '',
    version: source_opt.params.VERSION
  });
  // Trace
  if (this.get('trace')) {
    var tso = JSON.stringify([ source_opt ], null, "\t").replace(/\\"/g,'"');
    layer_opt.source = "SOURCE"; 
    var t = "new ol.layer.Tile (" +JSON.stringify(layer_opt, null, "\t")+ ")" 
    t = t.replace(/\\"/g,'"')
      .replace('"SOURCE"', "new ol.source.TileWMS("+tso+")")
      .replace(/\\t/g,"\t").replace(/\\n/g,"\n")
      .replace("([\n\t","(")
      .replace("}\n])","})");
    console.log(t);
    delete layer_opt.source;
  }
  // Legend ?
  var legend = [];
    if (caps.Style) {
    caps.Style.forEach(function(s) {
      if (s.LegendURL) {
        legend.push(s.LegendURL[0].OnlineResource);
      }
    });
  }
  return ({ 
    layer: layer_opt, 
    source: source_opt,
    data: {
      title: caps.Title,
      abstract: caps.Abstract,
      logo: caps.Attribution && caps.Attribution.LogoURL ? caps.Attribution.LogoURL.OnlineResource : undefined,
      keyword: caps.KeywordList,
      legend: legend,
      opaque: caps.opaque,
      queryable: caps.queryable
    } 
  });
};
/** Get WMS options from control form
 * @return {*} options
 * @private
 */
ol.control.WMSCapabilities.prototype._getFormOptions = function() {
  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
 */
ol.control.WMSCapabilities.prototype._fillForm = function(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)
 */
ol.control.WMSCapabilities.prototype.loadLayer = function(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)
  });
};

/** 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 = function (options) {
  options = options || {};
  options.title = options.title || 'WMTS';
  ol.control.WMSCapabilities.call(this, options);
  this.getDialog().element.classList.add('ol-wmtscapabilities');
};
ol.ext.inherits(ol.control.WMTSCapabilities, ol.control.WMSCapabilities);
/** Get service parser
 * @private
 */
ol.control.WMTSCapabilities.prototype._getParser = function() {
  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
 */
ol.control.WMTSCapabilities.prototype.getRequestParam = function(options) {
  return {
    SERVICE: 'WMTS',
    REQUEST: 'GetCapabilities',
    VERSION: options.version || '1.0.0'
  }
};
/** Get tile grid options only for EPSG:3857 projection
 * @returns {*}
 * @private
 */
ol.control.WMTSCapabilities.prototype._getTG = function(tileMatrixSet, minZoom, maxZoom) {
  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 = tileMatrixSet !== 'PM' ? 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
 * @returns {ol.tilegrid.WMTS}
 * @private
 */
ol.control.WMTSCapabilities.prototype.getTileGrid = function(tileMatrixSet, minZoom, maxZoom) {
  return new ol.tilegrid.WMTS(this._getTG(tileMatrixSet, minZoom, maxZoom));
};
/** Return a WMTS options for the given capabilities
 * @param {*} caps layer capabilities (read from the capabilities)
 * @param {*} parent capabilities
 * @return {*} options
 */
ol.control.WMTSCapabilities.prototype.getOptionsFromCap = function(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 (tm.TileMatrixSet === 'PM' || tm.TileMatrixSet === 'EPSG:3857') {
      tmatrix = tm;
      caps.TileMatrixSet = tm.TileMatrixSet;
    }
  });
  if (!tmatrix) {
    this.showError({ type: 'TileMatrix' });
    return;
  }
  tmatrix.TileMatrixSetLimits.forEach(function(tm) {
    var zoom = tm.TileMatrix.split(':').pop();
    minZoom = Math.min(minZoom, parseInt(zoom));
    maxZoom = Math.max(maxZoom, parseInt(zoom));
  });
  var view = new ol.View();
  view.setZoom(minZoom);
  var layer_opt = {
    title: caps.Title,
    extent: bbox,
    abstract: caps.Abstract,
    maxResolution: view.getResolution()
  };
  var source_opt = {
    url: parent.OperationsMetadata.GetTile.DCP.HTTP.Get[0].href,
    layer: caps.Identifier,
    matrixSet: caps.TileMatrixSet,
    format: caps.Format[0] || 'image/jpeg',
    projection: 'EPSG:3857',
    //tileGrid: tg,
    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), 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;
  }
  return ({ 
    layer: layer_opt, 
    source: source_opt,
    data: {
      title: caps.Title,
      abstract: caps.Abstract,
      legend: caps.Style ? [ caps.Style[0].LegendURL[0].href ] : undefined,
    } 
  });
};
/** Get WMS options from control form
 * @return {*} original original options 
 * @return {*} options
 * @private
 */
ol.control.WMTSCapabilities.prototype._getFormOptions = function() {
  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
 */
ol.control.WMTSCapabilities.prototype.getLayerFromOptions = function (options) {
  if (!options) return;
  options.source.tileGrid = this.getTileGrid(options.source.matrixSet, options.source.minZoom, options.source.maxZoom);
  options.layer.source = new ol.source.WMTS(options.source);
  var layer = new ol.layer.Tile(options.layer);
  // Restore options
  delete options.layer.source;
  delete options.source.tileGrid;
  return layer;
};

/*
  Copyright (c) 2016 Jean-Marc VIGLINO, 
  released under the CeCILL license (http://www.cecill.info/).
*/
/** Feature animation base class
 * Use the {@link ol.Map#animateFeature} or {@link ol.layer.Vector#animateFeature} to animate a feature
 * on postcompose in a map or a layer
* @constructor
* @fires animationstart
* @fires animating
* @fires animationend
* @param {ol.featureAnimationOptions} options
*	@param {Number} options.duration duration of the animation in ms, default 1000
*	@param {bool} options.revers revers the animation direction
*	@param {Number} options.repeat number of time to repeat the animation, default 0
*	@param {ol.style.Style} options.hiddenStyle a style to display the feature when playing the animation
*	  to be used to make the feature selectable when playing animation 
*	  (@see {@link ../examples/map.featureanimation.select.html}), default the feature 
*	  will be hidden when playing (and not selectable)
*	@param {ol.easing.Function} options.fade an easing function used to fade in the feature, default none
*	@param {ol.easing.Function} options.easing an easing function for the animation, default ol.easing.linear
*/
ol.featureAnimation = function(options) {
  options = options || {};
  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;
  ol.Object.call(this);
};
ol.ext.inherits(ol.featureAnimation, ol.Object);
/** Hidden style: a transparent style
 */
ol.featureAnimation.hiddenStyle = new ol.style.Style({ 
  image: new ol.style.Circle({}), 
  stroke: new ol.style.Stroke({ 
    color: 'transparent' 
  }) 
});
/** 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
*/
ol.featureAnimation.prototype.drawGeom_ = function (e, geom, shadow) {
  if (this.fade_) {
    e.context.globalAlpha = this.fade_(1-e.elapsed);
  }
  var style = e.style;
  for (var i=0; i<style.length; i++) {
    // Prevent crach if the style is not ready (image not loaded)
    try {
      var vectorContext = e.vectorContext || ol.render.getVectorContext(e);
      var s = ol.ext.getVectorContextStyle(e, style[i]);
      vectorContext.setStyle(s);
      if (s.getZIndex()<0) vectorContext.drawGeometry(shadow||geom);
      else vectorContext.drawGeometry(geom);
    } catch(e) { /* ok */ }
  }
};
/** Function to perform manipulations onpostcompose. 
 * This function is called with an ol.featureAnimationEvent argument.
 * The function will be overridden by the child implementation.    
 * Return true to keep this function for the next frame, false to remove it.
 * @param {ol.featureAnimationEvent} e
 * @return {bool} true to continue animation.
 * @api 
 */
ol.featureAnimation.prototype.animate = function (/* e */) {
  return false;
};
/** An animation controler object an object to control animation with start, stop and isPlaying function.    
 * To be used with {@link olx.Map#animateFeature} or {@link ol.layer.Vector#animateFeature}
 * @typedef {Object} animationControler
 * @property {function} start - start animation.
 * @property {function} stop - stop animation option arguments can be passed in animationend event.
 * @property {function} isPlaying - return true if animation is playing.
 */
/** Animate feature on a map
 * @function 
 * @param {ol.Feature} feature Feature to animate
 * @param {ol.featureAnimation|Array<ol.featureAnimation>} fanim the animation to play
 * @return {animationControler} an object to control animation with start, stop and isPlaying function
 */
ol.Map.prototype.animateFeature = function(feature, fanim) {
  // Get or create an animation layer associated with the map 
  var layer = this._featureAnimationLayer;
  if (!layer) {
    layer = this._featureAnimationLayer = new ol.layer.Vector({ source: new ol.source.Vector() });
    layer.setMap(this);
  }
  // Animate feature on this layer
  layer.getSource().addFeature(feature);
  var listener = fanim.on('animationend', function(e) {
    if (e.feature===feature) {
      // Remove feature on end
      layer.getSource().removeFeature(feature);
      ol.Observable.unByKey(listener);
    }
  });
  layer.animateFeature(feature, fanim);
};
/** Animate feature on a vector layer 
 * @fires animationstart, animationend
 * @param {ol.Feature} feature Feature to animate
 * @param {ol.featureAnimation|Array<ol.featureAnimation>} fanim the animation to play
 * @param {boolean} useFilter use the filters of the layer
 * @return {animationControler} an object to control animation with start, stop and isPlaying function
 */
ol.layer.Base.prototype.animateFeature = function(feature, fanim, useFilter) {
  var self = this;
  var listenerKey;
  // Save style
  var style = feature.getStyle();
  var flashStyle = style || (this.getStyleFunction ? this.getStyleFunction()(feature) : null);
  if (!flashStyle) flashStyle=[];
  if (!(flashStyle instanceof Array)) flashStyle = [flashStyle];
  // Structure pass for animating
  var event = {
    // Frame context
    vectorContext: null,
    frameState: null,
    start: 0,
    time: 0,
    elapsed: 0,
    extent: false,
    // Feature information
    feature: feature,
    geom: feature.getGeometry(),
    typeGeom: feature.getGeometry().getType(),
    bbox: feature.getGeometry().getExtent(),
    coord: ol.extent.getCenter(feature.getGeometry().getExtent()),
    style: flashStyle
  };
  if (!(fanim instanceof Array)) fanim = [fanim];
  // Remove null animations
  for (var i=fanim.length-1; i>=0; i--) {
    if (fanim[i].duration_===0) fanim.splice(i,1);
  }
  var nb=0, step = 0;
  // Filter availiable on the layer
  var filters = (useFilter && this.getFilters) ? this.getFilters() : [];
  function animate(e) {
    event.type = e.type;
    try {
      event.vectorContext = e.vectorContext || ol.render.getVectorContext(e);
    } catch(e) { /* nothing todo */ }
    event.frameState = e.frameState;
    event.inversePixelTransform = e.inversePixelTransform;
    if (!event.extent) {
      event.extent = e.frameState.extent;
      event.start = e.frameState.time;
      event.context = e.context;
    }
    event.time = e.frameState.time - event.start;
    event.elapsed = event.time / fanim[step].duration_;
    if (event.elapsed > 1) event.elapsed = 1;
    // Filter
    e.context.save();
    filters.forEach(function(f) {
      if (f.get('active')) f.precompose(e);
    });
    if (this.getOpacity) {
      e.context.globalAlpha = this.getOpacity();
    }
    // Stop animation?
    if (!fanim[step].animate(event)) {
      nb++;
      // Repeat animation
      if (nb < fanim[step].repeat_) {
        event.extent = false;
      } else if (step < fanim.length-1) {
        // newt step
        fanim[step].dispatchEvent({ type:'animationend', feature: feature });
        step++;
        nb=0;
        event.extent = false;
      } else {
        // the end
        stop();
      }
    } else {
      var animEvent = { 
        type: 'animating', 
        step: step,
        start: event.start,
        time: event.time,
        elapsed: event.elapsed,
        rotation: event.rotation||0,
        geom: event.geom,
        coordinate: event.coord,
        feature: feature 
      };
      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);
    // Send event
    var event = { type:'animationend', feature: feature };
    if (options) {
      for (var i in options) if (options.hasOwnProperty(i)) {
        event[i] = options[i]; 
      }
    }
    fanim[step].dispatchEvent(event);
    self.dispatchEvent(event);
  }
  // Launch animation
  function start(options) {
    if (fanim.length && !listenerKey) {
      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 event = { type:'animationstart', feature: feature };
      if (options) {
        for (var i in options) if (options.hasOwnProperty(i)) {
          event[i] = options[i]; 
        }
      }
      fanim[step].dispatchEvent(event);
      self.dispatchEvent(event);
    }
  }
  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 = function(options) {
  ol.featureAnimation.call(this, options);
  this.set('nb', options.nb || 10)
}
ol.ext.inherits(ol.featureAnimation.Blink, ol.featureAnimation);
/** Animate: Show or hide feature depending on the laptimes
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Blink.prototype.animate = function (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 = function(options)
{	options = options || {};
	ol.featureAnimation.call(this, options);
	this.amplitude_ = options.amplitude || 40;
	this.bounce_ = -Math.PI*(options.bounce || 3);
}
ol.ext.inherits(ol.featureAnimation.Bounce, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Bounce.prototype.animate = function (e)
{	// Animate
	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 = function(options)
{	options = options || {};
	this.speed_ = options.speed || 0;
	ol.featureAnimation.call(this, options);
	this.side_ = options.side || 'top';
}
ol.ext.inherits(ol.featureAnimation.Drop, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Drop.prototype.animate = function (e)
{	// First time > calculate duration / speed
	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 = function(options)
{	options = options || {};
	this.speed_ = options.speed || 0;
	ol.featureAnimation.call(this, options);
}
ol.ext.inherits(ol.featureAnimation.Fade, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Fade.prototype.animate = function (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 = function(options)
{	ol.featureAnimation.call(this, options);
};
ol.ext.inherits(ol.featureAnimation.None, ol.featureAnimation);
/** Animate: do nothing during the laps time
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.None.prototype.animate = function (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 = function() {
  ol.featureAnimation.call(this, { duration:0 });
};
ol.ext.inherits(ol.featureAnimation.Null, ol.featureAnimation);

/*
  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 = function(options){
  options = options || {};
  ol.featureAnimation.call(this, 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_;
}
ol.ext.inherits(ol.featureAnimation.Path, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Path.prototype.animate = function (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) {
      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 = function(options)
{	options = options || {};
	ol.featureAnimation.call(this, 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;
}
ol.ext.inherits(ol.featureAnimation.Shake, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Shake.prototype.animate = function (e)
{	// Animate
	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 = function(options) {
  ol.featureAnimation.call(this, options);
}
ol.ext.inherits(ol.featureAnimation.Show, ol.featureAnimation);
/** Animate: just show the object during the laps time
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Show.prototype.animate = function (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 = function(options)
{	options = options || {};
	this.speed_ = options.speed || 0;
	ol.featureAnimation.call(this, options);
	this.side_ = options.side || 'left';
}
ol.ext.inherits(ol.featureAnimation.Slide, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Slide.prototype.animate = function (e)
{	// First time > calculate duration / speed
	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
 * @constructor
 * @extends {ol.featureAnimation}
 * @param {ol.featureAnimationOptions} options
 */
ol.featureAnimation.Teleport = function(options)
{	ol.featureAnimation.call(this, options);
}
ol.ext.inherits(ol.featureAnimation.Teleport, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Teleport.prototype.animate = function (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 = function(options)
{	options = options || {};
	ol.featureAnimation.call(this, options);
	this.speed_ = options.speed || 0;
	this.side_ = options.side || 'left';
}
ol.ext.inherits(ol.featureAnimation.Throw, ol.featureAnimation);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Throw.prototype.animate = function (e)
{	// First time > calculate duration / speed
	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 = function(options){
  options = options || {};
  ol.featureAnimation.call(this, options);
  this.set('zoomout', options.zoomOut);
}
ol.ext.inherits(ol.featureAnimation.Zoom, ol.featureAnimation);
/** Zoom animation: feature zoom out (for points)
 * @constructor
 * @extends {ol.featureAnimation}
 * @param {ol.featureAnimationZoomOptions} options
 */
ol.featureAnimation.ZoomOut = function(options) {
  options = options || {};
  options.zoomOut = true;
  ol.featureAnimation.Zoom.call(this, options);
}
ol.ext.inherits(ol.featureAnimation.ZoomOut, ol.featureAnimation.Zoom);
/** Animate
* @param {ol.featureAnimationEvent} e
*/
ol.featureAnimation.Zoom.prototype.animate = function (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_);
}

/*	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 Extend {@link _ol_control_Control_} options.
 *  @param {boolean} [options.active]
 */
ol.filter.Base = function(options) {
  ol.Object.call(this, options);
  // Array of postcompose listener
  this._listener = [];
  if (options && options.active===false) this.set('active', false);
  else this.set('active', true);
};
ol.ext.inherits(ol.filter.Base, ol.Object);
/** Activate / deactivate filter
*	@param {boolean} b
*/
ol.filter.Base.prototype.setActive = function (b) {
  this.set('active', b===true);
};
/** Get filter active
*	@return {boolean}
*/
ol.filter.Base.prototype.getActive = function () {
  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_ = [];
  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 {boolean} [options.inner=false] mask inner, default false
 *  @param {boolean} [options.wrapX=false] wrap around the world, default false
 */
ol.filter.Mask = function(options) {
  options = options || {};
  ol.filter.Base.call(this, 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)";
}
ol.ext.inherits(ol.filter.Mask, ol.filter.Base);
/** Draw the feature into canvas */
ol.filter.Mask.prototype.drawFeaturePath_ = function(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 (0,0);
    ctx.lineTo (canvas.width, 0);
    ctx.lineTo (canvas.width, canvas.height);
    ctx.lineTo (0, canvas.height);
    ctx.lineTo (0, 0);
  }
  // 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;
    for (var i=start; i<=end; i++) {
      drawll(i*worldWidth);
    }
  } else {
    drawll(0);
  }
};
ol.filter.Mask.prototype.postcompose = function(e) {
  if (!this.feature_) return;
  var ctx = e.context;
  ctx.save();
    this.drawFeaturePath_(e, !this.get("inner"));
    ctx.fillStyle = this.fillColor_;
    ctx.fill("evenodd");
  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 = function(options) {
  ol.filter.Base.call(this, options);
  this._layers = [];
};
ol.ext.inherits(ol.filter.CSS, ol.filter.Base);
/** 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})
 */
ol.filter.CSS.prototype.setBlend = function(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})
 */
ol.filter.CSS.prototype.setFilter = function(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
 */
 ol.filter.CSS.prototype.setDisplay = function(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 
 */
ol.filter.CSS.prototype.addToLayer = function(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 
 */
ol.filter.CSS.prototype.removeFromLayer = function(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 = function(options) {
  ol.filter.Base.call(this, options);
  this._svg = {};
};
ol.ext.inherits(ol.filter.CanvasFilter, ol.filter.Base);
/** Add a new svg filter
 * @param {string|ol.ext.SVGFilter} url IRI pointing to an SVG filter element
 */
ol.filter.CanvasFilter.prototype.addSVGFilter = function(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
 */
ol.filter.CanvasFilter.prototype.removeSVGFilter = function(url) {
  if (url.getId) url = '#'+url.getId();
  delete this._svg[url]
  this.dispatchEvent({ type: 'propertychange', key: 'svg', oldValue: this._svg });
};
/**
 * @private
 */
ol.filter.CanvasFilter.prototype.precompose = function() {
};
/**
 * @private
 */
ol.filter.CanvasFilter.prototype.postcompose = function(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('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')+'%)'); 
  if (this.get('sepia')!==undefined) filter.push('sepia('+this.get('sepia')+'%)');
  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 = function(options) {
  options = options || {};
  ol.filter.Base.call(this, 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];
  }
}
ol.ext.inherits(ol.filter.Clip, ol.filter.Base);
ol.filter.Clip.prototype.clipPath_ = function(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
 */
ol.filter.Clip.prototype.precompose = function(e) {
  if (!this.get("color")){
    e.context.save();
    e.context.beginPath();
    this.clipPath_(e);
    e.context.clip();
  }
};
/**
 * @private
 */
ol.filter.Clip.prototype.postcompose = function(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 = function(options) {
  ol.filter.Base.call(this, options);
  this.setFilter(options);
}
ol.ext.inherits(ol.filter.Colorize, ol.filter.Base);
/** Set options to the filter
 * @param {FilterColorizeOptions} [options]
 */
ol.filter.Colorize.prototype.setFilter = function(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
 */
ol.filter.Colorize.prototype.setValue = function(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
 */
ol.filter.Colorize.prototype.setColor = function(c) {
  c = ol.color.asArray(c);
  if (c) {
    c[3] = this.get("value");
    this.set("color", ol.color.asString(c));
  }
}
/** @private 
 */
ol.filter.Colorize.prototype.precompose = function(/* e */) {
}
/** @private 
 */
ol.filter.Colorize.prototype.postcompose = function(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 = function(options) {
  ol.filter.Base.call(this, options);
  this.set("operation", options.operation || "source-over");
}
ol.ext.inherits(ol.filter.Composite, ol.filter.Base);
/** Change the current operation
*	@param {string} operation composite function
*/
ol.filter.Composite.prototype.setOperation = function(operation) {
  this.set('operation', operation || "source-over");
}
ol.filter.Composite.prototype.precompose = function(e) {
  var ctx = e.context;
  ctx.save();
  ctx.globalCompositeOperation = this.get('operation');
}
ol.filter.Composite.prototype.postcompose = function(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 {boolean} [options.inner=false] mask inner, default false
*/
ol.filter.Crop = function(options) {
  options = options || {};
  ol.filter.Mask.call(this, options);
}
ol.ext.inherits(ol.filter.Crop, ol.filter.Mask);
ol.filter.Crop.prototype.precompose = function(e) {
  if (this.feature_) {
    var ctx = e.context;
    ctx.save();
      this.drawFeaturePath_(e, this.get("inner"));
      ctx.clip("evenodd");
  }
}
ol.filter.Crop.prototype.postcompose = function(e) {
  if (this.feature_) e.context.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] shadow opacity
 */
ol.filter.Fold = function(options) {
  options = options || {};
  ol.filter.Base.call(this, 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.opacity||.2);
};
ol.ext.inherits(ol.filter.Fold, ol.filter.Base);
ol.filter.Fold.prototype.drawLine_ = function(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();
};
ol.filter.Fold.prototype.precompose = function(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();
};
ol.filter.Fold.prototype.postcompose = function(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')||.2)+")");
      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 = function(options) {
  if (!options) options = {};
  ol.filter.Base.call(this, options);
  this.internal_ = document.createElement('canvas');
  this.setSize(options.size);
  this.set('channel', options.channel);
}
ol.ext.inherits(ol.filter.Halftone, ol.filter.Base);
/** Set the current size
*	@param {number} width the pattern width, default 30
*/
ol.filter.Halftone.prototype.setSize = function (size) {
  size = Number(size) || 30;
  this.set("size", size);
};
/** Postcompose operation
*/
ol.filter.Halftone.prototype.postcompose = function(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++) {
      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 = function(options) {
  if (!options) options = {};
  ol.filter.Base.call(this, 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');
}
ol.ext.inherits(ol.filter.Lego, ol.filter.Base);
/** 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"
};
/** Overwrite to handle brickSize
* @param {string} key
* @param {any} val
*/
ol.filter.Lego.prototype.set = function (key, val) {
  ol.filter.Base.prototype.set.call(this, 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
*/
ol.filter.Lego.prototype.setBrick = function (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
*/
ol.filter.Lego.prototype.getPattern = function (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
*/
ol.filter.Lego.prototype.postcompose = function(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();
};

/*	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 = function(options) {
  options = options || {};
  ol.filter.Base.call(this, options);
  this.set('blur', options.blur || 8);
  this.set('intensity', options.intensity || .8);
};
ol.ext.inherits(ol.filter.PencilSketch, ol.filter.Base);
/** @private 
 */
ol.filter.PencilSketch.prototype.precompose = function(/* e */) {
};
/** @private 
 */
ol.filter.PencilSketch.prototype.postcompose = function(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 = function(options) {
  options = options || {};
  ol.filter.Base.call(this, options);
  this.set('saturate', Number(options.saturate) || 2);
  this.pixels = [];
};
ol.ext.inherits(ol.filter.Pointillism, ol.filter.Base);
/** Create points to place on the map
 * @private
 */
ol.filter.Pointillism.prototype._getPixels = function(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 
 */
ol.filter.Pointillism.prototype.precompose = function(/* e */) {
};
/** @private 
 */
ol.filter.Pointillism.prototype.postcompose = function(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 = function(filters) {
  ol.filter.Base.call(this);
  this._svg = {};
  if (filters) {
    if (!(filters instanceof Array)) filters = [filters];
    filters.forEach(function(f) {
      this.addSVGFilter(f);
    }.bind(this));
  }
};
ol.ext.inherits(ol.filter.SVGFilter, ol.filter.Base);
/** Add a new svg filter
 * @param {ol.ext.SVGFilter} filter
 */
ol.filter.SVGFilter.prototype.addSVGFilter = function(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
 */
ol.filter.SVGFilter.prototype.removeSVGFilter = function(filter) {
  var url = '#'+filter.getId();
  delete this._svg[url]
  this.dispatchEvent({ type: 'propertychange', key: 'svg', oldValue: this._svg });
};
/**
 * @private
 */
ol.filter.SVGFilter.prototype.precompose = function() {
};
/**
 * @private
 */
ol.filter.SVGFilter.prototype.postcompose = function(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 = function(options)
{	ol.filter.Base.call(this, options);
	this.setFilter(options);
}
ol.ext.inherits(ol.filter.Texture, ol.filter.Base);
/** Set texture
 * @param {FilterTextureOptions} [options]
 */
ol.filter.Texture.prototype.setFilter = function(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];
			} 
			// default source
			else {
				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
 */
ol.filter.Texture.prototype.getPattern = function (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 */
ol.filter.Texture.prototype.postcompose = function(e)
{	// not ready
	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 = function(options) {
  options = options || {};
  ol.format.GeoJSON.call (this, 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');
};
ol.ext.inherits(ol.format.GeoJSONX, ol.format.GeoJSON);
/** 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"
];
/** Set geometry layout
 * @param {string} layout the geometry layout (XY or XYZ or XYZM)
 */
ol.format.GeoJSONX.prototype.setLayout = function(layout) {
  switch(layout) {
    case 'XYZ': 
    case 'XYZM': {
      this._layout = layout;
      break;
    }
    default:  {
      this._layout = 'XY';
      break;
    }
  }
};
/** Get geometry layout
 * @return {string} layout 
 */
ol.format.GeoJSONX.prototype.getLayout = function() {
  return this._layout;
};
/** Encode a number
 * @param {number} number Number to encode
 * @param {number} decimals Number of decimals
 * @param {string}
 */
ol.format.GeoJSONX.prototype.encodeNumber = function(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}
 */
ol.format.GeoJSONX.prototype.decodeNumber = function(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
 */
ol.format.GeoJSONX.prototype.encodeCoordinates = function(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[i][2], 2);
    if (this._layout[3]=='M' && v.length > 3) p += ',' + this.encodeNumber(v[i][3], 0);
    return p;
  } else if (v.length && v[0]) {
    if (typeof(v[0][0]) === 'number') {
      var dxy = [0,0,0,0];
      var xy = [];
      var hasZ = (this._layout[2]=='Z' && v[0].length > 2);
      var hasM = (this._layout[3]=='M' && v[0].length > 3);
      for (i=0; i<v.length; i++) {
        tp = [
          Math.round( v[i][0] * Math.pow(10, decimal)),
          Math.round( v[i][1] * Math.pow(10, decimal))
        ];
        if (hasZ) tp[2] = v[i][2];
        if (hasM) tp[3] = v[i][3];
        v[i] = tp;
        var dx = v[i][0] - dxy[0];
        var dy = v[i][1] - dxy[1];
        if (i==0 || (dx!==0 || dy!==0)) {
          p = this.encodeNumber(dx, 0) +','
            + this.encodeNumber(dy, 0)
            + (hasZ ? ',' + this.encodeNumber(v[i][2] - dxy[2], 2) : '')
            + (hasM ? ',' + this.encodeNumber(v[i][3] - dxy[3], 0) : '');
          xy.push(p);
          dxy = v[i];
        }
      }
      // Almost 2 points...
      // if (xy.length<2) xy.push('A,A');
      return xy.join(';');
    } else {
      for (i=0; i<v.length; i++) {
        v[i] = this.encodeCoordinates(v[i], decimal);
      }
      return v;
    }
  } else {
    return this.encodeCoordinates([0,0], decimal);
  }
};
/** Decode coordinates
 * @param {string|Array<string>}
 * @param {number} decimal Number of decimals
 * @return {ol.coordinate|Array<ol.coordinate>} v
 * @api
 */
ol.format.GeoJSONX.prototype.decodeCoordinates = function(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
 */
ol.format.GeoJSONX.prototype.writeFeaturesObject = function (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
 */
ol.format.GeoJSONX.prototype.writeFeatureObject = function(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
 */
ol.format.GeoJSONX.prototype.writeGeometryObject = function(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
 */
ol.format.GeoJSONX.prototype.readFeaturesFromObject = function (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}
 */
ol.format.GeoJSONX.prototype.readFeatureFromObject = function (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;
};

/** 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 = function(options) {
  options = options || {};
  ol.format.GeoJSONX.call (this, options);
  this._lineFormat = new ol.format.Polyline({ factor: Math.pow(10, options.decimals || 6) });
};
ol.ext.inherits(ol.format.GeoJSONP, ol.format.GeoJSONX);
/** Encode coordinates
 * @param {ol.coordinate|Array<ol.coordinate>} v
 * @return {string|Array<string>}
 * @api
 */
ol.format.GeoJSONP.prototype.encodeCoordinates = function(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
 */
ol.format.GeoJSONP.prototype.decodeCoordinates = function(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 = function(options) {
  options = options || {};
  ol.Object.call (this, options);
};
ol.ext.inherits(ol.format.GeoRSS, ol.Object);
/**
 * 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
 */
ol.format.GeoRSS.prototype.readFeature = function(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
 */
ol.format.GeoRSS.prototype.readFeatures = function(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
 */
ol.format.GeoRSS.prototype.getDocumentItemsTagName = function(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 = function(options) {
  this.layers_ = [];
  ol.interaction.Pointer.call(this, {
    handleDownEvent: this._setPosition,
    handleMoveEvent: this._setPosition
  });
  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);
};
ol.ext.inherits(ol.interaction.Clip, ol.interaction.Pointer);
/** Set the map > start postcompose
*/
ol.interaction.Clip.prototype.setMap = function(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 */ }
  }
  ol.interaction.Pointer.prototype.setMap.call(this, 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
 */
ol.interaction.Clip.prototype.setRadius = function(radius) {
  this.radius = radius;
  if (this.getMap()) {
    try { this.getMap().renderSync(); } catch(e) { /* ok */ }
  }
};
/** Get clip radius
 *	@returns {integer} radius
 */
ol.interaction.Clip.prototype.getRadius = function() {
  return this.radius;
};
/** Add a layer to clip
 *	@param {ol.layer|Array<ol.layer>} layer to clip
*/
ol.interaction.Clip.prototype.addLayer = function(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
 */
ol.interaction.Clip.prototype.removeLayers = function() {
  this.removeLayer(this.layers_);
};
/** Remove a layer to clip
 *	@param {ol.layer|Array<ol.layer>} layer to clip
*/
ol.interaction.Clip.prototype.removeLayer = function(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
 */
ol.interaction.Clip.prototype.setPosition = function(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}
 */
ol.interaction.Clip.prototype.getPosition = function() {
  if (this.pos) return this.getMap().getCoordinateFromPixel(this.pos);
  return null;
};
/** Set position of the clip
 * @param {ol.Pixel} pixel
 */
 ol.interaction.Clip.prototype.setPixelPosition = function(pixel) {
  this.pos = pixel;
  if (this.getMap()) {
    try { this.getMap().renderSync(); } catch(e) { /* ok */ }
  }
};
/** Get position of the clip
 * @returns {ol.Pixel} pixel
 */
 ol.interaction.Clip.prototype.getPixelPosition = function() {
  return this.pos;
};
/** Set position of the clip
 * @param {ol.MapBrowserEvent} e
 * @privata
 */
ol.interaction.Clip.prototype._setPosition = function(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
*/
ol.interaction.Clip.prototype.precompose_ = function(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
*/
ol.interaction.Clip.prototype.postcompose_ = function(e) {
  if (!this.getActive()) return;
  e.context.restore();
};
/**
 * Activate or deactivate the interaction.
 * @param {boolean} active Active.
 * @observable
 * @api
 */
ol.interaction.Clip.prototype.setActive = function(b) {
  if (b===this.getActive()) return;
  ol.interaction.Pointer.prototype.setActive.call (this, b);
  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 = function(options) {
  options = options || {};
  var condition = options.condition || function() {
    return true;
  }
  // Check events on the map
  ol.interaction.Interaction.call(this, {
    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;
    }.bind(this)
  });
  // Add a key listener
  if (options.onKeyDown) { 
    document.addEventListener('keydown', function(e) {
      if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) {
        options.onKeyDown ({ type: e.type, map: this.getMap(), originalEvent: e });
      }
    }.bind(this));
  }
  if (options.onKeyPress) { 
    document.addEventListener('keydown', function(e) {
      if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) {
        options.onKeyPress ({ type: e.type, map: this.getMap(), originalEvent: e });
      }
    }.bind(this));
  }
  if (options.onKeyUp) { 
    document.addEventListener('keydown', function(e) {
      if (this.isCurrentMap() && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) {
        options.onKeyUp ({ type: e.type, map: this.getMap(), originalEvent: e });
      }
    }.bind(this));
  }
};
ol.ext.inherits(ol.interaction.CurrentMap, ol.interaction.Interaction);
/** The current map */
ol.interaction.CurrentMap.prototype._currentMap = undefined;
/** Check if is the current map 
 * @return {boolean}
 */
ol.interaction.CurrentMap.prototype.isCurrentMap = function() {
  return this.getMap() === ol.interaction.CurrentMap.prototype._currentMap;
};
/** Get the current map
 * @return {ol.Map}
 */
ol.interaction.CurrentMap.prototype.getCurrentMap = function() {
  return ol.interaction.CurrentMap.prototype._currentMap;
};
/** Set the current map
 * @param {ol.Map} map
 */
ol.interaction.CurrentMap.prototype.setCurrentMap = function(map) {
  ol.interaction.CurrentMap.prototype._currentMap = map;
};

/** 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 = function(options) {
  ol.interaction.Clip.call(this, options);
};
ol.ext.inherits(ol.interaction.Blob, ol.interaction.Clip);
/** Animate the blob
 * @private
 */
ol.interaction.Blob.prototype.precompose_ = function(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
 */
ol.interaction.Blob.prototype._getCenter = function(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
 */
ol.interaction.Blob.prototype._calculate = function(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 = function(options) {
  options = options || {};
  // 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
  });
  ol.interaction.Interaction.call(this, {
    handleEvent: function(e) {
      if (rex.test(e.type)) this.pos_ = e.coordinate;
      if (options.handleEvent) return options.handleEvent.call (this,e);
      return true; 
    }
  });
};
ol.ext.inherits(ol.interaction.CenterTouch, ol.interaction.Interaction);
/**
 * 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
 */
ol.interaction.CenterTouch.prototype.setMap = function(map) {
  if (this.getMap()) {
    this.getMap().removeInteraction(this.ctouch);
    this.getMap().removeInteraction(this._target);
  }
  ol.interaction.Interaction.prototype.setMap.call (this, 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
 */
ol.interaction.CenterTouch.prototype.setActive = function(b) {
  ol.interaction.Interaction.prototype.setActive.call (this, 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}
 */
ol.interaction.CenterTouch.prototype.getPosition = function () {
  if (!this.pos_) {
    var px =this.getMap().getSize();
    px = [ px[0]/2, px[1]/2 ];
    this.pos_ = this.getMap().getCoordinateFromPixel(px);
  }
  return this.pos_; 
};

/** 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 = function(options) {
  options = options || {};
  // Features to copy
  this.features = [];
  this._cloneFeature = true;
  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;
    }
  }
  this._featuresSource = options.features || new ol.Collection();
  this.setSources(options.sources);
  this.setDestination(options.destination);
  // Create intreaction
  ol.interaction.CurrentMap.call(this, {
    condition: options.mapCondition,
    onKeyDown: function (e) {
      switch (condition(e)) {
        case 'copy': {
          this.copy({ silent: false });
          break;
        }
        case 'cut': {
          this.copy({ cut: true, silent: false });
          break;
        }
        case 'paste': {
          this.paste({ silent: false });
          break;
        }
        default: break;
      }
    }.bind(this)
  });
};
ol.ext.inherits(ol.interaction.CopyPaste, ol.interaction.CurrentMap);
/** Sources to cut feature from
 * @param { ol.source.Vector | Array<ol.source.Vector> } sources
 */
ol.interaction.CopyPaste.prototype.setSources = function (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> } 
 */
ol.interaction.CopyPaste.prototype.getSources = function () {
  return this._source;
};
/** Source to paste features
 * @param { ol.source.Vector } source
 */
ol.interaction.CopyPaste.prototype.setDestination = function (destination) {
  this._destination = destination;
};
/** Get source to paste features
 * @param { ol.source.Vector } 
 */
ol.interaction.CopyPaste.prototype.getDestination = function () {
  return this._destination;
};
/** Get current feature to copy
 * @return {Array<ol.Feature>}
 */
ol.interaction.CopyPaste.prototype.getFeatures = function() {
  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
 */
ol.interaction.CopyPaste.prototype.copy = function (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
 */
ol.interaction.CopyPaste.prototype.paste = function(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 {*} options ol.interaction.Select options
 */
ol.interaction.Delete = function(options) {
  ol.interaction.Select.call(this, options);
  this.on('select', function(e) {
    this.getFeatures().clear();
    this.delete(e.selected);
  }.bind(this));
};
ol.ext.inherits(ol.interaction.Delete, ol.interaction.Select);
/** Get vector source of the map
 * @return {Array<ol.source.Vector>}
 */
ol.interaction.Delete.prototype._getSources = function(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
 */
ol.interaction.Delete.prototype.delete = function(features) {
  if (features && (features.length || features.getLength())) {
    this.dispatchEvent({ type: 'deletestart', features: features });
    var delFeatures = [];
    // Get the sources concerned
    this._getSources().forEach(function (source) {
      try {
        // Try to delete features in the source
        features.forEach(function(f) {
          source.removeFeature(f);
          delFeatures.push(f);
        });
      } catch(e) { /* ok */ }
    })
    this.dispatchEvent({ type: 'deleteend', features: delFeatures });
  }
};

/** Drag an overlay on the map
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @fires dragstart
 * @fires dragging
 * @fires dragend
 * @param {any} options
 *  @param {ol.Overlay|Array<ol.Overlay>} options.overlays the overlays to drag
 *  @param {ol.Size} options.offset overlay offset, default [0,0]
 */
ol.interaction.DragOverlay = function(options) {
  if (!options) options = {};
  var offset = options.offset || [0,0];
  // Extend pointer
  ol.interaction.Pointer.call(this, {
    // start draging on an overlay
    handleDownEvent: function(evt) {
      var res = evt.frameState.viewState.resolution
      var coordinate = [evt.coordinate[0] + offset[0]*res, evt.coordinate[1] - offset[1]*res];
      // Click on a button (closeBox) or on a link: don't drag!
      if (/^(BUTTON|A)$/.test(evt.originalEvent.target.tagName)) {
        this._dragging = false;
        return true;
      }
      // Start dragging
      if (this._dragging) {
        if (options.centerOnClick !== false) {
          this._dragging.setPosition(coordinate, true);
        } else {
          coordinate = this._dragging.getPosition();
        }
        this.dispatchEvent({ 
          type: 'dragstart',
          overlay: this._dragging,
          originalEvent: evt.originalEvent,
          frameState: evt.frameState,
          coordinate: coordinate
        });
        return true;
      }
      return false;
    },
    // Drag
    handleDragEvent: function(evt) {
      var res = evt.frameState.viewState.resolution
      var coordinate = [evt.coordinate[0] + offset[0]*res, evt.coordinate[1] - offset[1]*res];
      if (this._dragging) {
        this._dragging.setPosition(coordinate, true);
        this.dispatchEvent({ 
          type: 'dragging',
          overlay: this._dragging,
          originalEvent: evt.originalEvent,
          frameState: evt.frameState,
          coordinate: coordinate
        });
      }
    },
    // Stop dragging
    handleUpEvent: function(evt) {
      var res = evt.frameState.viewState.resolution
      var coordinate = [evt.coordinate[0] + offset[0]*res, evt.coordinate[1] - offset[1]*res];
      if (this._dragging) {
        this.dispatchEvent({ 
          type: 'dragend',
          overlay: this._dragging,
          originalEvent: evt.originalEvent,
          frameState: evt.frameState,
          coordinate: coordinate
        });
        this._dragging = false;
        return true;
      }
      return false;
    }
  });
  // List of overlays / listeners
  this._overlays = [];
  if (!(options.overlays instanceof Array)) options.overlays = [options.overlays];
  options.overlays.forEach(this.addOverlay.bind(this));
};
ol.ext.inherits(ol.interaction.DragOverlay, ol.interaction.Pointer);
/** Add an overlay to the interacton
 * @param {ol.Overlay} ov
 */
ol.interaction.DragOverlay.prototype.addOverlay = function (ov) {
  for (var i=0, o; o=this._overlays[i]; i++) {
    if (o===ov) return;
  }
  // Stop event overlay
  if (ov.element.parentElement && ov.element.parentElement.classList.contains('ol-overlaycontainer-stopevent')) {
    console.warn('[DragOverlay.addOverlay] overlay must be created with stopEvent set to false!');
    return;
  }
  // Add listener on overlay of the same map
  var handler = function() {
    if (this.getMap()===ov.getMap()) this._dragging = ov;
  }.bind(this);
  this._overlays.push({
    overlay: ov,
    listener: handler
  });
  ov.element.addEventListener('pointerdown', handler);
};
/** Remove an overlay from the interacton
 * @param {ol.Overlay} ov
 */
ol.interaction.DragOverlay.prototype.removeOverlay = function (ov) {
  for (var i=0, o; o=this._overlays[i]; i++) {
    if (o.overlay===ov) {
      var l = this._overlays.splice(i,1)[0];
      ov.element.removeEventListener('pointerdown', l.listener);
      break;
    }
  }
};

/*	Copyright (c) 2017 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** Interaction to draw holes in a polygon.
 * It fires a drawstart, drawend event when drawing the hole
 * and a modifystart, modifyend event before and after inserting the hole in the feature geometry.
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @fires drawstart
 * @fires drawend
 * @fires modifystart
 * @fires modifyend
 * @param {olx.interaction.DrawHoleOptions} options extend olx.interaction.DrawOptions
 * 	@param {Array<ol.layer.Vector> | function | undefined} options.layers A list of layers from which polygons should be selected. Alternatively, a filter function can be provided. default: all visible layers
 * 	@param {Array<ol.Feature> | ol.Collection<ol.Feature> | function | undefined} options.features An array or a collection of features the interaction applies on or a function that takes a feature and a layer and returns true if the feature is a candidate
 * 	@param { ol.style.Style | Array<ol.style.Style> | StyleFunction | undefined }	Style for the selected features, default: default edit style
 */
ol.interaction.DrawHole = function(options) {
  if (!options) options = {};
  var self = this;
  // Select interaction for the current feature
  this._select = new ol.interaction.Select({ style: options.style });
  this._select.setActive(false);
  // Geometry function that test points inside the current
  var geometryFn, geomFn = options.geometryFunction;
  if (geomFn) {
    geometryFn = function(c,g) {
      g = self._geometryFn (c, g);
      return geomFn (c,g);
    }
  } else {
    geometryFn = function(c,g) { return self._geometryFn (c, g); }
  }
  // Create draw interaction
  options.type = "Polygon";
  options.geometryFunction = geometryFn;
  ol.interaction.Draw.call(this, options);
  // 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.features) === 'function') {
    this._features = options.features;
  } else if (options.features) {
    var features = options.features;
    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));
};
ol.ext.inherits(ol.interaction.DrawHole, ol.interaction.Draw);
/**
 * 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
 */
ol.interaction.DrawHole.prototype.setMap = function(map) {
  if (this.getMap()) this.getMap().removeInteraction(this._select);
  if (map) map.addInteraction(this._select);
  ol.interaction.Draw.prototype.setMap.call (this, map);
};
/**
 * Activate/deactivate the interaction
 * @param {boolean}
 * @api stable
 */
ol.interaction.DrawHole.prototype.setActive = function(b) {
  this._select.getFeatures().clear();
  ol.interaction.Draw.prototype.setActive.call (this, b);
};
/**
 * Remove last point of the feature currently being drawn 
 * (test if points to remove before).
 */
ol.interaction.DrawHole.prototype.removeLastPoint = function() {
  if (this._feature && this._feature.getGeometry().getCoordinates()[0].length>2) {
    ol.interaction.Draw.prototype.removeLastPoint.call(this);
  }
};
/** 
 * Get the current polygon to hole
 * @return {ol.Feature}
 */
ol.interaction.DrawHole.prototype.getPolygon = function() {
  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
 */
ol.interaction.DrawHole.prototype._startDrawing = function(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) {
      if (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
 */
ol.interaction.DrawHole.prototype._finishDrawing = function(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
 */
ol.interaction.DrawHole.prototype._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;
};

/*	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 = function(options) {
  if (!options) options={};
  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;
  // 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
  });
  ol.interaction.Interaction.call(this, {	
      /*
      handleDownEvent: this.handleDownEvent_,
      handleMoveEvent: this.handleMoveEvent_,
      handleUpEvent: this.handleUpEvent_,
      */
      handleEvent: this.handleEvent_
    });
};
ol.ext.inherits(ol.interaction.DrawRegular, ol.interaction.Interaction);
/**
 * 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
 */
ol.interaction.DrawRegular.prototype.setMap = function(map) {
  if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_);
  ol.interaction.Interaction.prototype.setMap.call (this, map);
  this.overlayLayer_.setMap(map);
};
/**
 * Activate/deactivate the interaction
 * @param {boolean}
 * @api stable
 */
ol.interaction.DrawRegular.prototype.setActive = function(b) {
  this.reset();
  ol.interaction.Interaction.prototype.setActive.call (this, b);
}
/**
 * Reset the interaction
 * @api stable
 */
ol.interaction.DrawRegular.prototype.reset = function() {
  this.overlayLayer_.getSource().clear();
  this.started_ = false;
}
/**
 * Set the number of sides.
 * @param {int} number of sides.
 * @api stable
 */
ol.interaction.DrawRegular.prototype.setSides = function (nb) {
  nb = parseInt(nb);
  this.sides_ = nb>2 ? nb : 0;
}
/**
 * Allow rotation when centered + square
 * @param {bool} 
 * @api stable
 */
ol.interaction.DrawRegular.prototype.canRotate = function (b) {
  if (b===true || b===false) this.canRotate_ = b;
  return this.canRotate_;
}
/**
 * Get the number of sides.
 * @return {int} number of sides.
 * @api stable
 */
ol.interaction.DrawRegular.prototype.getSides = function () {
  return this.sides_;
}
/** Default start angle array for each sides
*/
ol.interaction.DrawRegular.prototype.startAngle = {
  'default':Math.PI/2,
  3: -Math.PI/2,
  4: Math.PI/4
};
/** Get geom of the current drawing
* @return {ol.geom.Polygon | ol.geom.Point}
*/
ol.interaction.DrawRegular.prototype.getGeom_ = function () {
  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.
*/
ol.interaction.DrawRegular.prototype.drawSketch_ = function(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)
*/
ol.interaction.DrawRegular.prototype.drawPoint_ = function(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.
 */
ol.interaction.DrawRegular.prototype.handleEvent_ = function(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.
 */
ol.interaction.DrawRegular.prototype.finishDrawing = function() {
  if (this.started_ && this.coord_) {
    this.end_({ pixel: this.upPx_, coordinate: this.coord_});
  }
};
/**
 * @param {ol.MapBrowserEvent} evt Event.
 */
ol.interaction.DrawRegular.prototype.handleMoveEvent_ = function(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.
 */
ol.interaction.DrawRegular.prototype.start_ = function(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_);
    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.
 */
ol.interaction.DrawRegular.prototype.end_ = function(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_();
};

/*	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 = function(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;
  }
  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));
  ol.interaction.CenterTouch.call(this, options);
  this._source = options.source;
  this.set('tap', options.tap!==false);
};
ol.ext.inherits(ol.interaction.DrawTouch, ol.interaction.CenterTouch);
/**
 * 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
 */
ol.interaction.DrawTouch.prototype.setMap = function(map) {
  if (this._listener) {
    for(var l in this._listener) ol.Observable.unByKey(l);
  }
  this._listener = {};
  ol.interaction.CenterTouch.prototype.setMap.call (this, 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
 */
ol.interaction.DrawTouch.prototype.setGeometryType = function(type) {
  return this.sketch.setGeometryType(type);
};
/** Get geometry type
 * @return {ol.geom.GeometryType}
 */
ol.interaction.DrawTouch.prototype.getGeometryType = function() {
  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.
 */
ol.interaction.DrawTouch.prototype.finishDrawing = function() {
  this.sketch.finishDrawing(true);
};
/** Add a new Point to the drawing
 */
ol.interaction.DrawTouch.prototype.addPoint = function() {
  this.sketch.addPoint(this.getPosition());
};
/** Remove last point of the feature currently being drawn.
 */
ol.interaction.DrawTouch.prototype.removeLastPoint = function() {
  this.sketch.removeLastPoint();
};
/**
 * Activate or deactivate the interaction.
 * @param {boolean} active Active.
 * @observable
 * @api
 */
ol.interaction.DrawTouch.prototype.setActive = function(b) {
  ol.interaction.CenterTouch.prototype.setActive.call (this, b);
  this.sketch.abortDrawing();
  this.sketch.setVisible(b);
};

/** Extend DragAndDrop choose drop zone + fires loadstart, loadend
 * @constructor
 * @extends {ol.interaction.DragAndDrop}
 * @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 = function(options) {
  options = options||{};
  ol.interaction.DragAndDrop.call(this, {});
  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"];
  var self = this;
  zone.addEventListener('drop', function(e){ return self.ondrop(e);});
};
ol.ext.inherits(ol.interaction.DropFile, ol.interaction.DragAndDrop);
/** Set the map
*/
ol.interaction.DropFile.prototype.setMap = function(map) {
  ol.interaction.Interaction.prototype.setMap.call(this, map);
};
/** Do something when over
*/
ol.interaction.DropFile.prototype.onstop = function(e) {
  e.preventDefault();
  e.stopPropagation();
  return false;
}
/** Do something when over
*/
ol.interaction.DropFile.prototype.ondrop = function(e) {
  e.preventDefault();
  if (e.dataTransfer && e.dataTransfer.files.length) {
    var self = this;
    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 file;
    var pat = /\.([0-9a-z]+)(?=[?#])|(\.)(?:[\w]+)$/;
    for (var i=0; file=files[i]; i++) {
      var ex = file.name.match(pat)[0];
      var isok = (this.accept_.indexOf(ex.toLocaleLowerCase()) >= 0);
      self.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) continue;
      // 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) {
              self.dispatchEvent({ type:'addfeatures', features: features, file: theFile, projection: projection });
              self.dispatchEvent({ type:'loadend', features: features, file: theFile, projection: projection });
              return;
            }
          } catch(e) { /* ok */ }
        }
        // Nothing match, try to load by yourself
        self.dispatchEvent({ type:'loadend', file: theFile, result: result });
      };
      // Start loading
      reader.readAsText(file);
    }
  }
  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 = function(options, properties) {
  options = options || {};
  if (!options.condition) options.condition = ol.events.condition.click;
  ol.interaction.Select.call(this, 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;
  }
};
ol.ext.inherits(ol.interaction.FillAttribute, ol.interaction.Select);
/** Define the interaction cursor
 * @param {string} cursor CSS cursor
 */
ol.interaction.FillAttribute.prototype.setCursor = function(cursor) {
  this._cursor = cursor;
};
/** Get the interaction cursor
 * @return {string} cursor
 */
ol.interaction.FillAttribute.prototype.getCursor = function() {
  return this._cursor;
};
/** Activate the interaction
 * @param {boolean} active
 */
ol.interaction.FillAttribute.prototype.setActive = function(active) {
  if(active === this.getActive()) return;
  ol.interaction.Select.prototype.setActive.call(this, active);
  if (this.getMap() && this._cursor) {
    if (active) {
      this._previousCursor = this.getMap().getTargetElement().style.cursor;
      this.getMap().getTargetElement().style.cursor = this._cursor;
//      console.log('setCursor',this._cursor)
    } else {
      this.getMap().getTargetElement().style.cursor = this._previousCursor;
      this._previousCursor = undefined;
    }
  }
};
/** Set attributes
 * @param {*} properties The properties as key/value
 */
ol.interaction.FillAttribute.prototype.setAttributes = function(properties) {
  this._attributes = properties;
};
/** Set an attribute
 * @param {string} key 
 * @param {*} val 
 */
ol.interaction.FillAttribute.prototype.setAttribute = function(key, val) {
  this._attributes[key] = val;
};
/** get attributes
 * @return {*} The properties as key/value
 */
ol.interaction.FillAttribute.prototype.getAttributes = function() {
  return this._attributes;
};
/** Get an attribute
 * @param {string} key 
 * @return {*} val 
 */
ol.interaction.FillAttribute.prototype.getAttribute = function(key) {
  return this._attributes[key];
};
/** Fill feature attributes
 * @param {Array<ol.Feature>} features The features to modify
 * @param {*} properties The properties as key/value
 */
ol.interaction.FillAttribute.prototype.fill = function(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 = function(options) {
  ol.interaction.Pointer.call(this, {
    handleDownEvent: this.setPosition,
    handleMoveEvent: this.setPosition
  });
  // Default options
  options = options||{};
  this.pos = false;
  this.radius = (options.radius||100);
  this.setColor(options);
};
ol.ext.inherits(ol.interaction.Flashlight, ol.interaction.Pointer);
/** Set the map > start postcompose
*/
ol.interaction.Flashlight.prototype.setMap = function(map) {
  if (this.getMap()) {
    this.getMap().render();
  }
  if (this._listener) ol.Observable.unByKey(this._listener);
  this._listener = null;
  ol.interaction.Pointer.prototype.setMap.call(this, map);
  if (map) {
    this._listener = map.on('postcompose', this.postcompose_.bind(this));
  }
}
/** Set flashlight radius
 *	@param {integer} radius
*/
ol.interaction.Flashlight.prototype.setRadius = function(radius) {
  this.radius = radius
  if (this.getMap()) {
    try { this.getMap().renderSync(); } catch(e) { /* ok */ }
  }
}
/** Set flashlight color
 *	@param {ol.flashlight.options} flashlight options param
*		- color {ol.Color} light color, default transparent
*		- fill {ol.Color} fill color, default rgba(0,0,0,0.8)
*/
ol.interaction.Flashlight.prototype.setColor = function(options) {
  // Backcolor
  var color = (options.fill ? options.fill : [0,0,0,0.8]);
  var c = ol.color.asArray(color);
  this.startColor = ol.color.asString(c);
  // Halo color
  if (options.color) {
    c = this.endColor = ol.color.asString(ol.color.asArray(options.color)||options.color);
  } else  {
    c[3] = 0
    this.endColor = ol.color.asString(c);
  }
  c[3] = 0.1;
  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}
*/
ol.interaction.Flashlight.prototype.setPosition = function(e) {
  if (e.pixel) this.pos = e.pixel;
  else this.pos = e;
  if (this.getMap()) {
    try { this.getMap().renderSync(); } catch(e) { /* ok */ }
  }
}
/** Postcompose function
*/
ol.interaction.Flashlight.prototype.postcompose_ = function(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.
 * @constructor
 * @fires focus
 * @extends {ol.interaction.Interaction}
 */
ol.interaction.FocusMap = function() {
  //
  ol.interaction.Interaction.call(this, {});
  // 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
    }
  });
};
ol.ext.inherits(ol.interaction.FocusMap, ol.interaction.Interaction);
/** Set the map > add the focus button and focus on the map when pointerdown to enable keyboard events.
 */
ol.interaction.FocusMap.prototype.setMap = function(map) {
  if (this._listener) ol.Observable.unByKey(this._listener);
  this._listener = null;
  if (this.getMap()) { this.getMap().getViewport().removeChild(this.focusBt); }
  ol.interaction.Interaction.prototype.setMap.call (this, 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 = function(options) {
  if (!options) options={};
  // 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") 
  };
  // Prevent interaction when tracking
  ol.interaction.Interaction.call(this, {
    handleEvent: function() {
      return (!this.get('followTrack') || this.get('followTrack')=='auto');//  || !geoloc.getTracking());
    }
  });
  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);
};
ol.ext.inherits(ol.interaction.GeolocationDraw, ol.interaction.Interaction);
/** Simplify 3D geometry
 * @param {ol.geom.Geometry} geo
 * @param {number} tolerance
 */
ol.interaction.GeolocationDraw.prototype.simplify3D = function(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
 */
ol.interaction.GeolocationDraw.prototype.setMap = function(map) {
  if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_);
  ol.interaction.Pointer.prototype.setMap.call (this, map);
  this.overlayLayer_.setMap(map);
  if (map) this.geolocation.setProjection(map.getView().getProjection());
};
/** Activate or deactivate the interaction.
 * @param {boolean} active
 */
ol.interaction.GeolocationDraw.prototype.setActive = function(active) {
  if (active === this.getActive()) return;
  ol.interaction.Interaction.prototype.setActive.call(this, active);
  this.overlayLayer_.setVisible(active);
  if (this.getMap()) {
    this.geolocation.setTracking(active);
    try { this.getMap().renderSync(); } catch(e) { /* ok */ }
  }
  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
 */
ol.interaction.GeolocationDraw.prototype.simulate = function(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}
 */
ol.interaction.GeolocationDraw.prototype.simulating = function() {
  return !!this._track;
};
/** Reset drawing
*/
ol.interaction.GeolocationDraw.prototype.reset = function() {
  this.sketch_[1].setGeometry();
  this.path_ = [];
  this.lastPosition_ = false;
};
/** Start tracking = setActive(true)
 */
ol.interaction.GeolocationDraw.prototype.start = function() {
  this.setActive(true);
};
/** Stop tracking = setActive(false)
 */
ol.interaction.GeolocationDraw.prototype.stop = function() {
  this.setActive(false);
};
/** Pause drawing
 * @param {boolean} b 
 */
ol.interaction.GeolocationDraw.prototype.pause = function(b) {
  this.pause_ = (b!==false);
};
/** Is paused
 * @return {boolean} b 
 */
ol.interaction.GeolocationDraw.prototype.isPaused = function() {
  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
*/
ol.interaction.GeolocationDraw.prototype.setFollowTrack = function(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
 */
ol.interaction.GeolocationDraw.prototype.draw_ = function(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
 */
ol.interaction.GeolocationDraw.prototype.getPosition = function (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 = function(options) {
  if (!options) options={};
  var self = this;
  var dragging = false;
  ol.interaction.Interaction.call(this, {
    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; 
    }
  });
  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);
};
ol.ext.inherits(ol.interaction.Hover, ol.interaction.Interaction);
/**
 * 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
 */
ol.interaction.Hover.prototype.setMap = function(map) {
  if (this.previousCursor_!==undefined && this.getMap()) {
    this.getMap().getTargetElement().style.cursor = this.previousCursor_;
    this.previousCursor_ = undefined;
  }
  ol.interaction.Interaction.prototype.setMap.call (this, map);
};
/** Activate / deactivate interaction
 * @param {boolean} b
 */
ol.interaction.Hover.prototype.setActive = function(b) {
  ol.interaction.Interaction.prototype.setActive.call (this, b);
  if (this.cursor_ && this.getMap() && this.getMap().getTargetElement()) {
    var style = this.getMap().getTargetElement().style;
    if (this.previousCursor_ !== undefined) {
      style.cursor = this.previousCursor_;
      this.previousCursor_ = undefined;
    }
  }
};
/**
 * Set cursor on hover
 * @param { string } cursor css cursor propertie or a function that gets a feature, default: none
 * @api stable
 */
ol.interaction.Hover.prototype.setCursor = function(cursor) {
  if (!cursor && this.previousCursor_!==undefined && this.getMap()) {
    this.getMap().getTargetElement().style.cursor = this.previousCursor_;
    this.previousCursor_ = undefined;
  }
  this.cursor_ = cursor;
};
/** Feature filter to get only one feature
* @param {function} filter a function with two arguments, the feature and the layer of the feature. Return true to select the feature 
*/
ol.interaction.Hover.prototype.setFeatureFilter = function(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
*/
ol.interaction.Hover.prototype.setLayerFilter = function(filter) {
  if (typeof (filter) == 'function') this.layerFilter_ = filter;
  else this.layerFilter_ = function(){ return true; };
};
/** Get features whenmove
* @param {ol.event} e "move" event
*/
ol.interaction.Hover.prototype.handleMove_ = function(e) {
  var map = this.getMap();
  if (map) {
    //var b = map.hasFeatureAtPixel(e.pixel);
    var feature, layer;
    var self = this;
    var b = map.forEachFeatureAtPixel(
      e.pixel, 
      function(f, l) {
        if (self.featureFilter_.call(null,f,l)) {
          feature = f;
          layer = l;
          return true;
        } else {
          feature = layer = null;
          return false;
        }
      },{ 
        hitTolerance: this.get('hitTolerance'),
        layerFilter: self.layerFilter_ 
      }
    );
    if (b) this.dispatchEvent({ 
      type: 'hover', 
      feature: feature, 
      layer: layer, 
      coordinate: e.coordinate, 
      pixel: e.pixel, 
      map: e.map, 
      originalEvent: e.originalEvent,
      dragging: e.dragging 
    });
    if (this.feature_===feature && this.layer_===layer){
      /* ok */
    } else {
      this.feature_ = feature;
      this.layer_ = layer;
      if (feature) {
        this.dispatchEvent({ 
          type: 'enter', 
          feature: feature, 
          layer: layer, 
          coordinate: e.coordinate, 
          pixel: e.pixel, 
          map: e.map, 
          originalEvent: e.originalEvent,
          dragging: e.dragging 
        });
      } else {
        this.dispatchEvent({ 
          type: 'leave', 
          coordinate: e.coordinate, 
          pixel: e.pixel, 
          map: e.map, 
          originalEvent: e.originalEvent,
          dragging: e.dragging 
        });
      }
    }
    if (this.cursor_) {
      var style = map.getTargetElement().style;
      if (b) {
        if (style.cursor != this.cursor_) {
          this.previousCursor_ = style.cursor;
          style.cursor = this.cursor_;
        }
      } else if (this.previousCursor_ !== undefined) {
        style.cursor = this.previousCursor_;
        this.previousCursor_ = undefined;
      }
    }
  }
};

/*	Copyright (c) 2016 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** Interaction to handle longtouch events
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @param {olx.interaction.LongTouchOptions} 
 * 	@param {function | undefined} options.handleLongTouchEvent Function handling 'longtouch' events, it will receive a mapBrowserEvent. Or listen to the map 'longtouch' event.
 *	@param {integer | undefined} [options.pixelTolerance=0] pixel tolerance before drag, default 0
 *	@param {integer | undefined} [options.delay=1000] The delay for a long touch in ms, default is 1000
 */
ol.interaction.LongTouch = function(options) {
  if (!options) options = {};
  this.delay_ = options.delay || 1000;
  var ltouch = options.handleLongTouchEvent || function(){};
  var _timeout = null;
  var position, event;
  var tol = options.pixelTolerance || 0;
  ol.interaction.Interaction.call(this, {
    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;
    }
  });
};
ol.ext.inherits(ol.interaction.LongTouch, ol.interaction.Interaction);

// 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.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.Pointer}
 * @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 = function(options){
  if (!options) options = {};
  var dragging, modifying;
  ol.interaction.Pointer.call(this,{
    /*
    handleDownEvent: this.handleDownEvent,
    handleDragEvent: this.handleDragEvent,
    handleMoveEvent: this.handleMoveEvent,
    handleUpEvent: this.handleUpEvent,
    */
    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 true;
        }
        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
  });
};
ol.ext.inherits(ol.interaction.ModifyFeature, ol.interaction.Pointer);
/**
 * 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
 */
ol.interaction.ModifyFeature.prototype.setMap = function(map) {
  if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_);
  ol.interaction.Interaction.prototype.setMap.call (this, map);
  this.overlayLayer_.setMap(map);
};
/**
 * Activate or deactivate the interaction + remove the sketch.
 * @param {boolean} active.
 * @api stable
 */
ol.interaction.ModifyFeature.prototype.setActive = function(active) {
  ol.interaction.Interaction.prototype.setActive.call (this, 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.
 */
ol.interaction.ModifyFeature.prototype.setFilter = function(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
 */
ol.interaction.ModifyFeature.prototype.getClosestFeature = function(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)
*/
ol.interaction.ModifyFeature.prototype.getNearestCoord = function(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
 */
ol.interaction.ModifyFeature.prototype.getArcs = function(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.
 */
ol.interaction.ModifyFeature.prototype.handleDownEvent = function(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
 */
ol.interaction.ModifyFeature.prototype.getModifiedFeatures = function() {
  return this._modifiedFeatures || [];
};
/** Removes the vertex currently being pointed.
 */
ol.interaction.ModifyFeature.prototype.removePoint = function() {
  this._removePoint({},{});
};
/**
 * @private
 */
ol.interaction.ModifyFeature.prototype._getModification = function(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
 */
ol.interaction.ModifyFeature.prototype._removePoint = function(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
 */
ol.interaction.ModifyFeature.prototype.handleUpEvent = function(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
 */
ol.interaction.ModifyFeature.prototype.setArcCoordinates = function(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
 */
ol.interaction.ModifyFeature.prototype.handleDragEvent = function(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
 */
ol.interaction.ModifyFeature.prototype.handleMoveEvent = function(e) {
  if (!this.getActive()) return false;
  this.overlayLayer_.getSource().clear();
  var current = this.getClosestFeature(e);
  // Draw sketch
  if (current) {
    var p = new ol.Feature(new ol.geom.Point(current.coord));
    this.overlayLayer_.getSource().addFeature(p);
  }
  // Show cursor
  var element = e.map.getTargetElement();
  if (this.cursor_) {
    if (current) {
      if (element.style.cursor != this.cursor_) {
        this.previousCursor_ = element.style.cursor;
        element.style.cursor = this.cursor_;
      }
    } else if (this.previousCursor_ !== undefined) {
      element.style.cursor = this.previousCursor_;
      this.previousCursor_ = undefined;
    }
  }
};
/** Get the current feature to modify
 * @return {ol.Feature} 
 */
ol.interaction.ModifyFeature.prototype.getCurrentFeature = function() {
  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 = function(options) {
  var self = this;
  if (!options) 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() {
    self.removePoint();
  };
  this.setPopupContent(a);
  var pixelTolerance = options.pixelTolerance || 0;
  var searchDist = pixelTolerance +5;
  // Check if there is a feature to select
  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;
  }
  ol.interaction.Modify.call(this, options);
  this.on(['modifystart','modifyend'], function(){
		this.showDeleteBt({ type:'hide', modifying: true });
  });
  // Use a popup ?
  this.set('usePopup', options.usePopup !== false);
};
ol.ext.inherits(ol.interaction.ModifyTouch, ol.interaction.Modify);
/**
 * 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
 */
ol.interaction.ModifyTouch.prototype.setMap = function(map) {	
  if (this.getMap()) {
    this.getMap().removeOverlay(this._popup);
  }
	ol.interaction.Modify.prototype.setMap.call (this, map);
  if (this.getMap()) {
    this.getMap().addOverlay(this._popup);
  }
};
/** Activate the interaction and remove popup
 * @param {Boolean} b
 */
ol.interaction.ModifyTouch.prototype.setActive = function(b) {	
  ol.interaction.Modify.prototype.setActive.call (this, b);
  this.showDeleteBt({ type:'hide' });
};
/**
 * Remove the current point
 */
ol.interaction.ModifyTouch.prototype.removePoint = function() {	
  // Prevent touch + click on popup 
  if (new Date() - this._timeout < 200) return;
  // Remove point
  ol.interaction.Modify.prototype.removePoint.call (this);
  this.showDeleteBt({ type:'hide' });
}
/**
 * Show the delete button (menu)
 * @param {Event} e
 * @api stable
 */
ol.interaction.ModifyTouch.prototype.showDeleteBt = function(e) {
  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 
 */
ol.interaction.ModifyTouch.prototype.setPopupContent = function(html) {
  this._menu = html;
}
/**
 * Get the popup content
 * @return {DOMElement}
 */
ol.interaction.ModifyTouch.prototype.getPopupContent = function() {
  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 {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 = function(options) {
  if (!options) options = {};
	// Extend pointer
	ol.interaction.Pointer.call(this, {
    handleDownEvent: this.handleDownEvent_,
    handleDragEvent: this.handleDragEvent_,
    handleMoveEvent: this.handleMoveEvent_,
    handleUpEvent: this.handleUpEvent_
  });
	// 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;
};
ol.ext.inherits(ol.interaction.Offset, ol.interaction.Pointer);
/**
 * 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
 */
ol.interaction.Offset.prototype.setMap = function(map) {
	ol.interaction.Pointer.prototype.setMap.call (this, map);
};
/** Get Feature at pixel
 * @param {ol.MapBrowserEvent} evt Map browser event.
 * @return {any} a feature and the hit point
 * @private
 */
ol.interaction.Offset.prototype.getFeatureAtPixel_ = function(e) {
  var self = this;
	return this.getMap().forEachFeatureAtPixel(e.pixel,
		function(feature, layer) {
      var current;
			// 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
 */
ol.interaction.Offset.prototype.handleDownEvent_ = function(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
 */
ol.interaction.Offset.prototype.handleDragEvent_ = function(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
 */
ol.interaction.Offset.prototype.handleUpEvent_ = function(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
 */
ol.interaction.Offset.prototype.handleMoveEvent_ = function(e) {	
  var f = this.getFeatureAtPixel_(e);
  if (f) {
    if (this.previousCursor_ === false) {
      this.previousCursor_ = e.map.getTargetElement().style.cursor;
    }
    e.map.getTargetElement().style.cursor = 'pointer';
  } else {
    e.map.getTargetElement().style.cursor = this.previousCursor_;
    this.previousCursor_ = false;
  }
};

/*	
  Water ripple effect.
  Original code (Java) by Neil Wallis 
  @link http://www.neilwallis.com/java/water.html
  Original code (JS) by Sergey Chikuyonok (serge.che@gmail.com)
  @link http://chikuyonok.ru
  @link http://media.chikuyonok.ru/ripple/
  Copyright (c) 2015 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
  @link https://github.com/Viglino
*/
/**
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {*} options
 *  @param {ol/layer/Layer} options.layer layer to animate
 *  @param {number} options.radius raindrop radius
 *  @param {number} options.interval raindrop interval (in ms), default 1000
 */
ol.interaction.Ripple = function(options) {
  ol.interaction.Pointer.call(this, {
    handleDownEvent: this.rainDrop,
    handleMoveEvent: this.rainDrop
  });
  // 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));
};
ol.ext.inherits(ol.interaction.Ripple, ol.interaction.Pointer);
/** Generate random rain drop
*	@param {integer} interval
*/
ol.interaction.Ripple.prototype.rains = function(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}
*/
ol.interaction.Ripple.prototype.rainDrop = function(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
*/
ol.interaction.Ripple.prototype.postcompose_ = function(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 = function(options) {
  options = options || {};
  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)
  // Create a new overlay layer for 
  var overlay = this.overlayLayer_ = 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(this.overlayLayer_);
    }
  }
  // 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;
  };
  this.filter_ = options.filter;
  if (!this._autoClose && !options.toggleCondition) {
    options.toggleCondition = ol.events.condition.singleClick;
  }
  ol.interaction.Select.call(this, options);
  this.on("select", this.selectCluster.bind(this));
};
ol.ext.inherits(ol.interaction.SelectCluster, ol.interaction.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
 */
ol.interaction.SelectCluster.prototype.setMap = function(map) {
  if (this.getMap()) {
    this.getMap().removeLayer(this.overlayLayer_);
  }
  if (this._listener) ol.Observable.unByKey(this._listener);
  this._listener = null;
  ol.interaction.Select.prototype.setMap.call (this, 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
 */
ol.interaction.SelectCluster.prototype.clear = function() {
  this.getFeatures().clear();
  this.overlayLayer_.getSource().clear();
};
/**
 * Get the layer for the revealed features
 * @api stable
 */
ol.interaction.SelectCluster.prototype.getLayer = function() {
  return this.overlayLayer_;
};
/**
 * Select a cluster 
 * @param {ol.SelectEvent | ol.Feature} a cluster feature ie. a feature with a 'features' attribute.
 * @api stable
 */
ol.interaction.SelectCluster.prototype.selectCluster = function (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');
        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
 */
ol.interaction.SelectCluster.prototype.animateCluster_ = function(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)
 */
ol.interaction.SelectCluster.prototype.getClusterExtent = function(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 = function(options) {
  if (!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);
  }
  // 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
    });
  // Use snap interaction
  ol.interaction.Interaction.call(this, {
    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;
    }
  });
};
ol.ext.inherits(ol.interaction.SnapGuides, ol.interaction.Interaction);
/**
 * 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
 */
ol.interaction.SnapGuides.prototype.setMap = function(map) {
  if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_);
  ol.interaction.Interaction.prototype.setMap.call (this, map);
  this.overlayLayer_.setMap(map);
  if (map) this.projExtent_ = map.getView().getProjection().getExtent();
};
/** Activate or deactivate the interaction.
 * @param {boolean} active
 */
ol.interaction.SnapGuides.prototype.setActive = function(active) {
  this.overlayLayer_.setVisible(active);
  ol.interaction.Interaction.prototype.setActive.call (this, active);
}
/** Clear previous added guidelines
 * @param {Array<ol.Feature> | undefined} features a list of feature to remove, default remove all feature
 */
ol.interaction.SnapGuides.prototype.clearGuides = function(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
 */
ol.interaction.SnapGuides.prototype.getGuides = function() {
  return this.overlaySource_.getFeaturesCollection();
}
/** Add a new guide to snap to
 * @param {Array<ol.coordinate>} v the direction vector
 * @return {ol.Feature} feature guide
 */
ol.interaction.SnapGuides.prototype.addGuide = function(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
 */
ol.interaction.SnapGuides.prototype.addOrthoGuide = function(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
 */
ol.interaction.SnapGuides.prototype.setDrawInteraction = function(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
 */
ol.interaction.SnapGuides.prototype.setModifyInteraction = function (modifyi) {
  function mod(d, n) {
    return ((d % n) + n) % n;
  }
  var self = this;
  // Current guidelines
  var features = [];
  function computeGuides(e) {
    var selectedVertex = e.target.vertexFeature_
    if (!selectedVertex) return;
    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 modifyVertex = selectedVertex.getGeometry().getCoordinates();
    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 = function(options) {
  options = options || {};
  // Get layer canevas context
  this._layer = options.layer;
  this._layer.on(['postcompose', 'postrender'], function(e) {
    this._ctx = e.context;
  }.bind(this));
  var radius = options.radius || 8;
  var size = 2*radius;
  ol.interaction.Interaction.call(this, {
    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; 
    }
  });
};
ol.ext.inherits(ol.interaction.SnapLayerPixel, ol.interaction.Interaction);

/*	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.source a list of source to split (configured with useSpatialIndex set to true)
 *  @param {ol.Collection.<ol.Feature>} options.features collection of feature to split
 *  @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} opttion.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 = function(options) {
  if (!options) options = {};
  ol.interaction.Interaction.call(this, {
    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.sources_ = options.sources ? (options.sources instanceof Array) ? options.sources:[options.sources] : [];
  if (options.features) {
    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;
    }
  });
};
ol.ext.inherits(ol.interaction.Split, ol.interaction.Interaction);
/**
 * 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
 */
ol.interaction.Split.prototype.setMap = function(map) {
  if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_);
  ol.interaction.Interaction.prototype.setMap.call (this, map);
  this.overlayLayer_.setMap(map);
};
/** Get closest feature at pixel
 * @param {ol.Pixel} 
 * @return {ol.feature} 
 * @private
 */
ol.interaction.Split.prototype.getClosestFeature = function(e) {
  var source, f, c, g, d = this.snapDistance_+1;
  // Look for closest point in the sources
  this.sources_.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
*/
ol.interaction.Split.prototype.getNearestCoord = function(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.
 */
ol.interaction.Split.prototype.handleDownEvent = function(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.
 */
ol.interaction.Split.prototype.handleMoveEvent = function(e) {
  var map = e.map;
  this.overlayLayer_.getSource().clear();
  var current = this.getClosestFeature(e);
  if (current && this.filterSplit_(current.feature)) {
    var p, l;
    // Draw sketch
    this.overlayLayer_.getSource().addFeature(current.feature);
    p = new ol.Feature(new ol.geom.Point(current.coord));
    p._sketch_ = true;
    this.overlayLayer_.getSource().addFeature(p);
    //
    l = new ol.Feature(current.link);
    l._sketch_ = true;
    this.overlayLayer_.getSource().addFeature(l);
    // move event
    this.dispatchEvent({
      type: 'pointermove',
      coordinate: e.coordinate,
      frameState: e.frameState,
      originalEvent: e.originalEvent,
      map: e.map,
      pixel: e.pixel,
      feature: current.feature,
      linkGeometry: current.link
    });
  } else {
    this.dispatchEvent(e);
  }
  var element = map.getTargetElement();
  if (this.cursor_) {
    if (current) {
      if (element.style.cursor != this.cursor_) {
        this.previousCursor_ = element.style.cursor;
        element.style.cursor = this.cursor_;
      }
    } else if (this.previousCursor_ !== undefined) {
      element.style.cursor = this.previousCursor_;
      this.previousCursor_ = undefined;
    }
  }
};

/*	Copyright (c) 2016 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
/** Interaction splitter: acts as a split feature agent while editing vector features (LineString).
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @fires  beforesplit, aftersplit
 * @param {options} 
 *  @param {ol.source.Vector|Array{ol.source.Vector}} options.source The target source (or array of source) with features to be split (configured with useSpatialIndex set to true)
 *  @param {ol.source.Vector} options.triggerSource Any newly created or modified features from this source will be used to split features on the target source. If none is provided the target source is used instead.
 *  @param {ol.Collection.<ol.Feature>} options.features A collection of feature to be split (replace source target).
 *  @param {ol.Collection.<ol.Feature>} options.triggerFeatures Any newly created or modified features from this collection will be used to split features on the target source (replace triggerSource).
 *  @param {function|undefined} options.filter a filter that takes a feature and return true if the feature is eligible for splitting, default always split.
 *  @param {function|undefined} options.tolerance Distance between the calculated intersection and a vertex on the source geometry below which the existing vertex will be used for the split. Default is 1e-10.
 * @todo verify auto intersection on features that split.
 */
ol.interaction.Splitter = function(options) {
  if (!options) options = {};
  ol.interaction.Interaction.call(this, {
    handleEvent: function(e) {
      // Hack to get only one changeFeature when draging with ol.interaction.Modify on.
      if (e.type != "pointermove" && e.type != "pointerdrag") {
        if (this.lastEvent_) {
          this.splitSource(this.lastEvent_.feature);
          this.lastEvent_ = null;
        }
        this.moving_ = false;
      }
      else this.moving_ = true;
      return true; 
    },
  });
  // Features added / remove
  this.added_ = [];
  this.removed_ = [];
  // Source to split
  if (options.features) {
    this.source_ = new ol.source.Vector({ features: options.features });
  } else {
    this.source_ = options.source ? options.source : new ol.source.Vector({ features: new ol.Collection() });
  }
  var trigger = this.triggerSource;
  if (options.triggerFeatures) {
    trigger = new ol.source.Vector({ features: options.triggerFeatures });
  }
  if (trigger) {
    trigger.on("addfeature", this.onAddFeature.bind(this));
    trigger.on("changefeature", this.onChangeFeature.bind(this));
    trigger.on("removefeature", this.onRemoveFeature.bind(this));
  } else {
    this.source_.on("addfeature", this.onAddFeature.bind(this));
    this.source_.on("changefeature", this.onChangeFeature.bind(this));
    this.source_.on("removefeature", this.onRemoveFeature.bind(this));
  }
  // Split tolerance between the calculated intersection and the geometry
  this.tolerance_ = options.tolerance || 1e-10;
  // Get all features candidate
  this.filterSplit_ = options.filter || function(){ return true; };
};
ol.ext.inherits(ol.interaction.Splitter, ol.interaction.Interaction);
/** 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
*/
ol.interaction.Splitter.prototype.intersectSegs = function(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
*/
ol.interaction.Splitter.prototype.splitSource = function(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
*/
ol.interaction.Splitter.prototype._splitSource = function(feature) {
  var i, k, f2;
  this.splitting = true;
  this.added_ = [];
  this.removed_ = [];
  var c = feature.getGeometry().getCoordinates();
  var seg, split = [];
  function intersect (f) {
    if (f !== feature) {
      var c2 = f.getGeometry().getCoordinates();
      for (var j=0; j<c2.length-1; j++) {
        var p = this.intersectSegs (seg, [c2[j],c2[j+1]]);
        if (p) {
          split.push(p);
          g = f.getGeometry().splitAt(p, this.tolerance_);
          if (g && g.length>1) {
            found = f;
            return true;
          }
        }
      }
    }
    return false;
  }
  // Split existing features
  for (i=0; i<c.length-1; i++) {
    seg = [c[i],c[i+1]];
    var extent = ol.extent.buffer(ol.extent.boundingExtent(seg), this.tolerance_ /*0.01*/ );
    var g;
    while (true) {
      var found = false;
      this.source_.forEachFeatureIntersectingExtent(extent, intersect.bind(this));
      // Split feature
      if (found) {
        var f = found;
        this.source_.removeFeature(f);
        for (k=0; k<g.length; k++) {
          f2 = f.clone();
          f2.setGeometry(g[k]);
          this.source_.addFeature(f2);
        }
      }
      else break;
    }
  }
  // Auto intersect
  for (i=0; i<c.length-2; i++) {
    for (var j=i+1; j<c.length-1; j++) {
      var p = this.intersectSegs ([c[i],c[i+1]], [c[j],c[j+1]]);
      if (p && p!=c[i+1]) {
        split.push(p);
      }
    }
  }
  // Split original
  var splitOriginal = false;
  if (split.length) {
    var result = feature.getGeometry().splitAt(split, this.tolerance_);
    if (result.length>1) {
      for (k=0; k<result.length; k++) {
        f2 = feature.clone();
        f2.setGeometry(result[k]);
        this.source_.addFeature(f2);
      }
      splitOriginal = true;
    }
  }
  // End splitting
  setTimeout(function() {
    if (splitOriginal) this.source_.removeFeature(feature);
    this.source_.dispatchEvent({ type:'aftersplit', featureAdded: this.added_, featureRemoved: this.removed_, source: this.source_ });
    this.dispatchEvent({ type:'aftersplit', featureAdded: this.added_, featureRemoved: this.removed_, source: this.source_ });
    // Finish
    this.splitting = false;
  }.bind(this));
};
/** New feature source is added 
 * @private
*/
ol.interaction.Splitter.prototype.onAddFeature = function(e) {
  this.splitSource(e.feature);
  if (this.splitting) {
    this.added_.push(e.feature);
  }
};
/** Feature source is removed > count features added/removed
 * @private
*/
ol.interaction.Splitter.prototype.onRemoveFeature = function(e) {
  if (this.splitting) {
    var n = this.added_.indexOf(e.feature);
    if (n==-1) {
      this.removed_.push(e.feature);
    } else {
      this.added_.splice(n,1);
    }
  }
};
/** Feature source is changing 
 * @private
*/
ol.interaction.Splitter.prototype.onChangeFeature = function(e) {
  if (this.moving_) {
    this.lastEvent_ = e;
  } else {
    this.splitSource(e.feature, true);
  }
};

/*	Copyright (c) 2016 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** Interaction synchronize
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @param {*} options
 *  @param {Array<ol.Map>} options maps An array of maps to synchronize with the map of the interaction
 */
ol.interaction.Synchronize = function(options) {
  if (!options) options={};
  var self = this;
  ol.interaction.Interaction.call(this, {
    handleEvent: function(e) {
      if (e.type=="pointermove") { self.handleMove_(e); }
      return true;
    }
  });
  this.maps = options.maps || [];
  if (options.active === false) this.setActive(false);
};
ol.ext.inherits(ol.interaction.Synchronize, ol.interaction.Interaction);
/**
 * 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
 */
ol.interaction.Synchronize.prototype.setMap = function(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;
  ol.interaction.Interaction.prototype.setMap.call (this, 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
 */
ol.interaction.Synchronize.prototype.setActive = function (b) {
  ol.interaction.Interaction.prototype.setActive.call(this, b);
  this.syncMaps();
};
/** Synchronize the maps
 */
ol.interaction.Synchronize.prototype.syncMaps = function() {
  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
*/
ol.interaction.Synchronize.prototype.handleMove_ = function(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
*/
ol.interaction.Synchronize.prototype.handleMouseOut_ = function(/*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
*		- color {ol.color} color of the sparkles
*/
ol.interaction.TinkerBell = function(options) {
  options = options || {};
  ol.interaction.Pointer.call(this, {
    handleDownEvent: this.onMove,
    handleMoveEvent: this.onMove
  });
  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;
};
ol.ext.inherits(ol.interaction.TinkerBell, ol.interaction.Pointer);
/** Set the map > start postcompose
*/
ol.interaction.TinkerBell.prototype.setMap = function(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();
  }
  ol.interaction.Pointer.prototype.setMap.call(this, map);
  if (map) {
    this._listener = map.on('postcompose', this.postcompose_.bind(this));
    map.getViewport().addEventListener('mouseout', this.out_, false);
  }
};
ol.interaction.TinkerBell.prototype.onMove = function(e) {
  this.sparkle = e.pixel;
  this.isout_ = false;
  this.getMap().render();
};
/** Postcompose function
*/
ol.interaction.TinkerBell.prototype.postcompose_ = function(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 {olx.interaction.TouchCompass} 
 *	- onDrag {function|undefined} Function handling "drag" events. It provides a dpixel and a traction (in projection) vector form the center of the compas
 *	- size {Number} size of the compass in px, default 80
 *	- alpha {Number} opacity of the compass, default 0.5
 */
ol.interaction.TouchCompass = function(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;
	};
	ol.interaction.Pointer.call(this, opt);
	this.ondrag_ = options.onDrag;
	this.size = options.size || 80;
	this.alpha = options.alpha || 0.5;
	if (!ol.interaction.TouchCompass.prototype.compass)
	{	var canvas = ol.interaction.TouchCompass.prototype.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();
	}
};
ol.ext.inherits(ol.interaction.TouchCompass, ol.interaction.Pointer);
/** Compass Image as a JS Image object
* @api
*/
ol.interaction.TouchCompass.prototype.compass = null;
/**
 * 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
 */
ol.interaction.TouchCompass.prototype.setMap = function(map) {
	if (this._listener) ol.Observable.unByKey(this._listener);
	this._listener = null;
	ol.interaction.Pointer.prototype.setMap.call (this, 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
 */
ol.interaction.TouchCompass.prototype.setActive = function(b)
{	ol.interaction.Pointer.prototype.setActive.call (this, b);
	if (this.getMap()) {
		try { this.getMap().renderSync(); } catch(e) { /* ok */ }
	}
}
/**
 * Get the center of the compass
 * @param {_ol_coordinate_}
 * @private
 */
ol.interaction.TouchCompass.prototype.getCenter_ = function()
{	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
 */
ol.interaction.TouchCompass.prototype.drawCompass_ = function(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 = function(options) {
  options = options || {};
  // 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);
  // Add Overlay
  this.overlay = new ol.Overlay.Fixed({
    className: ('ol-touch-cursor '+(options.className||'')).trim(),
    positioning: 'top-left',
    element: ol.ext.element.create('DIV', {}),
    stopEvent: false,
  });
  ol.interaction.DragOverlay.call(this, {
    centerOnClick: false,
    //offset: [-20,-20],
    overlays: this.overlay
  });
  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))
};
ol.ext.inherits(ol.interaction.TouchCursor, ol.interaction.DragOverlay);
/**
 * 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
 */
ol.interaction.TouchCursor.prototype.setMap = function(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 = {};
  ol.interaction.DragOverlay.prototype.setMap.call (this, 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
 */
ol.interaction.TouchCursor.prototype.setActive = function(b, position) {
  if (b!==this.getActive()) {
    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);
    }
    ol.interaction.DragOverlay.prototype.setActive.call (this, 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
 */
ol.interaction.TouchCursor.prototype.setPosition = function (coord) {
  this.overlay.setPosition(coord, true); 
  if (this.getMap() && coord) {
    this._pixel = this.getMap().getPixelFromCoordinate(coord);
  }
};
/** Offset the target position
 * @param {ol.coordinate} coord
 */
ol.interaction.TouchCursor.prototype.offsetPosition = function (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}
 */
ol.interaction.TouchCursor.prototype.getPosition = function () {
  return this.overlay.getPosition(); 
};
/** Get pixel position
 * @return {ol.pixel}
 */
ol.interaction.TouchCursor.prototype.getPixel = function () {
  if (this.getMap()) return this.getMap().getPixelFromCoordinate(this.getPosition());
};
/** Get cursor overlay
 * @return {ol.Overlay}
 */
ol.interaction.TouchCursor.prototype.getOverlay = function () {
  return this.overlay; 
};
/** Get cursor overlay element
 * @return {Element}
 */
ol.interaction.TouchCursor.prototype.getOverlayElement = function () {
  return this.overlay.element; 
};
/** Get cursor button element
 * @param {string|number} button the button className or the button index
 * @return {Element}
 */
ol.interaction.TouchCursor.prototype.getButtonElement = function (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}
 */
ol.interaction.TouchCursor.prototype.removeButton = function (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
 */
ol.interaction.TouchCursor.prototype.addButton = function (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 = function(options) {
  options = options || {};
  // 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));
  // Create cursor
  ol.interaction.TouchCursor.call(this, {
    className: options.className,
    coordinate: options.coordinate,
  });
  this.getOverlayElement().classList.add('nodrawing');
  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))
};
ol.ext.inherits(ol.interaction.TouchCursorDraw, ol.interaction.TouchCursor);
/**
 * 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
 */
ol.interaction.TouchCursorDraw.prototype.setMap = function(map) {
  ol.interaction.TouchCursor.prototype.setMap.call (this, 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
 */
ol.interaction.TouchCursorDraw.prototype.setActive = function(b, position) {
  ol.interaction.TouchCursor.prototype.setActive.call (this, b, position);
  this.sketch.abortDrawing();
  this.sketch.setPosition(position);
  this.sketch.setVisible(b);
};
/**
 * Set Geometry type
 * @param {string} type Geometry type
 */
ol.interaction.TouchCursorDraw.prototype.setType = function(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
 */
ol.interaction.TouchCursorDraw.prototype.getType = function() {
  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 = function(options) {
  options = options || {};
  var drag = false;       // enable drag
  var dragging = false;   // dragging a point
  var del = false;        // deleting a point
  // 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;
    }
  });
  ol.interaction.TouchCursor.call(this, {
    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(this._lastEvent);
          mod.handleUpEvent(this._lastEvent);
          dragging = false;
        }.bind(this)
      }, { 
        // Remove a point
        className: 'ol-button-remove', 
        click: function() { 
          del = true;
          mod.handleDownEvent(this._lastEvent); 
          del = false;
        }.bind(this)
      }
    ]
  });
  // 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;
  });
};
ol.ext.inherits(ol.interaction.TouchCursorModify, ol.interaction.TouchCursor);
/**
 * 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
 */
ol.interaction.TouchCursorModify.prototype.setMap = function(map) {
  if (this.getMap()) {
    this.getMap().removeInteraction(this._modify);
  }
  if (map) {
    map.addInteraction(this._modify);
  }
  ol.interaction.TouchCursor.prototype.setMap.call (this, 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
 */
ol.interaction.TouchCursorModify.prototype.setActive = function(b, position) {
  ol.interaction.TouchCursor.prototype.setActive.call (this, b, position);
  this._modify.setActive(b);
};
/**
 * Get the modify interaction.
 * @retunr {ol.interaction.ModifyFeature} 
 * @observable
 * @api
 */
ol.interaction.TouchCursorModify.prototype.getInteraction = function() {
  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 = function(options) {
  options = options || {};
  ol.interaction.TouchCursor.call(this, {
    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() });
};
ol.ext.inherits(ol.interaction.TouchCursorSelect, ol.interaction.TouchCursor);
/**
 * 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
 */
ol.interaction.TouchCursorSelect.prototype.setMap = function(map) {
  ol.interaction.TouchCursor.prototype.setMap.call (this, 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}
 */
ol.interaction.TouchCursorSelect.prototype.getSelection = function() {
  return this._selection ? this._selection.feature : null;
};
/** Set position
 * @param {ol.coordinate} coord
 */
ol.interaction.TouchCursorSelect.prototype.setPosition = function(coord) {
  ol.interaction.TouchCursor.prototype.setPosition.call (this, coord);
  this.select();
};
/** Select feature 
 * @param {ol.Feature|undefined} f a feature to select or select at the cursor position
 */
ol.interaction.TouchCursorSelect.prototype.select = function(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 {} options.style list of ol.style for handles
 *
 */
ol.interaction.Transform = function(options) {
  if (!options) options = {};
	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')||'')]);
    },
  });
  // Extend pointer
  ol.interaction.Pointer.call(this, {
    handleDownEvent: this.handleDownEvent_,
    handleDragEvent: this.handleDragEvent_,
    handleMoveEvent: this.handleMoveEvent_,
    handleUpEvent: this.handleUpEvent_
  });
  // 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; };
  /* 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));
  // Force redraw when changed
  this.on ('propertychange', function() {
    this.drawSketch_();
  });
  // setstyle
  this.setDefaultStyle();
};
ol.ext.inherits(ol.interaction.Transform, ol.interaction.Pointer);
/** 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'
};
/**
 * 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
 */
ol.interaction.Transform.prototype.setMap = function(map) {
  var oldMap = this.getMap();
  if (oldMap) {
    var targetElement = oldMap.getTargetElement();
    oldMap.removeLayer(this.overlayLayer_);
    if (this.previousCursor_ && targetElement) {
      targetElement.style.cursor = this.previousCursor_;
    }
    this.previousCursor_ = undefined;
  }
  ol.interaction.Pointer.prototype.setMap.call (this, 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
 */
ol.interaction.Transform.prototype.setActive = function(b) {
  this.select(null);
  this.overlayLayer_.setVisible(b);
  ol.interaction.Pointer.prototype.setActive.call (this, b);
};
/** Set default sketch style
 * @param {Object|undefined} options
 *  @param {ol.style.Stroke} stroke stroke style for selection rectangle
 *  @param {ol.style.Fill} fill fill style for selection rectangle
 *  @param {ol.style.Stroke} pointStroke stroke style for handles
 *  @param {ol.style.Fill} pointFill fill style for handles
 */
ol.interaction.Transform.prototype.setDefaultStyle = function(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,
      points: 15
    });
  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
 */
ol.interaction.Transform.prototype.setStyle = function(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
 */
ol.interaction.Transform.prototype.getFeatureAtPixel_ = function(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
 */
ol.interaction.Transform.prototype.getGeometryRotateToZero_ = function(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
 */
ol.interaction.Transform.prototype._isRectangle = function(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
*/
ol.interaction.Transform.prototype.drawSketch_ = function(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, 0);
  this.selection_.forEach(function (f) {
    var extendExt = this.getGeometryRotateToZero_(f).getExtent();
    ol.extent.extend(ext, extendExt);
  }.bind(this));
  if (center===true) {
    if (!this.ispt_) {
      this.overlayLayer_.getSource().addFeature(new ol.Feature( { geometry: new ol.geom.Point(this.center_), handle:'rotate0' }) );
      geom = ol.geom.Polygon.fromExtent(ext);
      if (this.get('enableRotatedTransform') && viewRotation !== 0) {
        geom.rotate(viewRotation, this.getMap().getView().getCenter())
      }
      f = this.bbox_ = new ol.Feature(geom);
      this.overlayLayer_.getSource().addFeature (f);
    }
  } else {
    if (this.ispt_) {
      // Calculate extent arround the point
      var p = this.getMap().getPixelFromCoordinate([ext[0], ext[1]]);
      if (p) {
        ext = ol.extent.boundingExtent([
          this.getMap().getCoordinateFromPixel([p[0]-10, p[1]-10]),
          this.getMap().getCoordinateFromPixel([p[0]+10, p[1]+10])
        ]);
      }
    }
    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_) {
      features.push(f);
      // Middle
      if (!this.iscircle_ && 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
*/
ol.interaction.Transform.prototype.select = function(feature, add) {
  if (!feature) {
    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
*/
ol.interaction.Transform.prototype.setSelection = function(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
 */
ol.interaction.Transform.prototype.watchFeatures_ = function() {
  // 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
 */
ol.interaction.Transform.prototype.handleDownEvent_ = function(evt) {
  if (!this._handleEvent(evt, this.selection_)) return;
  var sel = this.getFeatureAtPixel_(evt.pixel);
  var feature = sel.feature;
  if (this.selection_.getLength()
    && this.selection_.getArray().indexOf(feature) >= 0
    && ((this.ispt_ && this.get('translate')) || this.get('translateFeature'))
  ){
    sel.handle = 'translate';
  }
  if (sel.handle) {
    this.mode_ = sel.handle;
    this.opt_ = sel.option;
    this.constraint_ = sel.constraint;
    // Save info
    var viewRotation = this.getMap().getView().getRotation();
    this.coordinate_ = evt.coordinate;
    this.pixel_ = evt.pixel;
    this.geoms_ = [];
    this.rotatedGeoms_ = [];
    var extent = ol.extent.createEmpty();
    var rotExtent = ol.extent.createEmpty();
    for (var i=0, f; f=this.selection_.item(i); i++) {
      this.geoms_.push(f.getGeometry().clone());
      extent = ol.extent.extend(extent, f.getGeometry().getExtent());
      if (this.get('enableRotatedTransform') && viewRotation !== 0) {
        var rotGeom = this.getGeometryRotateToZero_(f, true);
        this.rotatedGeoms_.push(rotGeom);
        rotExtent = ol.extent.extend(rotExtent, rotGeom.getExtent());
      }
    }
    this.extent_ = (ol.geom.Polygon.fromExtent(extent)).getCoordinates()[0];
    if (this.get('enableRotatedTransform') && viewRotation !== 0) {
      this.rotatedExtent_ = (ol.geom.Polygon.fromExtent(rotExtent)).getCoordinates()[0];
    }
    if (this.mode_==='rotate') {
      this.center_ = this.getCenter() || ol.extent.getCenter(extent);
      // we are now rotating (cursor down on rotate mode), so apply the grabbing cursor
      var element = evt.map.getTargetElement();
      element.style.cursor = this.Cursors.rotate0;
      this.previousCursor_ = element.style.cursor;
    } else {
      this.center_ = ol.extent.getCenter(extent);
    }
    this.angle_ = Math.atan2(this.center_[1]-evt.coordinate[1], this.center_[0]-evt.coordinate[0]);
    this.dispatchEvent({
      type: this.mode_+'start',
      feature: this.selection_.item(0), // backward compatibility
      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 features to transform
 * @return {ol.Collection<ol.Feature>}
 */
ol.interaction.Transform.prototype.getFeatures = function() {
  return this.selection_;
};
/**
 * Get the rotation center
 * @return {ol.coordinates|undefined}
 */
ol.interaction.Transform.prototype.getCenter = function() {
  return this.get('center');
};
/**
 * Set the rotation center
 * @param {ol.coordinates|undefined} c the center point, default center on the objet
 */
ol.interaction.Transform.prototype.setCenter = function(c) {
  return this.set('center', c);
}
function 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];
}
function countVector(start, end) {
  return [end[0] - start[0], end[1] - start[1]];
}
function movePoint(point, displacementVector) {
  return [point[0]+displacementVector[0], point[1]+displacementVector[1]];
}
/**
 * @param {ol.MapBrowserEvent} evt Map browser event.
 * @private
 */
ol.interaction.Transform.prototype.handleDragEvent_ = function(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) ? countVector(pointA, pointB) : countVector(pointD, pointA);
              var projectedVector = 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 = countVector(pointD, dragCoordinate);
                  projectedLeft = projectVectorOnVector(displacementVector, countVector(pointC, pointD));
                  projectedRight = projectVectorOnVector(displacementVector, countVector(pointA, pointD));
                  [g2[0], g2[1]] = movePoint(pointA, projectedLeft);
                  [g2[4], g2[5]] = movePoint(pointC, projectedRight);
                  [g2[6], g2[7]] = movePoint(pointD, displacementVector);
                  [g2[8], g2[9]] = movePoint(pointA1, projectedLeft);
                  break;
                case 1:
                  displacementVector = countVector(pointA, dragCoordinate);
                  projectedLeft = projectVectorOnVector(displacementVector, countVector(pointD, pointA));
                  projectedRight = projectVectorOnVector(displacementVector, countVector(pointB, pointA));
                  [g2[0], g2[1]] = movePoint(pointA, displacementVector);
                  [g2[2], g2[3]] = movePoint(pointB, projectedLeft);
                  [g2[6], g2[7]] = movePoint(pointD, projectedRight);
                  [g2[8], g2[9]] = movePoint(pointA1, displacementVector);
                  break;
                case 2:
                  displacementVector = countVector(pointB, dragCoordinate);
                  projectedLeft = projectVectorOnVector(displacementVector, countVector(pointA, pointB));
                  projectedRight = projectVectorOnVector(displacementVector, countVector(pointC, pointB));
                  [g2[0], g2[1]] = movePoint(pointA, projectedRight);
                  [g2[2], g2[3]] = movePoint(pointB, displacementVector);
                  [g2[4], g2[5]] = movePoint(pointC, projectedLeft);
                  [g2[8], g2[9]] = movePoint(pointA1, projectedRight);
                  break;
                case 3:
                  displacementVector = countVector(pointC, dragCoordinate);
                  projectedLeft = projectVectorOnVector(displacementVector, countVector(pointB, pointC));
                  projectedRight = projectVectorOnVector(displacementVector, countVector(pointD, pointC));
                  [g2[2], g2[3]] = movePoint(pointB, projectedRight);
                  [g2[4], g2[5]] = movePoint(pointC, displacementVector);
                  [g2[6], g2[7]] = movePoint(pointD, projectedLeft);
                  break;
              }
            }
          }
          // bug: ol, bad calculation circle geom extent
          if (geometry.getType() == 'Circle') geometry.setCenterAndRadius(geometry.getCenter(), geometry.getRadius());
          return g2;
        });
        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
 */
ol.interaction.Transform.prototype.handleMoveEvent_ = function(evt) {
  if (!this._handleEvent(evt, this.features_)) return;
  // console.log("handleMoveEvent");
  if (!this.mode_) {
    var sel = this.getFeatureAtPixel_(evt.pixel);
    var element = evt.map.getTargetElement();
    if (sel.feature) {
      var c = sel.handle ? this.Cursors[(sel.handle||'default')+(sel.constraint||'')+(sel.option||'')] : this.Cursors.select;
      if (this.previousCursor_===undefined) {
        this.previousCursor_ = element.style.cursor;
      }
      element.style.cursor = c;
    } else {
      if (this.previousCursor_!==undefined) element.style.cursor = this.previousCursor_;
      this.previousCursor_ = undefined;
    }
  }
};
/**
 * @param {ol.MapBrowserEvent} evt Map browser event.
 * @return {boolean} `false` to stop the drag sequence.
 */
ol.interaction.Transform.prototype.handleUpEvent_ = function(evt) {
  // remove rotate0 cursor on Up event, otherwise it's stuck on grab/grabbing
  if (this.mode_ === 'rotate') {
    var element = evt.map.getTargetElement();
    element.style.cursor = this.Cursors.default;
    this.previousCursor_ = undefined;
  }
  //dispatchEvent
  this.dispatchEvent({
    type:this.mode_+'end',
    feature: this.selection_.item(0),
    features: this.selection_,
    oldgeom: this.geoms_[0],
    oldgeoms: this.geoms_
  });
  this.drawSketch_();
  this.mode_ = null;
  return false;
};
/** Get the features that are selected for transform
 * @return ol.Collection
 */
ol.interaction.Transform.prototype.getFeatures = function() {
  return this.selection_;
};

/** 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 = function(options) {
  if (!options) options = {};
	ol.interaction.Interaction.call(this, {	
    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 = {};
};
ol.ext.inherits(ol.interaction.UndoRedo, ol.interaction.Interaction);
/** 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
 */
ol.interaction.UndoRedo.prototype.define = function(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}
 */
ol.interaction.UndoRedo.prototype.length = function(type) {
  return (type==='redo') ? this._redo.length : this._undo.length;
};
/** Set undo stack max length
 * @param {number} length
 */
ol.interaction.UndoRedo.prototype.setMaxLength = function(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}
 */
ol.interaction.UndoRedo.prototype.size = function(type) {
  return (type==='redo') ? this._redoStack.getLength() : this._undoStack.getLength();
};
/** Set undo stack max size
 * @param {number} size
 */
ol.interaction.UndoRedo.prototype.setMaxSize = function(size) {
  size = parseInt(size);
  if (size && size<0) size = 0;
  this.set('maxSize', size);
  this._reduce();
};
/** Reduce stack: shift undo to set size
 * @private
 */
ol.interaction.UndoRedo.prototype._reduce = function() {
  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<*>}
 */
ol.interaction.UndoRedo.prototype.getStack = function(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
 */
ol.interaction.UndoRedo.prototype.push = function(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.
 */
ol.interaction.UndoRedo.prototype.shift = function() {
  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
 */
ol.interaction.UndoRedo.prototype.setActive = function(active) {
  ol.interaction.Interaction.prototype.setActive.call (this, 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
 */
ol.interaction.UndoRedo.prototype.setMap = function(map) {
  if (this._mapListener) {
    this._mapListener.forEach(function(l) { ol.Observable.unByKey(l); })
  }
  this._mapListener = [];
  ol.interaction.Interaction.prototype.setMap.call (this, 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
 */
ol.interaction.UndoRedo.prototype._watchSources = function() {
  var map = this.getMap();
  // Clear listeners
  if (this._sourceListener) {
    this._sourceListener.forEach(function(l) { ol.Observable.unByKey(l); })
  }
  this._sourceListener = [];
  var self = this;
  // Ges vector layers 
  function getVectorLayers(layers, init) {
    if (!init) init = [];
    layers.forEach(function(l) {
      if (l instanceof ol.layer.Vector) {
        if (!self._layers || self._layers.indexOf(l) >= 0) {
          init.push(l);
        }
      } else if (l.getLayers) {
        getVectorLayers(l.getLayers(), init);
      }
    });
    return init;
  }
  if (map) {
    // Watch the vector sources in the map 
    var vectors = getVectorLayers(map.getLayers());
    vectors.forEach((function(l) {
      var s = l.getSource();
      this._sourceListener.push( s.on(['addfeature', 'removefeature'], this._onAddRemove.bind(this)) );
      this._sourceListener.push( s.on('clearstart', function() {
        this.blockStart('clear')
      }.bind(this)));
      this._sourceListener.push( s.on('clearend', this.blockEnd.bind(this)) );
    }).bind(this));
    // Watch new inserted/removed
    this._sourceListener.push( map.getLayers().on(['add', 'remove'], this._watchSources.bind(this) ) );
  }
};
/** Watch for interactions
 * @private
 */
ol.interaction.UndoRedo.prototype._watchInteractions = function() {
  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
 */
ol.interaction.UndoRedo.prototype._onAddRemove = function(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
 */
ol.interaction.UndoRedo.prototype._onInteraction = function(e) {
  var fn = this._onInteraction[e.type];
  if (fn) fn.call(this,e);
};
/** 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();
};
/** Start an undo block
 * @param {string} [name] name f the action
 * @api
 */
ol.interaction.UndoRedo.prototype.blockStart = function (name) {
  this._redoStack.clear();
  this._redo.length = 0;
  this._undoStack.push({ 
    type: 'blockstart', 
    name: name
  });
  this._level++;
};
/** @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');
}
/** End an undo block
 * @api
 */
ol.interaction.UndoRedo.prototype.blockEnd = function () {
  this._undoStack.push({ type: 'blockend' });
  this._level--;
};
/** @private
 */
ol.interaction.UndoRedo.prototype._onInteraction.aftersplit =
ol.interaction.UndoRedo.prototype._onInteraction.deleteend =
ol.interaction.UndoRedo.prototype.blockEnd;
/** handle undo/redo
 * @private
 */
ol.interaction.UndoRedo.prototype._handleDo = function(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
 */
ol.interaction.UndoRedo.prototype.undo = function() {
  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
 */
ol.interaction.UndoRedo.prototype.redo = function() {
  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
 */
ol.interaction.UndoRedo.prototype.clear = function() {
  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
 */
ol.interaction.UndoRedo.prototype.hasUndo = function() {
  return this._undoStack.getLength();
};
/** Check if redo is avaliable
 * @return {number} the number of redo
 * @api
 */
ol.interaction.UndoRedo.prototype.hasRedo = function() {
  return this._redoStack.getLength();
};

/*	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 = function (options) {
  options = options || {};
  this._bindModify = this._onModifyFeature.bind(this);
  this._watch = true;
  ol.source.Vector.call(this, options);
  this._origin = options.source;
  this._listen = (options.listenChange !== false);
  // Geometry function
  this._geomFn = options.geometryFunction || ol.coordinate.getFeatureCenter || function (f) { return f.getGeometry().getFirstCoordinate(); };
  // Existing features
  this.reset();
  // 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;
};
ol.ext.inherits(ol.source.BinBase, ol.source.Vector);
/**
 * On add feature
 * @param {ol.events.Event} e
 * @param {ol.Feature} bin
 * @private
 */
ol.source.BinBase.prototype._onAddFeature = function (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
 */
ol.source.BinBase.prototype._onRemoveFeature = function (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
 */
ol.source.BinBase.prototype._onClearFeature = function (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
 */
ol.source.BinBase.prototype.getBin = function (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
 */
ol.source.BinBase.prototype.getGridGeomAt = function (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
 */
ol.source.BinBase.prototype.getBinAt = function (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
 */
ol.source.BinBase.prototype._onModifyFeature = function (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. 
 */
ol.source.BinBase.prototype.reset = function () {
  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>}
 */
ol.source.BinBase.prototype.getGridFeatures = function () {
  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
 */
ol.source.BinBase.prototype._flatAttributes = function(/*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
 */
 ol.source.BinBase.prototype.setFlatAttributesFn = function(fn) {
  if (typeof(fn)==='function') this._flatAttributes = fn;
};
/**
 * Get the orginal source
 * @return {ol.source.Vector}
 */
ol.source.BinBase.prototype.getSource = function () {
  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 = function(opt_options) {
  var options = opt_options || {};
  options.loader = this._loaderFn;
  /** Url for DBPedia SPARQL */
  this._url = options.url || "http://fr.dbpedia.org/sparql";
  /** Max resolution to load features  */
  this._maxResolution = options.maxResolution || 100;
  /** Result language */
  this._lang = options.lang || "fr";
  /** Query limit */
  this._limit = options.limit || 1000;
  /** Default attribution */
  if (!options.attributions) options.attributions = [ "&copy; <a href='http://dbpedia.org/'>DBpedia</a> CC-by-SA" ];
  // Bbox strategy : reload at each move
    if (!options.strategy) options.strategy = ol.loadingstrategy.bbox;
  ol.source.Vector.call (this, options);
};
ol.ext.inherits(ol.source.DBPedia, ol.source.Vector);
/** 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
*/
ol.source.DBPedia.prototype.readFeature = function (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
*/
ol.source.DBPedia.prototype.querySubject = function () {
  return "?subject rdfs:label ?label. "
    + "OPTIONAL {?subject dbpedia-owl:thumbnail ?thumbnail}."
    + "OPTIONAL {?subject dbpedia-owl:abstract ?abstract} . "
    + "OPTIONAL {?subject rdf:type ?type}";
}
/** Set RDF query filter, default: select language
* @API stable
*/
ol.source.DBPedia.prototype.queryFilter = function () {
  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
*/
ol.source.DBPedia.prototype._loaderFn = function(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 = function(options) {
	options = options || {};
	options.loader = this._calcGrid;
  options.strategy =  function(extent, resolution) {
    if (this.resolution && this.resolution != resolution){
      this.clear();
      this.refresh();
    }
    return [extent];
  }
  this._bbox = [[0,1600000],[11*100000, 1600000+10*100000]];
  ol.source.Vector.call (this, options);
  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);
};
ol.ext.inherits(ol.source.DFCI, ol.source.Vector);
/** Cacluate grid according extent/resolution
 */
ol.source.DFCI.prototype._calcGrid = function (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
 */
ol.source.DFCI.prototype._midPt = function(p1,p2) {
  return [(p1[0]+p2[0])/2, (p1[1]+p2[1])/2];
};
/**
 * Get feature with geom
 * @private
 */
ol.source.DFCI.prototype._trFeature = function(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
 * 
 */
ol.source.DFCI.prototype._getFeatures = function (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 = function(options) {
  options = options || {};
  options.loader = this._loader
  options.strategy = ol.loadingstrategy.all;
  ol.source.Vector.call (this, options);
  this.set('time', options.time || new Date());
  this.set('step', options.step || 1);
};
ol.ext.inherits(ol.source.DayNight, ol.source.Vector);
(function(){
/** Loader
 * @private
 */
ol.source.DayNight.prototype._loader = function(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
 */
ol.source.DayNight.prototype.setTime = function(time) {
  this.set('time', time);
  this.refresh();
};
/** 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
 */
function _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
 */
function _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
 */
function _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 sun coordinates on earth
 * @param {string} time DateTime string, default yet
 * @returns {ol.coordinate} position in lonlat
 */
ol.source.DayNight.prototype.getSunPosition = function (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 = _sunEclipticPosition(julianDay);
  var eclObliq = _eclipticObliquity(julianDay);
  var sunEqPos = _sunEquatorialPosition(sunEclPos.lambda, eclObliq);
  return [sunEqPos.alpha - gst * 15, sunEqPos.delta]
};
/** Get the day/night separation latitude
 * @param {number} lon
 * @param {Date} time
 * @returns {number}
 */
ol.source.DayNight.getNightLat = function (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 = _sunEclipticPosition(julianDay);
  var eclObliq = _eclipticObliquity(julianDay);
  var sunEqPos = _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;
};
/** 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>>}
 */
ol.source.DayNight.prototype.getCoordinates = function (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 = _sunEclipticPosition(julianDay);
  var eclObliq = _eclipticObliquity(julianDay);
  var sunEqPos = _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 = function(options) {
  options = options || {};
  this._nodes = options.source;
  delete options.source;
  ol.source.Vector.call (this, options);
  // 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)
};
ol.ext.inherits(ol.source.Delaunay, ol.source.Vector);
/** Clear source (and points)
 * @param {boolean} opt_fast
 */
ol.source.Delaunay.prototype.clear = function(opt_fast) {
  ol.source.Vector.prototype.clear.call(this, opt_fast);
  this.getNodeSource().clear(opt_fast);
};
/** Add a new triangle in the source
 * @param {Array<ol/coordinates>} pts
 */
ol.source.Delaunay.prototype._addTriangle = function(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 
 */
ol.source.Delaunay.prototype.getNodes = function () {
  return this._nodes.getFeatures();
};
/** Get nodes source
 */
ol.source.Delaunay.prototype.getNodeSource = function () {
  return this._nodes;
};
/**
 * A point has been removed
 * @param {ol/source/Vector.Event} evt 
 */
ol.source.Delaunay.prototype._onRemoveNode = function(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 
 */
ol.source.Delaunay.prototype._onAddNode = function(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
 */
ol.source.Delaunay.prototype.flipTriangles = function ()	{
  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}
 */
ol.source.Delaunay.prototype.intersectSegs = 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;
    // 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}
 */
ol.source.Delaunay.prototype._ptInTriangle = function(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
 */
ol.source.Delaunay.prototype.listpt = function (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}
 */
ol.source.Delaunay.prototype.inCircle = function (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 {*}
 */
ol.source.Delaunay.prototype.getCircumCircle = function (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
 */
ol.source.Delaunay.prototype.getTrianglesAt = function(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
 */
ol.source.Delaunay.prototype.getNodesAt = function(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 > }
 */
ol.source.Delaunay.prototype.calculateVoronoi = function(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 = function (options) {
  options = 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 || [] });
  }
  ol.source.BinBase.call(this, options);
};
ol.ext.inherits(ol.source.FeatureBin, ol.source.BinBase);
/** Set features to use as bin collector
 * @param {ol.Feature} features
 */
ol.source.FeatureBin.prototype.setFeatures = function (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
 */
ol.source.FeatureBin.prototype.getGridGeomAt = function (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 = function(opt_options) {
  var options = { 
    attributions: opt_options.attributions,
    logo: opt_options.logo,
    projection: opt_options.projection
  };
  // 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
  var self = this;
  this._image.onload = function() {
    self.setCrop (self.crop);
    self.changed();
  }
  if (!opt_options.image) this._image.src = opt_options.url;
  // Draw image on canvas
  options.canvasFunction = this.calculateImage;
  ol.source.ImageCanvas.call (this, options);	
  // 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));
};
ol.ext.inherits(ol.source.GeoImage, ol.source.ImageCanvas);
/** calculate image at extent / resolution
 * @param {ol/extent/Extent} extent
 * @param {number} resolution
 * @param {number} pixelRatio
 * @param {ol/size/Size} size
 * @return {HTMLCanvasElement}
 */
ol.source.GeoImage.prototype.calculateImage = function(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
 */
ol.source.GeoImage.prototype.getCenter = function() {
  return this.center;
}
/**
 * Set coordinate of the image center.
 * @param {ol.Coordinate} coordinate of the image center.
 * @api stable
 */
ol.source.GeoImage.prototype.setCenter = function(center) {
  this.center = center;
  this.changed();
}
/**
 * Get image scale.
 * @return {ol.size} image scale (along x and y axis).
 * @api stable
 */
ol.source.GeoImage.prototype.getScale = function() {
  return this.scale;
}
/**
 * Set image scale.
 * @param {ol.size|Number} image scale (along x and y axis or both).
 * @api stable
 */
ol.source.GeoImage.prototype.setScale = function(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 degre.
 * @api stable
 */
ol.source.GeoImage.prototype.getRotation = function() {
  return this.rotate;
};
/**
 * Set image rotation.
 * @param {Number} rotation in radian.
 * @api stable
 */
ol.source.GeoImage.prototype.setRotation = function(angle) {
  this.rotate = angle;
  this.changed();
};
/**
 * Get the image.
 * @api stable
 */
ol.source.GeoImage.prototype.getGeoImage = function() {
  return this._image;
};
/**
 * Get image crop extent.
 * @return {ol.extent} image crop extent.
 * @api stable
 */
ol.source.GeoImage.prototype.getCrop = function() {
  return this.crop;
};
/**
 * Set image mask.
 * @param {ol.geom.LineString} coords of the mask
 * @api stable
 */
ol.source.GeoImage.prototype.setMask = function(mask) {
  this.mask = mask;
  this.changed();
};
/**
 * Get image mask.
 * @return {ol.geom.LineString} coords of the mask
 * @api stable
 */
ol.source.GeoImage.prototype.getMask = function() {
  return this.mask;
};
/**
 * Set image crop extent.
 * @param {ol.extent|Number} image crop extent or a number to crop from original size.
 * @api stable
 */
ol.source.GeoImage.prototype.setCrop = function(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}
 */
ol.source.GeoImage.prototype.getExtent = function(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}
 */
ol.source.GeoImage.prototype.calculateExtent = function(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 = function(options) {
  options = options || {};
  options.loader = this._loaderFn;
  ol.source.Vector.call (this, options);
};
ol.ext.inherits(ol.source.GeoRSS, ol.source.Vector);
/** Loader function used to load features.
* @private
*/
ol.source.GeoRSS.prototype._loaderFn = function(extent, resolution, projection){
  // Ajax request to get source
  ol.ext.Ajax.get({
    url: this.getUrl(),
    dataType: 'XML',
    error: function(){ console.log('oops'); },
    success: function(xml) {
      var features = (new ol.format.GeoRSS()).readFeatures(xml, { featureProjection: projection });
      this.addFeatures(features);
    }.bind(this)
  });
};

/*	Copyright (c) 2019 Jean-Marc VIGLINO,
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** IGN's Geoportail WMTS source
 * @constructor
 * @extends {ol.source.WMTS}
 * @param {olx.source.Geoportail=} options WMTS options 
 *  @param {string=} options.layer Geoportail layer name
 *  @param {number} options.minZoom
 *  @param {number} options.maxZoom
 *  @param {string} options.server
 *  @param {string} options.gppKey api key, default 'choisirgeoportail'
 *  @param {string} options.authentication basic authentication associated with the gppKey as btoa("login:pwd")
 *  @param {string} options.format image format, default 'image/jpeg'
 *  @param {string} options.style layer style, default 'normal'
 *  @param {string} options.crossOrigin default 'anonymous'
 *  @param {string} options.wrapX default true
 */
ol.source.Geoportail = function (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.prototype.attribution ];
  if (options.attributions) attr = options.attributions;
  this._server = options.server || 'https://wxs.ign.fr/geoportail/wmts';
  this._gppKey = options.gppKey || options.key || 'choisirgeoportail';
  var wmts_options = {
    url: this.serviceURL(),
    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)
  };
  ol.source.WMTS.call(this, wmts_options);
  // Load url using basic authentification
  if (options.authentication) {
    this.setTileLoadFunction(ol.source.Geoportail.tileLoadFunctionWithAuthentication(options.authentication, this.getFormat()));
  }
};
ol.ext.inherits(ol.source.Geoportail, ol.source.WMTS);
/** Standard IGN-GEOPORTAIL attribution 
*/
ol.source.Geoportail.prototype.attribution = '<a href="http://www.geoportail.gouv.fr/">Géoportail</a> &copy; <a href="http://www.ign.fr/">IGN-France</a>';
/** Get service URL according to server url or standard url
*/
ol.source.Geoportail.prototype.serviceURL = function() {
  if (this._server) {
    return this._server.replace (/^(https?:\/\/[^/]*)(.*)$/, "$1/"+this._gppKey+"$2") ;
  } else {
    return (window.geoportailConfig ? window.geoportailConfig.url : "https://wxs.ign.fr/") +this._gppKey+ "/geoportail/wmts" ;
  }
};
/**
 * Return the associated API key of the Map.
 * @function
 * @return the API key.
 * @api stable
 */
ol.source.Geoportail.prototype.getGPPKey = function() {
  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
 */
ol.source.Geoportail.prototype.setGPPKey = function(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.
 */
ol.source.Geoportail.prototype.getFeatureInfoUrl  = function(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
 */
ol.source.Geoportail.prototype.getFeatureInfo = function(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(){}
  })
};
/** 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
 */
ol.source.Geoportail.tileLoadFunctionWithAuthentication = function(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();
  };
};

/*	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 = function (options) {
  options = options || {};
  ol.source.BinBase.call(this, options);
  this.set('gridProjection', options.gridProjection || 'EPSG:4326');
  this.set('size', options.size || 1);
};
ol.ext.inherits(ol.source.GridBin, ol.source.BinBase);
/** Set grid projection
 * @param {ol.ProjectionLike} proj
 */
ol.source.GridBin.prototype.setGridProjection = function (proj) {
  this.set('gridProjection', proj);
  this.reset();
};
/** Set grid size
 * @param {number} size
 */
ol.source.GridBin.prototype.setSize = function (size) {
  this.set('size', size);
  this.reset();
};
/** Get the grid geometry at the coord 
 * @param {ol.Coordinate} coord
 * @returns {ol.geom.Polygon} 
 * @api
 */
ol.source.GridBin.prototype.getGridGeomAt = function (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 = function (options) {
  options = options || {};
  /** The HexGrid
   * 	@type {ol.HexGrid}
   */
  this._hexgrid = new ol.HexGrid(options);
  ol.source.BinBase.call(this, options);
};
ol.ext.inherits(ol.source.HexBin, ol.source.BinBase);
/** Get the hexagon geometry at the coord 
 * @param {ol.Coordinate} coord
 * @returns {ol.geom.Polygon} 
 * @api
 */
ol.source.HexBin.prototype.getGridGeomAt = function (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)
 */
ol.source.HexBin.prototype.setSize = function (newSize, noreset) {
  this._hexgrid.setSize(newSize);
  if (!noreset) {
    this.reset();
  }
}
/**	Get the inner HexGrid size.
 * 	@return {number}
 */
ol.source.HexBin.prototype.getSize = function () {
  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)
 */
ol.source.HexBin.prototype.setLayout = function (newLayout, noreset) {
  this._hexgrid.setLayout(newLayout);
  if (!noreset) {
    this.reset();
  }
}
/**	Get the inner HexGrid layout.
 * 	@return {HexagonLayout}
 */
ol.source.HexBin.prototype.getLayout = function () {
  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)
 */
ol.source.HexBin.prototype.setOrigin = function (newLayout, noreset) {
  this._hexgrid.setOrigin(newLayout);
  if (!noreset) {
    this.reset();
  }
};
/**	Get the inner HexGrid origin.
 * 	@return {ol.Coordinate}
 */
ol.source.HexBin.prototype.getOrigin = function () {
  return this._hexgrid.getOrigin();
};
/**
 * Get hexagons without circular dependencies (vs. getFeatures)
 * @return {Array<ol.Feature>}
 */
ol.source.HexBin.prototype.getHexFeatures = function () {
  return ol.source.BinBase.prototype.getGridFeatures.call(this);
};

/*	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}
 * @param {*} [options]
 *  @param {ol.source.vector} options.source a source to interpolate
 *  @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 = function(options) {
  options = options || {};
  // Draw image on canvas
  options.canvasFunction = this.calculateImage;
  this._source = options.source;
  this._canvas = document.createElement('CANVAS');
  this._source.on(['addfeature','removefeature','clear','removefeature'], function() {
    this.changed();
  }.bind(this));
  ol.source.ImageCanvas.call (this, options);
  this.set('scale', options.scale || 4);
  this._weight = typeof(options.weight) === 'function' ? options.weight : function(f) { return f.get(options.weight||'weight'); }
};
ol.ext.inherits(ol.source.IDW, ol.source.ImageCanvas);
/** Get the source
 */
ol.source.IDW.prototype.getSource = function() {
  return this._source;
};
/** Convert hue to rgb factor
 * @param {number} h
 * @return {number}
 * @private
 */
ol.source.IDW.prototype.hue2rgb = function(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
 */
ol.source.IDW.prototype.getColor = function(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
  ]
};
/** 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
 */
ol.source.IDW.prototype.setData = function(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}
 */
ol.source.IDW.prototype.getValue = function(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);
};
/** Calculate IDW at extent / resolution
 * @param {ol/extent/Extent} extent
 * @param {number} resolution
 * @param {number} pixelRatio
 * @param {ol/size/Size} size
 * @return {HTMLCanvasElement}
 * @private
 */
ol.source.IDW.prototype.calculateImage = function(extent, resolution, pixelRatio, size) {
  if (!this._source) return this._canvas;
  // Calculation canvas at small resolution
  var canvas = document.createElement('CANVAS');
  var width = canvas.width = Math.round(size[0] / (this.get('scale')*pixelRatio));
  var height = canvas.height = Math.round(size[1] / (this.get('scale')*pixelRatio));
  var ctx = canvas.getContext('2d');
  var imageData = ctx.getImageData(0, 0, width, height);
  // 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));
  // 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;
      }
      this.setData(t/b, imageData.data, (y*width + x)*4);
    }
  }
  ctx.putImageData(imageData, 0, 0);
  /* DEBUG : Draw points * /
  pts.forEach(function(p) {
    ctx.fillRect(p[0], p[1], 1, 1);
  });
  /**/
  // Draw full resolution canvas
  this._canvas.width = Math.round(size[0]);
  this._canvas.height = Math.round(size[1]);
  this._canvas.getContext('2d').drawImage(canvas, 0, 0, size[0], size[1]);
  return this._canvas;
}

/*	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 = function (options) {
  options = options || {};
  this._grid = new ol.InseeGrid({ size: options.size });
  ol.source.BinBase.call(this, options);
};
ol.ext.inherits(ol.source.InseeBin, ol.source.BinBase);
/** Set grid size
 * @param {number} size
 */
ol.source.InseeBin.prototype.setSize = function (size) {
  if (this.getSize() !== size) {
    this._grid.set('size', size);
    this.reset();
  }
};
/** Get grid size
 * @return {number} size
 */
ol.source.InseeBin.prototype.getSize = function () {
  return this._grid.get('size');
};
/** Get the grid geometry at the coord 
 * @param {ol.Coordinate} coord
 * @returns {ol.geom.Polygon} 
 * @api
 */
ol.source.InseeBin.prototype.getGridGeomAt = function (coord) {
  return this._grid.getGridAtCoordinate(coord, this.getProjection());
};
/** Get grid extent 
 * @param {ol.ProjectionLike} proj
 * @return {ol.Extent}
 */
ol.source.InseeBin.prototype.getGridExtent = function (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>
*/
/**
* @constructor ol.source.Mapillary
* @extends {ol.source.Vector}
* @param {olx.source.Mapillary=} options
*/
ol.source.Mapillary = function(opt_options)
{	var options = opt_options || {};
	options.loader = this._loaderFn;
	/** Max resolution to load features  */
	this._maxResolution = options.maxResolution || 100;
	/** Query limit */
	this._limit = options.limit || 100;
	/** 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
	ol.source.Vector.call (this, options);
	// Client ID
	// this.set("clientId", options.clientId);
};
ol.ext.inherits(ol.source.Mapillary, ol.source.Vector);
/** 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
*/
ol.source.Mapillary.prototype.readFeature = function (/*feature, attributes*/)
{	// Allways read feature (no filter)
	return true;
};
/** Loader function used to load features.
* @private
*/
ol.source.Mapillary.prototype._loaderFn = function(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 = function (options) {
  options.operation = this._operation;
  options.operationType = 'image';
  ol.source.Raster.call(this, 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));
};
ol.ext.inherits(ol.source.OilPainting, ol.source.Raster);
/** Set value and force change
 */
ol.source.OilPainting.prototype.set = function(key, val) {
  if (val) {
    switch (key) {
      case 'intensity': 
      case 'radius': {
        val = Number(val);
        if (val<1) val = 1;
        this.changed();
        break;
      }
    }
  }
  return ol.source.Raster.prototype.set.call(this, key, val);
};
/**
 * @private
 */
ol.source.OilPainting.prototype._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 = function(options) {
	options = options || {};
	options.loader = this._loaderFn;
	/** Ovepass API Url */
	this._url = options.url || 'https://overpass-api.de/api/interpreter';
	/** Max resolution to load features  */
	this._maxResolution = options.maxResolution || 100;
	/** 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;
  ol.source.Vector.call (this, options);
  this._types = {
    node: options.node!==false,
    way: options.way!==false,
    rel: options.rel===true
  };
  this._filter = options.filter;
};
ol.ext.inherits(ol.source.Overpass, ol.source.Vector);
/** Loader function used to load features.
* @private
*/
ol.source.Overpass.prototype._loaderFn = function(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
 */
ol.source.Overpass.prototype.hasFeature = function(feature) {
	var p = feature.getGeometry().getFirstCoordinate();
	var id = feature.getId();
	var existing = this.getFeaturesInExtent([p[0]-0.1, p[1]-0.1, p[0]+0.1, p[1]+0.1]);
	for (var i=0, f; f=existing[i]; i++) {
		if (id===f.getId()) {
      return true;
    }
	}
	return false;
};

/*	Copyright (c) 2019 Jean-Marc VIGLINO,
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** A vector source to load WFS at a tile zoom level
 * @constructor
 * @fires tileloadstart
 * @fires tileloadend
 * @fires tileloaderror
 * @fires overload
 * @extends {ol.source.Vector}
 * @param {Object} options
 *  @param {string} [options.version=1.1.0] WFS version to use. Can be either 1.0.0, 1.1.0 or 2.0.0.
 *  @param {string} options.typeName WFS type name parameter
 *  @param {number} options.tileZoom zoom to load the tiles
 *  @param {number} options.maxFeatures maximum features returned in the WFS
 *  @param {number} options.featureLimit maximum features in the source before refresh, default Infinity
 *  @param {boolean} [options.pagination] experimental enable pagination, default no pagination
 */
ol.source.TileWFS = function (options) {
  options = options || {};
  if (!options.featureLimit) options.featureLimit = Infinity;
  // Tile loading strategy
  var zoom = options.tileZoom || 14;
  var sourceOpt = {
    strategy: ol.loadingstrategy.tile(ol.tilegrid.createXYZ({ minZoom: zoom, maxZoom: zoom, tileSize:512  }))
  };
  // Loading params
  var format = new ol.format.GeoJSON();
  var url = options.url
    + '?service=WFS'
    + '&request=GetFeature'
    + '&version=' + (options.version || '1.1.0')
    + '&typename=' + (options.typeName || '')
    + '&outputFormat=application/json';
  if (options.maxFeatures) {
    url += '&maxFeatures=' + options.maxFeatures + '&count=' + options.maxFeatures;
  }
  var loader = { loading: 0, loaded: 0 };
  // Loading fn
  sourceOpt.loader = function(extent, resolution, projection) {
    if (loader.loading === loader.loaded) {
      loader.loading = loader.loaded = 0;
      if (this.getFeatures().length > options.maxFeatures) {
        this.clear();
        this.refresh();
      }
    }
    loader.loading++;
    this.dispatchEvent({ 
      type: 'tileloadstart',
      loading: loader.loading, 
      loaded: loader.loaded
    });
    this._loadTile(url, extent, projection, format, loader);
  }
  ol.source.Vector.call(this, sourceOpt);
  this.set('pagination', options.pagination);
};
ol.ext.inherits(ol.source.TileWFS, ol.source.Vector);
/**
 * 
 */
ol.source.TileWFS.prototype._loadTile = function(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 = function (options) {
  options = options || {};
  this._source = options.source;
  this.height_ = this.getHfn (options.height);
  var canvas = document.createElement('canvas');
  ol.layer.Image.call (this, { 
    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.setStyle(options.style);
  this.on (['postcompose', 'postrender'], this.onPostcompose_.bind(this));
}
ol.ext.inherits(ol.layer.Vector3D, ol.layer.Image);
/**
 * 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
 */
ol.layer.Vector3D.prototype.setHeight = function(height) {
  this.height_ = this.getHfn (height);
  this.changed();
};
/**
 * Set style associated with the layer
 * @param {ol.style.Style} s
 */
ol.layer.Vector3D.prototype.setStyle = function(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}
 */
ol.layer.Vector3D.prototype.getStyle = function() {
  return this._style;
};
/** Calculate 3D at potcompose
 * @private
 */
ol.layer.Vector3D.prototype.onPostcompose_ = function(e) {
  var res = e.frameState.viewState.resolution;
  if (res > this.get('maxResolution')) return;
  this.res_ = res*400;
  if (this.animate_) {
    var elapsed = e.frameState.time - this.animate_;
    if (elapsed < this.animateDuration_) {
      this.elapsedRatio_ = this.easing_(elapsed / this.animateDuration_);
      // tell OL3 to continue postcompose animation
      e.frameState.animate = true;
    } else {
      this.animate_ = false;
      this.height_ = this.toHeight_
    }
  }
  var ratio = e.frameState.pixelRatio;
  var ctx = e.context;
  var m = this.matrix_ = e.frameState.coordinateToPixelTransform;
  // Old version (matrix)
  if (!m) {
    m = e.frameState.coordinateToPixelMatrix,
    m[2] = m[4];
    m[3] = m[5];
    m[4] = m[12];
    m[5] = m[13];
  }
  this.center_ = [ 
    ctx.canvas.width*this.get('center')[0]/ratio, 
    ctx.canvas.height*this.get('center')[1]/ratio 
  ];
  var f = this._source.getFeaturesInExtent(e.frameState.extent);
  ctx.save();
    ctx.scale(ratio,ratio);
    var s = this.getStyle();
    ctx.lineWidth = s.getStroke().getWidth();
    ctx.lineCap = s.getStroke().getLineCap();
    ctx.strokeStyle = ol.color.asString(s.getStroke().getColor());
    ctx.fillStyle = ol.color.asString(s.getFill().getColor());
    var builds = [];
    for (var i=0; i<f.length; i++) {
      builds.push (this.getFeature3D_ (f[i], this._getFeatureHeight(f[i])));
    }
    this.drawFeature3D_ (ctx, builds);
  ctx.restore();
};
/** Create a function that return height of a feature
 * @param {function|string|number} h a height function or a popertie name or a fixed value
 * @return {function} function(f) return height of the feature f
 * @private
 */
ol.layer.Vector3D.prototype.getHfn= function(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
 */
ol.layer.Vector3D.prototype.animate = function(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}
*/
ol.layer.Vector3D.prototype.animating = function() {
  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
 */
ol.layer.Vector3D.prototype._getFeatureHeight = function (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
 */
ol.layer.Vector3D.prototype.hvector_ = function (pt, h) {
  var p0 = [
    pt[0]*this.matrix_[0] + pt[1]*this.matrix_[1] + this.matrix_[4],
    pt[0]*this.matrix_[2] + pt[1]*this.matrix_[3] + this.matrix_[5]
  ];
  return {
    p0: p0, 
    p1: [
      p0[0] + h/this.res_ * (p0[0]-this.center_[0]),
      p0[1] + h/this.res_ * (p0[1]-this.center_[1])
    ]
  };
};
/** Get a vector 3D for a feature
 * @private
 */
ol.layer.Vector3D.prototype.getFeature3D_ = function (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
 */
ol.layer.Vector3D.prototype.drawFeature3D_ = function(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 = function(opt_options) {
  var options = opt_options || {};
  options.loader = this._loaderFn;
  /** Max resolution to load features  */
  this._maxResolution = options.maxResolution || 100;
  /** Result language */
  this._lang = options.lang || "fr";
  /** Query limit */
  this._limit = options.limit || 100;
  /** 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;
  ol.source.Vector.call (this, options);
};
ol.ext.inherits(ol.source.WikiCommons, ol.source.Vector);
/** 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
*/
ol.source.WikiCommons.prototype.readFeature = function (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
*/
ol.source.WikiCommons.prototype._loaderFn = function(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 = function(opt_options) {
  var options = opt_options || {};
  ol.layer.Vector.call (this, options);
  this.oldcluster = new ol.source.Vector();
  this.clusters = [];
  this.animation={start:false};
  this.set('animationDuration', typeof(options.animationDuration)=='number' ? options.animationDuration : 700);
  this.set('animationMethod', options.animationMethod || ol.easing.easeOut);
  // Save cluster before change
  this.getSource().on('change', this.saveCluster.bind(this));
  // Animate the cluster
  this.on(['precompose','prerender'], this.animate.bind(this));
  this.on(['postcompose','postrender'], this.postanimate.bind(this));
};
ol.ext.inherits(ol.layer.AnimatedCluster, ol.layer.Vector);
/** save cluster features before change
 * @private
 */
ol.layer.AnimatedCluster.prototype.saveCluster = function() {
  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
*/
ol.layer.AnimatedCluster.prototype.getClusterForFeature = function(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 
 */
ol.layer.AnimatedCluster.prototype.stopAnimation = function() {
  this.animation.start = false;
  this.animation.cA = [];
  this.animation.cB = [];
};
/** 
 * animate the cluster
 * @private
 */
ol.layer.AnimatedCluster.prototype.animate = function(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
 */
ol.layer.AnimatedCluster.prototype.postanimate = function(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 = function(options) {
  ol.layer.Image.call(this, options);
}
ol.ext.inherits (ol.layer.GeoImage, ol.layer.Image);
/**
 * Return the {@link module:ol/extent~Extent extent} of the source associated with the layer.
 * @return {ol.Extent} The layer extent.
 * @observable
 * @api
 */
ol.layer.GeoImage.prototype.getExtent = function() {
  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 = function(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];
  }
  this._originators = capabilities.originators;
  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;
  }
  ol.layer.Tile.call (this, options);
  // BUG GPP: Attributions constraints are not set properly :(
/** /
  // Set attribution according to the originators
  var counter = 0;
  // Get default attribution
  var getAttrib = function(title, o) {
    if (this.get('attributionMode')==='logo') {
      if (!title) return ol.source.Geoportail.prototype.attribution;
      else return '<a href="'+o.href+'"><img src="'+o.logo+'" title="&copy; '+o.attribution+'" /></a>';
    } else {
      if (!title) return ol.source.Geoportail.prototype.attribution;
      else return '&copy; <a href="'+o.href+'" title="&copy; '+(o.attribution||title)+'" >'+title+'</a>'
    }
  }.bind(this);
  var currentZ, currentCenter = [];
  var setAttribution = function(e) {
    var a, o, i;
    counter--;
    if (!counter) {
      var z = e.frameState.viewState.zoom;
      console.log(e)
      if (z===currentZ 
        && e.frameState.viewState.center[0]===currentCenter[0]
        && e.frameState.viewState.center[1]===currentCenter[1]){
          return;
      }
      currentZ = z;
      currentCenter = e.frameState.viewState.center;
      var ex = e.frameState.extent;
      ex = ol.proj.transformExtent (ex, e.frameState.viewState.projection, 'EPSG:4326');
      if (this._originators) {
        var attrib = this.getSource().getAttributions();
        // ol v5
        if (typeof(attrib)==='function') attrib = attrib();
        attrib.splice(0, attrib.length);
        var maxZoom = 0;
        for (a in this._originators) {
          o = this._originators[a];
          for (i=0; i<o.constraint.length; i++) {
            if (o.constraint[i].maxZoom > maxZoom
              && ol.extent.intersects(ex, o.constraint[i].bbox)) {
                maxZoom = o.constraint[i].maxZoom;
            }
          }	
        }
        if (maxZoom < z) z = maxZoom;
        if (this.getSource().getTileGrid() && z < this.getSource().getTileGrid().getMinZoom()) {
          z = this.getSource().getTileGrid().getMinZoom();
        }
        for (a in this._originators) {
          o = this._originators[a];
          if (!o.constraint.length) {
            attrib.push (getAttrib(a, o));
          } else {
            for (i=0; i<o.constraint.length; i++) {
              if ( z <= o.constraint[i].maxZoom 
                && z >= o.constraint[i].minZoom 
                && ol.extent.intersects(ex, o.constraint[i].bbox)) {
                  attrib.push (getAttrib(a, o));
                  break;
              }
            }
          }
        }
        if (!attrib.length) attrib.push ( getAttrib() );
        this.getSource().setAttributions(attrib);
      }
    }
  }.bind(this);
  this.on('precompose', function(e) {
    counter++;
    setTimeout(function () { setAttribution(e) }, 500);
  });
/**/
};
ol.ext.inherits (ol.layer.Geoportail, ol.layer.Tile);
/** Default capabilities for main layers
 */
ol.layer.Geoportail.capabilities = {
  // choisirgeoportail
  "GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2": { "key":"cartes", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2","title":"Plan IGN v2","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":19,"bbox":[-175,-85,175,85],"desc":"Cartographie multi-échelles sur le territoire national, issue des bases de données vecteur de l’IGN, mis à jour régulièrement et réalisée selon un processus entièrement automatisé. Version actuellement en beta test","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":19,"constraint":[{"minZoom":0,"maxZoom":19,"bbox":[-175,-85,175,85]}]}}},
  "CADASTRALPARCELS.PARCELLAIRE_EXPRESS": { "key":"parcellaire", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"CADASTRALPARCELS.PARCELLAIRE_EXPRESS","title":"PCI vecteur","format":"image/png","style":"PCI vecteur","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":19,"bbox":[-63.37252,-21.475586,55.925865,51.31212],"desc":"Plan cadastral informatisé vecteur de la DGFIP.","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":19,"constraint":[{"minZoom":0,"maxZoom":19,"bbox":[-63.37252,-21.475586,55.925865,51.31212]}]}}},
  "ORTHOIMAGERY.ORTHOPHOTOS": { "key":"ortho", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"ORTHOIMAGERY.ORTHOPHOTOS","title":"Photographies aériennes","format":"image/jpeg","style":"normal","queryable":true,"tilematrix":"PM","minZoom":0,"bbox":[-178.18713,-22.767689,167.94624,51.11242],"desc":"Photographies aériennes","originators":{"CRCORSE":{"href":"http://www.corse.fr//","attribution":"CRCORSE","logo":"https://wxs.ign.fr/static/logos/CRCORSE/CRCORSE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[8.428783,41.338627,9.688606,43.08541]}]},"SIGLR":{"href":"http://www.siglr.org//","attribution":"SIGLR","logo":"https://wxs.ign.fr/static/logos/SIGLR/SIGLR.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.6784439,42.316307,4.8729386,44.978218]}]},"BOURGOGNE-FRANCHE-COMTE":{"href":"https://www.bourgognefranchecomte.fr/","attribution":"Auvergne","logo":"https://wxs.ign.fr/static/logos/BOURGOGNE-FRANCHE-COMTE/BOURGOGNE-FRANCHE-COMTE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.837849,46.131435,7.1713247,48.408287]}]},"FEDER_AUVERGNE":{"href":"http://www.europe-en-auvergne.eu/","attribution":"Auvergne","logo":"https://wxs.ign.fr/static/logos/FEDER_AUVERGNE/FEDER_AUVERGNE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.0398402,44.60505,3.38408,45.49146]}]},"FEDER_PAYSDELALOIRE":{"href":"https://www.europe.paysdelaloire.fr/","attribution":"Pays-de-la-Loire","logo":"https://wxs.ign.fr/static/logos/FEDER_PAYSDELALOIRE/FEDER_PAYSDELALOIRE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-2.457367,46.19304,0.951426,48.57609]}]},"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":13,"maxZoom":20,"constraint":[{"minZoom":19,"maxZoom":19,"bbox":[-63.160706,-21.401262,55.84643,51.11242]},{"bbox":[0.035491213,43.221077,6.0235267,49.696926]},{"minZoom":20,"maxZoom":20,"bbox":[0.035491213,43.221077,6.0235267,49.696926]},{"minZoom":13,"maxZoom":18,"bbox":[-178.18713,-21.401329,55.85611,51.11242]}]},"E-MEGALIS":{"href":"http://www.e-megalisbretagne.org//","attribution":"Syndicat mixte de coopération territoriale (e-Megalis)","logo":"https://wxs.ign.fr/static/logos/E-MEGALIS/E-MEGALIS.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-3.7059498,47.971947,-1.8486879,48.99035]}]},"FEDER2":{"href":"http://www.europe-en-france.gouv.fr/","attribution":"Fonds européen de développement économique et régional","logo":"https://wxs.ign.fr/static/logos/FEDER2/FEDER2.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.3577043,48.824635,4.269964,50.37648]}]},"PREFECTURE_GUADELOUPE":{"href":"www.guadeloupe.pref.gouv.fr/","attribution":"guadeloupe","logo":"https://wxs.ign.fr/static/logos/PREFECTURE_GUADELOUPE/PREFECTURE_GUADELOUPE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-61.82342,14.371942,-60.787838,16.521578]}]},"OCCITANIE":{"href":"https://www.laregion.fr/","attribution":"La Région Occitanie; Pyrénées - Méditerranée","logo":"https://wxs.ign.fr/static/logos/OCCITANIE/OCCITANIE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.2086434,48.805965,2.4859917,48.915382]}]},"RGD_SAVOIE":{"href":"http://www.rgd.fr","attribution":"Régie de Gestion de Données des Pays de Savoie (RGD 73-74)","logo":"https://wxs.ign.fr/static/logos/RGD_SAVOIE/RGD_SAVOIE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":19,"maxZoom":19,"bbox":[5.7759595,45.65335,7.0887337,46.438328]},{"minZoom":13,"maxZoom":18,"bbox":[5.5923314,45.017353,7.2323394,46.438328]}]},"CG45":{"href":"http://www.loiret.com","attribution":"Le conseil général du Loiret","logo":"https://wxs.ign.fr/static/logos/CG45/CG45.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.4883244,47.471867,3.1349874,48.354233]}]},"CRAIG":{"href":"http://www.craig.fr","attribution":"Centre Régional Auvergnat de l'Information Géographique (CRAIG)","logo":"https://wxs.ign.fr/static/logos/CRAIG/CRAIG.gif","minZoom":13,"maxZoom":20,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.0398402,44.60505,6.4295278,46.8038]},{"minZoom":20,"maxZoom":20,"bbox":[2.2243388,44.76621,2.7314367,45.11295]}]},"e-Megalis":{"href":"http://www.e-megalisbretagne.org//","attribution":"Syndicat mixte de coopération territoriale (e-Megalis)","logo":"https://wxs.ign.fr/static/logos/e-Megalis/e-Megalis.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-5.1937118,47.23789,-0.98568505,48.980812]}]},"PPIGE":{"href":"http://www.ppige-npdc.fr/","attribution":"PPIGE","logo":"https://wxs.ign.fr/static/logos/PPIGE/PPIGE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.5212119,49.957302,4.2673664,51.090965]}]},"CG06":{"href":"http://www.cg06.fr","attribution":"Département Alpes Maritimes (06) en partenariat avec : Groupement Orthophoto 06 (NCA, Ville de Cannes, CARF, CASA,CG06, CA de Grasse) ","logo":"https://wxs.ign.fr/static/logos/CG06/CG06.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[6.6093955,43.44647,7.7436337,44.377018]}]},"MEGALIS-BRETAGNE":{"href":"https://www.megalisbretagne.org/","attribution":"Syndicat mixte Mégalis Bretagne","logo":"https://wxs.ign.fr/static/logos/MEGALIS-BRETAGNE/MEGALIS-BRETAGNE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-5.2086344,47.591938,-3.3396015,48.808697]}]},"FEDER":{"href":"http://www.europe-en-france.gouv.fr/","attribution":"Fonds européen de développement économique et régional","logo":"https://wxs.ign.fr/static/logos/FEDER/FEDER.gif","minZoom":0,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[-1.9662633,42.316307,8.25674,50.18387]},{"minZoom":0,"maxZoom":12,"bbox":[-2.400665,41.333557,9.560094,50.366302]}]},"LANGUEDOC-ROUSSILLON":{"href":"https://www.laregion.fr/","attribution":"Région Occitanie","logo":"https://wxs.ign.fr/static/logos/LANGUEDOC-ROUSSILLON/LANGUEDOC-ROUSSILLON.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[1.6784439,42.63972,4.208843,43.979004]}]},"GRAND_EST":{"href":"https://www.grandest.fr/","attribution":"Hauts-de-France","logo":"https://wxs.ign.fr/static/logos/GRAND_EST/GRAND_EST.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[5.362788,47.390827,7.6924667,49.58011]}]},"CNES_AUVERGNE":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_AUVERGNE/CNES_AUVERGNE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.2656832,45.279934,4.0227704,46.8038]}]},"HAUTS_DE_FRANCE":{"href":"https://www.hautsdefrance.fr/","attribution":"Hauts-de-France","logo":"https://wxs.ign.fr/static/logos/HAUTS_DE_FRANCE/HAUTS_DE_FRANCE.gif","minZoom":13,"maxZoom":19,"constraint":[{"minZoom":13,"maxZoom":19,"bbox":[2.0740242,48.81521,4.3390365,51.11242]}]},"MPM":{"href":"http://www.marseille-provence.com/","attribution":"Marseille Provence Métropole","logo":"https://wxs.ign.fr/static/logos/MPM/MPM.gif","minZoom":20,"maxZoom":20,"constraint":[{"minZoom":20,"maxZoom":20,"bbox":[5.076959,43.153347,5.7168245,43.454994]}]},"DITTT":{"href":"http://www.dittt.gouv.nc/portal/page/portal/dittt/","attribution":"Direction des Infrastructures, de la Topographie et des Transports Terrestres","logo":"https://wxs.ign.fr/static/logos/DITTT/DITTT.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[163.47784,-22.767689,167.94624,-19.434975]}]},"CNES_978":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_978/CNES_978.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-63.160706,18.04345,-62.962185,18.133898]}]},"CNES_ALSACE":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_ALSACE/CNES_ALSACE.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[6.8086324,47.39981,7.668318,48.32695]}]},"CNES_974":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_974/CNES_974.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[55.205757,-21.401262,55.84643,-20.862825]}]},"CNES_975":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_975/CNES_975.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-56.410988,46.734093,-56.10308,47.149963]}]},"CNES_976":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_976/CNES_976.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[44.916977,-13.089187,45.30442,-12.564543]}]},"CNES_977":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_977/CNES_977.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-62.952805,17.862621,-62.78276,17.98024]}]},"CNES":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES/CNES.gif","minZoom":13,"maxZoom":16,"constraint":[{"minZoom":13,"maxZoom":16,"bbox":[-55.01953,1.845384,-50.88867,6.053161]}]},"ASTRIUM":{"href":"http://www.geo-airbusds.com/","attribution":"Airbus Defence and Space","logo":"https://wxs.ign.fr/static/logos/ASTRIUM/ASTRIUM.gif","minZoom":13,"maxZoom":16,"constraint":[{"minZoom":13,"maxZoom":16,"bbox":[-55.01953,1.845384,-50.88867,6.053161]}]},"CNES_971":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_971/CNES_971.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-61.82342,15.819616,-60.99497,16.521578]}]},"CNES_972":{"href":"http://www.cnes.fr/","attribution":"Centre national d'études spatiales (CNES)","logo":"https://wxs.ign.fr/static/logos/CNES_972/CNES_972.gif","minZoom":13,"maxZoom":18,"constraint":[{"minZoom":13,"maxZoom":18,"bbox":[-61.247208,14.371855,-60.778458,14.899901]}]}}},
  // Deprecated
  "GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD": {"server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD","title":"Carte IGN","format":"image/jpeg","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":18,"bbox":[-179.62723,-84.5047,179.74588,85.47958],"desc":"Cartographie topographique multi-échelles du territoire français issue des bases de données vecteur de l’IGN - emprise nationale, visible du 1/200 au 1/130000000","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":18,"constraint":[{"minZoom":5,"maxZoom":5,"bbox":[-179.57285,-83.84196,178.4975,85.36646]},{"minZoom":0,"maxZoom":2,"bbox":[-175.99709,-84.42859,175.99709,84.2865]},{"minZoom":3,"maxZoom":3,"bbox":[-176.23093,-84.5047,179.08267,84.89126]},{"minZoom":4,"maxZoom":4,"bbox":[-179.62723,-84.0159,-179.21112,85.47958]},{"minZoom":6,"maxZoom":8,"bbox":[-179.49689,-84.02368,179.74588,85.30035]},{"minZoom":15,"maxZoom":18,"bbox":[-5.6663494,41.209736,10.819784,51.175068]},{"minZoom":14,"maxZoom":14,"bbox":[-5.713191,40.852314,11.429714,51.44377]},{"minZoom":13,"maxZoom":13,"bbox":[-63.37252,13.428586,11.429714,51.44377]},{"minZoom":11,"maxZoom":12,"bbox":[-63.37252,13.428586,11.496459,51.444122]},{"minZoom":9,"maxZoom":9,"bbox":[-64.81273,13.428586,11.496459,51.444016]},{"minZoom":10,"maxZoom":10,"bbox":[-63.37252,13.428586,11.496459,51.444016]}]}}},
  // Need API key
  "GEOGRAPHICALGRIDSYSTEMS.MAPS": {"server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS","title":"Cartes IGN","format":"image/jpeg","style":"normal","queryable":true,"tilematrix":"PM","minZoom":0,"maxZoom":18,"bbox":[-180,-75,180,80],"desc":"Cartes IGN","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":18,"constraint":[{"minZoom":7,"maxZoom":7,"bbox":[-178.20573,-68.138855,144.84375,51.909786]},{"minZoom":8,"maxZoom":8,"bbox":[-178.20573,-68.138855,168.24327,51.909786]},{"minZoom":13,"maxZoom":13,"bbox":[-178.20573,-67.101425,168.24327,51.44377]},{"minZoom":14,"maxZoom":14,"bbox":[-178.20573,-67.101425,168.23909,51.44377]},{"minZoom":11,"maxZoom":12,"bbox":[-178.20573,-67.101425,168.24327,51.444122]},{"minZoom":9,"maxZoom":10,"bbox":[-178.20573,-68.138855,168.24327,51.444016]},{"minZoom":15,"maxZoom":15,"bbox":[-178.20573,-46.502903,168.23909,51.175068]},{"minZoom":16,"maxZoom":16,"bbox":[-178.20573,-46.502903,168.29811,51.175068]},{"minZoom":0,"maxZoom":6,"bbox":[-180,-60,180,80]},{"minZoom":18,"maxZoom":18,"bbox":[-5.6663494,41.209736,10.819784,51.175068]},{"minZoom":17,"maxZoom":17,"bbox":[-179.5,-75,179.5,75]}]},"DITTT":{"href":"http://www.dittt.gouv.nc/portal/page/portal/dittt/","attribution":"Direction des Infrastructures, de la Topographie et des Transports Terrestres","logo":"https://wxs.ign.fr/static/logos/DITTT/DITTT.gif","minZoom":8,"maxZoom":16,"constraint":[{"minZoom":8,"maxZoom":10,"bbox":[163.47784,-22.972307,168.24327,-19.402702]},{"minZoom":11,"maxZoom":13,"bbox":[163.47784,-22.972307,168.24327,-19.494438]},{"minZoom":14,"maxZoom":15,"bbox":[163.47784,-22.764496,168.23909,-19.493542]},{"minZoom":16,"maxZoom":16,"bbox":[163.47784,-22.809465,168.29811,-19.403923]}]}}},
  // Other layers
  "ADMINEXPRESS-COG-CARTO.LATEST": {"key": "administratif", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"ADMINEXPRESS-COG-CARTO.LATEST","title":"ADMINEXPRESS COG CARTO","format":"image/png","style":"normal","queryable":true,"tilematrix":"PM","minZoom":6,"maxZoom":16,"bbox":[-63.37252,-21.475586,55.925865,51.31212],"desc":"Limites administratives Express COG code officiel géographique 2021","originators":{"IGN":{"href":"https://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":6,"maxZoom":16,"constraint":[{"minZoom":6,"maxZoom":16,"bbox":[-63.37252,-21.475586,55.925865,51.31212]}]}}},
  "GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN": {"key":"altimetrie","server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN","title":"Carte des pentes","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":17,"bbox":[-63.161392,-21.544624,56.001812,51.099052],"desc":"Carte des zones ayant une valeur de pente supérieure à 30°-35°-40°-45° d'après la BD ALTI au pas de 5m","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":17,"constraint":[{"minZoom":0,"maxZoom":17,"bbox":[-5.1504726,41.32521,9.570543,51.099052]}]}}},
  "ELEVATION.SLOPES": {"key":"altimetrie","server":"https://wxs.ign.fr/geoportail/wmts","layer":"ELEVATION.SLOPES","title":"Altitude","format":"image/jpeg","style":"normal","queryable":true,"tilematrix":"PM","minZoom":6,"maxZoom":14,"bbox":[-178.20589,-22.595179,167.43176,50.93085],"desc":"La couche altitude se compose d'un MNT (Modèle Numérique de Terrain) affiché en teintes hypsométriques et issu de la BD ALTI®.","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":6,"maxZoom":14,"constraint":[{"minZoom":6,"maxZoom":14,"bbox":[55.205746,-21.392344,55.846554,-20.86271]}]}}},
  "GEOGRAPHICALGRIDSYSTEMS.MAPS.BDUNI.J1": { "key":"cartes", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"GEOGRAPHICALGRIDSYSTEMS.MAPS.BDUNI.J1","title":"Plan IGN j+1","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":0,"maxZoom":18,"bbox":[-179.5,-75,179.5,75],"desc":"Plan IGN j+1","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":0,"maxZoom":18,"constraint":[{"minZoom":0,"maxZoom":18,"bbox":[-179,-80,179,80]}]}}},
  "TRANSPORTNETWORKS.ROADS": { "key": "topographie", "server":"https://wxs.ign.fr/geoportail/wmts","layer":"TRANSPORTNETWORKS.ROADS","title":"Routes","format":"image/png","style":"normal","queryable":false,"tilematrix":"PM","minZoom":6,"maxZoom":18,"bbox":[-63.969162,-21.49687,55.964417,71.584076],"desc":"Affichage du réseau routier français et européen.","originators":{"IGN":{"href":"http://www.ign.fr","attribution":"Institut national de l'information géographique et forestière","logo":"https://wxs.ign.fr/static/logos/IGN/IGN.gif","minZoom":6,"maxZoom":18,"constraint":[{"minZoom":15,"maxZoom":18,"bbox":[-63.37252,-21.475586,55.925865,51.31212]},{"minZoom":6,"maxZoom":14,"bbox":[-63.969162,-21.49687,55.964417,71.584076]}]}}},
};
/** Register new layer capability
 * @param {string} layer layer name
 * @param {*} capability
 */
ol.layer.Geoportail.register = function(layer, capability) {
  ol.layer.Geoportail.capabilities[layer] = capability;
};
/** Check if a layer registered with a key?
 * @param {string} layer layer name
 * @returns {boolean} 
 */
ol.layer.Geoportail.isRegistered = function(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
 */
ol.layer.Geoportail.loadCapabilities = function(gppKey, all) {
  var onSuccess = function() {}
  var onError = function() {}
  var onFinally = function() {};
  this.getCapabilities(gppKey,all).then(function(c) {
    ol.layer.Geoportail.capabilities = c;
    onSuccess(c);
  }).catch(function(e) { 
    onError(e);
  }).finally(function(c) {
    onFinally(c);
  });
  var response = {
    then: function (callback) {
      if (typeof(callback)==='function') onSuccess = callback;
      return response;
    },
    catch: function (callback) {
      if (typeof(callback)==='function') onError = callback;
      return response;
    },
    finally: function (callback) {
      if (typeof(callback)==='function') onFinally = callback;
      return response;
    }
  }
  return response;
};
/** Get Key capabilities
 * @param {string} gppKey the API key to get capabilities for
 * @return {*} Promise-like response
 */
ol.layer.Geoportail.getCapabilities = function(gppKey) {
  var capabilities = {};
  var onSuccess = function() {}
  var onError = function() {}
  var onFinally = function() {}
  var geopresolutions = [156543.03390625,78271.516953125,39135.7584765625,19567.87923828125,9783.939619140625,4891.9698095703125,2445.9849047851562,1222.9924523925781,611.4962261962891,305.74811309814453,152.87405654907226,76.43702827453613,38.218514137268066,19.109257068634033,9.554628534317017,4.777314267158508,2.388657133579254,1.194328566789627,0.5971642833948135,0.29858214169740677,0.14929107084870338];
  // Transform resolution to zoom
  function getZoom(res) {
    res = Number(res) * 0.000281;
    for (var r=0; r<geopresolutions.length; r++) 
      if (res>geopresolutions[r]) return r;
  }
  // Merge constraints 
  function mergeConstraints(ori) {
    for (var i=ori.constraint.length-1; i>0; i--) {
      for (var j=0; j<i; j++) {
        var bok = true;
        for (var k=0; k<4; k++) {
          if (ori.constraint[i].bbox[k] != ori.constraint[j].bbox[k]) {
            bok = false;
            break;
          }
        }
        if (!bok) continue;
        if (ori.constraint[i].maxZoom == ori.constraint[j].minZoom 
        || ori.constraint[j].maxZoom == ori.constraint[i].minZoom 
        || ori.constraint[i].maxZoom+1 == ori.constraint[j].minZoom 
        || ori.constraint[j].maxZoom+1 == ori.constraint[i].minZoom
        || ori.constraint[i].minZoom-1 == ori.constraint[j].maxZoom
        || ori.constraint[j].minZoom-1 == ori.constraint[i].maxZoom) {
          ori.constraint[j].maxZoom = Math.max(ori.constraint[i].maxZoom, ori.constraint[j].maxZoom);
          ori.constraint[j].minZoom = Math.min(ori.constraint[i].minZoom, ori.constraint[j].minZoom);
          ori.constraint.splice(i,1);
          break;
        }
      }
    }
  }
  // Get capabilities
  ol.ext.Ajax.get({
    url: 'https://wxs.ign.fr/'+gppKey+'/autoconf/',
    dataType: 'TEXT',
    error: function (e) {
      onError(e);
      onFinally({});
    },
    success: function(resp) {
      var parser = new DOMParser();
      var config = parser.parseFromString(resp,"text/xml");
      var layers = config.getElementsByTagName('Layer');
      for (var i=0, l; l=layers[i]; i++) {
        // WMTS ?
        if (!/WMTS/.test(l.getElementsByTagName('Server')[0].attributes['service'].value)) continue;
//        if (!all && !/geoportail\/wmts/.test(l.find("OnlineResource").attr("href"))) continue;
        var service = {
          key: gppKey,
          server: l.getElementsByTagName('gpp:Key')[0].innerHTML.replace(gppKey+"/",""), 
          layer: l.getElementsByTagName('Name')[0].innerHTML,
          title: l.getElementsByTagName('Title')[0].innerHTML,
          format: l.getElementsByTagName('Format')[0].innerHTML,
          style: l.getElementsByTagName('Style')[0].getElementsByTagName('Name')[0].innerHTML,
          queryable: (l.attributes.queryable.value==='1'),
          tilematrix: 'PM',
          minZoom: getZoom(l.getElementsByTagName('sld:MaxScaleDenominator')[0].innerHTML),
          maxZoom: getZoom(l.getElementsByTagName('sld:MinScaleDenominator')[0].innerHTML),
          bbox: JSON.parse('['+l.getElementsByTagName('gpp:BoundingBox')[0].innerHTML+']'),
          desc: l.getElementsByTagName('Abstract')[0].innerHTML.replace(/^<!\[CDATA\[(.*)\]\]>$/, '$1')
        };
        service.originators = {};
        var origin = l.getElementsByTagName('gpp:Originator');
        for (var k=0, o; o=origin[k]; k++) {
          var ori = service.originators[o.attributes['name'].value] = {
            href: o.getElementsByTagName('gpp:URL')[0].innerHTML,
            attribution: o.getElementsByTagName('gpp:Attribution')[0].innerHTML,
            logo: o.getElementsByTagName('gpp:Logo')[0].innerHTML,
            minZoom: 20,
            maxZoom: 0,
            constraint: []
          };
          // Scale contraints
          var constraint = o.getElementsByTagName('gpp:Constraint');
          for (var j=0, c; c=constraint[j]; j++) {
            var zmax = getZoom(c.getElementsByTagName('sld:MinScaleDenominator')[0].innerHTML);
            var zmin = getZoom(c.getElementsByTagName('sld:MaxScaleDenominator')[0].innerHTML);
            if (zmin > ori.maxZoom) ori.maxZoom = zmin;
            if (zmin < ori.minZoom) ori.minZoom = zmin;
            if (zmax>ori.maxZoom) ori.maxZoom = zmax;
            if (zmax<ori.minZoom) ori.minZoom = zmax;
            ori.constraint.push({
              minZoom: zmin,
              maxZoom: zmax,
              bbox: JSON.parse('['+c.getElementsByTagName('gpp:BoundingBox')[0].innerHTML+']')
            });
          }
          // Merge constraints
          mergeConstraints(ori)
        }
        capabilities[service.layer] = service;
      }
      onSuccess(capabilities);
      onFinally(capabilities);
    }
  });
  // Promise like response
  var response = {
    then: function (callback) {
      if (typeof(callback)==='function') onSuccess = callback;
      return response;
    },
    catch: function (callback) {
      if (typeof(callback)==='function') onError = callback;
      return response;
    },
    finally: function (callback) {
      if (typeof(callback)==='function') onFinally = callback;
      return response;
    },
  }
  return response;
};

/*	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()) {
    return [ this.getSource().getPreview(lonlat, resolution) ];
  }
  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 = function (options) {
  options = options || {};
  options.maxResolution = options.maxResolution || 100
  options.defaultHeight = options.defaultHeight || 0;
  ol.Object.call (this, 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);
}
ol.ext.inherits(ol.render3D, ol.Object);
/**
 * Set style associated with the renderer
 * @param {ol.style.Style} s
 */
ol.render3D.prototype.setStyle = function(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}
 */
ol.render3D.prototype.getStyle = function() {
  return this._style;
};
/** Set active
 * @param {function|boolean} active
 */
ol.render3D.prototype.setActive = function(active) {
  if (typeof(active)==='function') {
    this._active = active;
  }
  else {
    this._active = function() { return active; };
  }
  if (this.layer_) this.layer_.changed();
};
/** Get active
 * @return {boolean} 
 */
ol.render3D.prototype.getActive = function() {
  return this._active();
};
/** Calculate 3D at potcompose
 * @private
 */
ol.render3D.prototype.onPostcompose_ = function(e) {
  if (!this.getActive()) return;
  var res = e.frameState.viewState.resolution;
  if (res > this.get('maxResolution')) return;
  this.res_ = res*400;
  if (this.animate_) {
    var elapsed = e.frameState.time - this.animate_;
    if (elapsed < this.animateDuration_) {
      this.elapsedRatio_ = this.easing_(elapsed / this.animateDuration_);
      // tell OL3 to continue postcompose animation
      e.frameState.animate = true;
    } else {
      this.animate_ = false;
      this.height_ = this.toHeight_
    }
  }
  var ratio = e.frameState.pixelRatio;
  var ctx = e.context;
  var m = this.matrix_ = e.frameState.coordinateToPixelTransform;
  // Old version (matrix)
  if (!m) {
    m = e.frameState.coordinateToPixelMatrix,
    m[2] = m[4];
    m[3] = m[5];
    m[4] = m[12];
    m[5] = m[13];
  }
  this.center_ = [ ctx.canvas.width/2/ratio, ctx.canvas.height/ratio ];
  var f = this.layer_.getSource().getFeaturesInExtent(e.frameState.extent);
  ctx.save();
    ctx.scale(ratio,ratio);
    var s = this.getStyle();
    ctx.lineWidth = s.getStroke().getWidth();
    ctx.strokeStyle = ol.color.asString(s.getStroke().getColor());
    ctx.fillStyle = ol.color.asString(s.getFill().getColor());
    var builds = [];
    for (var i=0; i<f.length; i++) {
      var h = this.getFeatureHeight(f[i])
      if (h) builds.push (this.getFeature3D_ (f[i], h));
    }
    if (this.get('ghost')) this.drawGhost3D_ (ctx, builds);
    else this.drawFeature3D_ (ctx, builds);
  ctx.restore();
};
/** Set layer to render 3D
 */
ol.render3D.prototype.setLayer = function(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
 */
ol.render3D.prototype.getHfn= function(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
 */
ol.render3D.prototype.animate = function(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}
 */
ol.render3D.prototype.animating = function() {
  if (this.animate_ && new Date().getTime() - this.animate_ > this.animateDuration_) {
    this.animate_ = false;
  }
  return !!this.animate_;
}
/** Get feature height
 * @param {ol.Feature} f
 */
ol.render3D.prototype.getFeatureHeight = function (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
 */
ol.render3D.prototype.hvector_ = function (pt, h) {
  var p0 = [
    pt[0]*this.matrix_[0] + pt[1]*this.matrix_[1] + this.matrix_[4],
    pt[0]*this.matrix_[2] + pt[1]*this.matrix_[3] + this.matrix_[5]
  ];
  return {
    p0: p0, 
    p1: [
      p0[0] + h/this.res_ * (p0[0]-this.center_[0]),
      p0[1] + h/this.res_ * (p0[1]-this.center_[1])
    ]
  };
};
/** Get drawing
 * @private
 */
ol.render3D.prototype.getFeature3D_ = function (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
 */
ol.render3D.prototype.drawFeature3D_ = function(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
 */
ol.render3D.prototype.drawGhost3D_ = function(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 = function(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
      })
    }));
  }
  this._geom = [];
  ol.layer.Vector.call (this, {
    name: 'sketch',
    source: new ol.source.Vector({ useSpatialIndex: false }),
    style: function(f) {
      return (f.get('sketch') ? sketchStyle : style);
    },
    updateWhileAnimating: true,
    updateWhileInteracting: true
  });
  // 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);
};
ol.ext.inherits (ol.layer.SketchOverlay, ol.layer.Vector);
/** Set geometry type
 * @param {string} type Geometry type
 * @return {string} the current type
 */
ol.layer.SketchOverlay.prototype.setGeometryType = function(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
 */
ol.layer.SketchOverlay.prototype.getGeometryType = function() {
  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
 */
ol.layer.SketchOverlay.prototype.addPoint = function(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
 */
ol.layer.SketchOverlay.prototype.removeLastPoint = function() {
  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)
 */
ol.layer.SketchOverlay.prototype.startDrawing = function(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
 */
ol.layer.SketchOverlay.prototype.finishDrawing = function(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
 */
ol.layer.SketchOverlay.prototype.abortDrawing = function() {
  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
 */
ol.layer.SketchOverlay.prototype.setPosition = function(coord) {
  this._position = coord;
  this.drawLink();
};
/** Get current position
 * @return {ol.coordinate} 
 */
ol.layer.SketchOverlay.prototype.getPosition = function() {
  return this._position;
};
/** Draw/refresh link
 */
ol.layer.SketchOverlay.prototype.drawLink = function() {
  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
 */
ol.layer.SketchOverlay.prototype.getFeature = function() {
  return this.getSource().getFeatures()[2];
};
/** Draw/refresh sketch
 */
ol.layer.SketchOverlay.prototype.drawSketch = function() {
  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 = function(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;
  ol.Map.call (this, opts);
  this._tiltCondition = options.tiltCondition || ol.events.condition.altKeyOnly;
};
ol.ext.inherits (ol.PerspectiveMap, ol.Map);
/** Get pixel ratio for the map
 */
ol.PerspectiveMap.prototype.getPixelRatio = function(){
  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).
 */
ol.PerspectiveMap.prototype.setPerspective = function(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
 */
ol.PerspectiveMap.prototype._animatePerpective = function(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.
 */
ol.PerspectiveMap.prototype.handleMapBrowserEvent = function(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>>} 
 */
ol.PerspectiveMap.prototype.getMatrix3D = function (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} 
 */
ol.PerspectiveMap.prototype.getPixelScreenFromCoordinate = function (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...
 * 
 */
ol.PerspectiveMap.prototype.getPixelFromPixelScreen = function (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]];  
};
/* 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).
*/
/*global ol*/
if (window.ol && !ol.particule) {
  ol.particule = {};
}
/** 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} coordinate the position of the particule
 */
ol.particule.Base = function(options) {
  if (!options) options = {};
  ol.Object.call(this);
  this.setOverlay(options.overlay) ;
  this.coordinate = options.coordinate || [0,0];
};
ol.ext.inherits(ol.particule.Base, ol.Object);
/** Set the particule overlay
 * @param {ol.Overlay} overl
 */
ol.particule.Base.prototype.setOverlay = function(overlay) {
  this._overlay = overlay;
};
/** Get the particule overlay
 * @return {ol.Overlay}
 */
ol.particule.Base.prototype.getOverlay = function() {
  return this._overlay;
};
/** Draw the particule
 * @param { CanvasRenderingContext2D } ctx
 */
ol.particule.Base.prototype.draw = function(/* ctx */) {
};
/** Update the particule
 * @param {number} dt timelapes since last call
 */
ol.particule.Base.prototype.update = function(/* dt */) {
};
/** Update the particule
 * @param {number} dt timelapes since last call
 */
ol.particule.Base.prototype.getRandomCoord = function(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} coordinate the position of the particule
 */
ol.particule.Bird = function(options) {
  if (!options) options = {};
  ol.particule.Base.call(this, options);
  this.bird = new Image();
  this.bird.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]);
};
ol.ext.inherits(ol.particule.Bird, ol.particule.Base);
/** Draw the particule
 * @param {CanvasRenderingContext2D } ctx
 */
ol.particule.Bird.prototype.draw = function(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
 */
ol.particule.Bird.prototype.update = function(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} coordinate the position of the particule
 */
ol.particule.Cloud = function(options) {
  if (!options) options = {};
  ol.particule.Base.call(this, 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();
  }
};
ol.ext.inherits(ol.particule.Cloud, ol.particule.Base);
/** Draw the particule
 * @param {CanvasRenderingContext2D } ctx
 */
ol.particule.Cloud.prototype.draw = function(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
 */
ol.particule.Cloud.prototype.update = function(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} coordinate the position of the particule
 */
ol.particule.Rain = function(options) {
  if (!options) options = {};
  ol.particule.Base.call(this, 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)');
};
ol.ext.inherits(ol.particule.Rain, ol.particule.Base);
/** Draw the particule
 * @param {CanvasRenderingContext2D } ctx
 */
ol.particule.Rain.prototype.draw = function(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
 */
ol.particule.Rain.prototype.update = function(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} coordinate the position of the particule
 */
ol.particule.RainDrop = function(options) {
  if (!options) options = {};
  ol.particule.Base.call(this, 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);
};
ol.ext.inherits(ol.particule.RainDrop, ol.particule.Base);
/** Draw the particule
 * @param {CanvasRenderingContext2D } ctx
 */
ol.particule.RainDrop.prototype.draw = function(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
 */
ol.particule.RainDrop.prototype.update = function(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} coordinate the position of the particule
 */
ol.particule.Snow = function(options) {
  if (!options) options = {};
  ol.particule.Base.call(this, 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);
};
ol.ext.inherits(ol.particule.Snow, ol.particule.Base);
/** Draw the particule
 * @param {CanvasRenderingContext2D } ctx
 */
ol.particule.Snow.prototype.draw = function(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
 */
ol.particule.Snow.prototype.update = function(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 = function (options) {
  options = options || {};
  if (typeof(options.offsetBox)==='number') this.offsetBox = [options.offsetBox,options.offsetBox,options.offsetBox,options.offsetBox];
  else this.offsetBox = options.offsetBox;
  // Popup div
  var element = document.createElement("div");
  //element.classList.add('ol-overlaycontainer-stopevent');
  options.element = element;
  // 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(); });
  }
  ol.Overlay.call(this, options);
  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));
  }
};
ol.ext.inherits(ol.Overlay.Popup, ol.Overlay);
/**
 * Get CSS class of the popup according to its positioning.
 * @private
 */
ol.Overlay.Popup.prototype.getClassPositioning = function () {
  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
 */
ol.Overlay.Popup.prototype.setClosebox = function (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
 */
ol.Overlay.Popup.prototype.setPopupClass = function (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
 */
ol.Overlay.Popup.prototype.addPopupClass = function (c) {
  this.element.classList.add(c);
};
/**
 * Remove a CSS class to the popup.
 * @param {string} c class name.
 * @api stable
 */
ol.Overlay.Popup.prototype.removePopupClass = function (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
 */
ol.Overlay.Popup.prototype.setPositioning = function (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
 */
ol.Overlay.Popup.prototype.setPositioning_ = function (pos) {
  if (this.element) {
    ol.Overlay.prototype.setPositioning.call(this, 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}
*/
ol.Overlay.Popup.prototype.getVisible = function () {
  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
*/
ol.Overlay.Popup.prototype.show = function (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
 */
ol.Overlay.Popup.prototype.hide = function () {
  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 = function(options) {
  if (!options) options = {};
  this._canvas = ol.ext.element.create('CANVAS', {
    className: ((options.className || '') + ' ol-animated-overlay').trim()
  });
  this._ctx = this._canvas.getContext('2d');
  ol.Overlay.call(this, {
    element: this._canvas,
    stopEvent: false
  });
  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));
};
ol.ext.inherits(ol.Overlay.AnimatedCanvas, ol.Overlay);
/** Set the visibility
 * @param {boolean} b
 */
ol.Overlay.AnimatedCanvas.prototype.setVisible = function (b) {
  this.element.style.display = b ? 'block' : 'none';
  if (b) this.setAnimation(this.get('animation'));
}
/** Get the visibility
 * @return {boolean} b
 */
ol.Overlay.AnimatedCanvas.prototype.getVisible = function () {
  return this.element.style.display != 'none';
}
/** No update for this overlay
 */
ol.Overlay.AnimatedCanvas.prototype.updatePixelPosition = function () {};
/**
 * Set the map instance the overlay is associated with
 * @param {ol.Map} map The map instance.
 */
ol.Overlay.AnimatedCanvas.prototype.setMap = function (map) {
  if (this.getMap()) {
    this.getMap().getViewport().querySelector('.ol-overlaycontainer').removeChild(this._canvas);
  }
  this._listener.forEach(function(l) {
    ol.Observable.unByKey(l);
  });
  this._listener = [];
  ol.Overlay.prototype.setMap.call(this, map);
  if (map) {
    var size = map.getSize();
    this._canvas.width = size[0];
    this._canvas.height = size[1];
    this.draw();
    this._listener.push (map.on('change:size', function()  {
      var size = map.getSize();
      if (this._canvas.width !== size[0] || this._canvas.height !== size[1]) {
        this._canvas.width = size[0];
        this._canvas.height = size[1];
        this.draw();
      }
    }.bind(this)));
  }
};
/** Create particules or return exiting ones
 */
ol.Overlay.AnimatedCanvas.prototype.getParticules = function() {
  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
 */
ol.Overlay.AnimatedCanvas.prototype._createParticule = function(overlay, coordinate) {
  return new this._particuleClass({
    overlay: overlay, 
    coordinate: coordinate
  });
};
/** Get random coordinates on canvas
 */
ol.Overlay.AnimatedCanvas.prototype.randomCoord = function() {
  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
 */
ol.Overlay.AnimatedCanvas.prototype.draw = function(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
 */
ol.Overlay.AnimatedCanvas.prototype.testExit = function(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
 */
ol.Overlay.AnimatedCanvas.prototype.clear = function() {
  this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
};
/** Get overlay canvas
 * @return {CanvasElement}
 */
ol.Overlay.AnimatedCanvas.prototype.getCanvas = function() {
  return this._canvas;
};
/** Set canvas animation
 * @param {boolean} anim, default true
 * @api
 */
ol.Overlay.AnimatedCanvas.prototype.setAnimation = function(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
 */
ol.Overlay.AnimatedCanvas.prototype._animate = function(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 = function (options) {
  ol.Overlay.call(this, options);
};
ol.ext.inherits(ol.Overlay.Fixed, ol.Overlay);
/** 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
 */
ol.Overlay.Fixed.prototype.setPosition = function(position, force) {
  if (this.getMap() && position) {
    this._pixel = this.getMap().getPixelFromCoordinate(position);
  }
  ol.Overlay.prototype.setPosition.call(this, position)
  if (force) {
    ol.Overlay.prototype.updatePixelPosition.call(this);
  } 
};
/** Update position according the pixel position
 */
ol.Overlay.Fixed.prototype.updatePixelPosition = function() {
  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 = function (options) {
  options.anchor = false;
  options.positioning = options.positioning || 'center-center';
  options.className = (options.className || '') + ' ol-fixPopup';
  ol.Overlay.Popup.call(this, options);
  this.set('minScale', options.minScale || .5);
  this.set('maxScale', options.maxScale || 2);
  var canvas = document.createElement('canvas');
  this._overlay = new ol.layer.Image({
    source: new ol.source.ImageCanvas({
      canvasFunction: function(extent, res, ratio, size) {
        canvas.width  = size[0];
        canvas.height = size[1];
        return canvas
      }
    })
  });
  this._style = options.style || new ol.style.Style({
    fill: new ol.style.Fill({ color: [102,153,255] })
  });
  this._overlay.on(['postcompose','postrender'], function(e) {
    if (this.getVisible() && this._pixel) {
      var map = this.getMap();
      var position = this.getPosition();
      var pixel = map.getPixelFromCoordinate(position);
      var r1 = this.element.getBoundingClientRect()
      var r2 = this.getMap().getTargetElement().getBoundingClientRect();
      var pixel2 = [r1.left-r2.left+r1.width/2, r1.top-r2.top+r1.height/2]
      e.context.save();
        var tr = e.inversePixelTransform;
        if (tr) {
          e.context.transform(tr[0],tr[1],tr[2],tr[3],tr[4],tr[5]);
        } else {
          // ol ~ v5.3.0
          e.context.scale(e.frameState.pixelRatio,e.frameState.pixelRatio)
        }
        e.context.beginPath();
        e.context.moveTo(pixel[0], pixel[1]);
        if (Math.abs(pixel2[0]-pixel[0]) > Math.abs(pixel2[1]-pixel[1])) {
          e.context.lineTo(pixel2[0],pixel2[1]-8);
          e.context.lineTo(pixel2[0],pixel2[1]+8);
        } else {
          e.context.lineTo(pixel2[0]-8,pixel2[1]);
          e.context.lineTo(pixel2[0]+8,pixel2[1]);
        }
        e.context.moveTo(pixel[0], pixel[1]);
        if (this._style.getFill()) {
          e.context.fillStyle = ol.color.asString(this._style.getFill().getColor());
          e.context.fill();
        }
        if (this._style.getStroke()) {
          e.context.strokeStyle = ol.color.asString(this._style.getStroke().getColor());
          e.context.lineWidth = this._style.getStroke().width();
          e.context.stroke();
        }
      e.context.restore();
    }
  }.bind(this));
  var update = function() { 
    this.setPixelPosition();
  }.bind(this);
  this.on(['hide', 'show'], function() {
    setTimeout(update)
  }.bind(this))
  // Get events centroid
  function centroid(pevents) {
    var clientX = 0;
    var clientY = 0;
    var length = 0;
    for (var i in pevents) {
      clientX += pevents[i].clientX;
      clientY += pevents[i].clientY;
      length++;
    }
    return [clientX / length, clientY / length];
  }
  // Get events angle
  function angle() {
    var p1,p2, v = Object.keys(pointerEvents);
    if (v.length<2) return false;
    p1 = pointerEvents[v[0]];
    p2 = pointerEvents[v[1]];
    var v1 = [p2.clientX - p1.clientX, p2.clientY - p1.clientY];
    p1 = pointerEvents2[v[0]];
    p2 = pointerEvents2[v[1]];
    var v2 = [p2.clientX - p1.clientX, p2.clientY - p1.clientY];
    var d1 = Math.sqrt(v1[0]*v1[0]+v1[1]*v1[1]);
    var d2 = Math.sqrt(v2[0]*v2[0]+v2[1]*v2[1]);
    var a = Math.acos((v1[0]*v2[0]+v1[1]*v2[1]) / (d1*d2)) * 360 / Math.PI;
    if (v1[0]*v2[1]-v1[1]*v2[0] < 0) return -a;
    else return a;
  }
  // Get distance beetween events
  function distance(pevents) {
    var v = Object.keys(pevents);
    if (v.length<2) return false;
    return ol.coordinate.dist2d([pevents[v[0]].clientX, pevents[v[0]].clientY], [pevents[v[1]].clientX, pevents[v[1]].clientY]);
  }
  // Handle popup move
  var pointerEvents = {};
  var pointerEvents2 = {};
  var pixelPosition = [];
  var distIni, rotIni, scaleIni, move;
  // down
  this.element.addEventListener('pointerdown', function(e) {
    e.preventDefault();
    e.stopPropagation();
    // Reset events to this position
    for (var i in pointerEvents) {
      if (pointerEvents2[i]) {
        pointerEvents[i] = pointerEvents2[i];
      }
    }
    pointerEvents[e.pointerId] = e;
    pixelPosition = this._pixel;
    rotIni = this.get('rotation') || 0;
    scaleIni = this.get('scale') || 1;
    distIni = distance(pointerEvents);
    move = false;
  }.bind(this));
  // Prevent click when move
  this.element.addEventListener('click', function(e) {
    if (move) {
      e.preventDefault();
      e.stopPropagation();
    }
  }, true);
  // up / cancel
  var removePointer = function(e) {
    if (pointerEvents[e.pointerId]) {
      delete pointerEvents[e.pointerId];
      e.preventDefault();
    }
    if (pointerEvents2[e.pointerId]) {
      delete pointerEvents2[e.pointerId];
    }
    /* Simulate a second touch pointer * /
    if (e.metaKey || e.ctrlKey) {
      pointerEvents['touch'] = e;
      pointerEvents2['touch'] = e;
    } else {
      delete pointerEvents['touch'];
      delete pointerEvents2['touch'];
    }
    /**/
  }.bind(this);
  document.addEventListener('pointerup', removePointer);
  document.addEventListener('pointercancel', removePointer);
  // move
  document.addEventListener('pointermove', function(e) {
    if (pointerEvents[e.pointerId]) {
      e.preventDefault();
      pointerEvents2[e.pointerId] = e;
      var c1 = centroid(pointerEvents);
      var c2 = centroid(pointerEvents2);
      var dx = c2[0] - c1[0];
      var dy = c2[1] - c1[1];
      move = move || Math.abs(dx) > 3 || Math.abs(dy) > 3;
      var a = angle();
      if (a) {
        this.setRotation(rotIni + a*1.5, false);
      }
      var d = distance(pointerEvents2);
      if (d!==false && distIni) {
        this.setScale(scaleIni * d / distIni, false);
        distIni = scaleIni * d / this.get('scale');
      }
      this.setPixelPosition([pixelPosition[0]+dx, pixelPosition[1]+dy]);
    }
  }.bind(this));
};
ol.ext.inherits(ol.Overlay.FixedPopup, ol.Overlay.Popup);
/**
 * Set the map instance the control is associated with
 * and add its controls associated to this map.
 * @param {_ol_Map_} map The map instance.
 */
ol.Overlay.FixedPopup.prototype.setMap = function (map) {
  ol.Overlay.Popup.prototype.setMap.call(this, 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
 */
ol.Overlay.FixedPopup.prototype.updatePixelPosition = function () {
  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
 */
ol.Overlay.FixedPopup.prototype.updateRenderedPosition = function (pixel, mapsize) {
  ol.Overlay.Popup.prototype.updateRenderedPosition.call(this, pixel, mapsize);
  this.setRotation();
  this.setScale()
};
/** Set pixel position
 * @param {ol.pixel} pix
 * @param {string} position top/bottom/middle-left/right/center
 */
ol.Overlay.FixedPopup.prototype.setPixelPosition = function (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 && 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}
 */
ol.Overlay.FixedPopup.prototype.getPixelPosition = function () {
  return this._pixel;
};
/**
 * Set the CSS class of the popup.
 * @param {string} c class name.
 * @api stable
 */
ol.Overlay.FixedPopup.prototype.setPopupClass = function (c) {
  ol.Overlay.Popup.prototype.setPopupClass.call(this, c);
  this.addPopupClass('ol-fixPopup');
};
/** Set poppup rotation
 * @param {number} angle
 * @param {booelan} update update popup, default true
 * @api
 */
ol.Overlay.FixedPopup.prototype.setRotation = function (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
 */
ol.Overlay.FixedPopup.prototype.setScale = function (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
 */
ol.Overlay.FixedPopup.prototype.setLinkStyle = function (style) {
  this._style = style;
  this._overlay.changed();
};
/** Get link style
 * @return {ol.style.Style} style
 */
ol.Overlay.FixedPopup.prototype.getLinkStyle = function () {
  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 = function (options) {
  var elt = document.createElement("div");
      elt.className = "ol-magnify";
  this._elt = elt;
  ol.Overlay.call(this, {
    positioning: options.positioning || "center-center",
    element: this._elt,
    stopEvent: false
  });
  // 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));
};
ol.ext.inherits(ol.Overlay.Magnify, ol.Overlay);
/**
 * Set the map instance the overlay is associated with.
 * @param {ol.Map} map The map instance.
 */
ol.Overlay.Magnify.prototype.setMap = function(map) {
  if (this.getMap()) {
    this.getMap().getViewport().removeEventListener("mousemove", this.onMouseMove_);
  }
  if (this._listener) ol.Observable.unByKey(this._listener);
  this._listener = null;
  ol.Overlay.prototype.setMap.call(this, map);
  map.getViewport().addEventListener("mousemove", this.onMouseMove_.bind(this));
  this._listener = map.getView().on('propertychange', this.setView_.bind(this));
  this.setView_();
};
/** Get the magnifier map
*	@return {_ol_Map_}
*/
ol.Overlay.Magnify.prototype.getMagMap = function() {
  return this.mgmap_;
};
/** Magnify is active
*	@return {boolean}
*/
ol.Overlay.Magnify.prototype.getActive = function() {
  return this.get("active");
};
/** Activate or deactivate
*	@param {boolean} active
*/
ol.Overlay.Magnify.prototype.setActive = function(active) {
  return this.set("active", active);
};
/** Mouse move
 * @private
 */
ol.Overlay.Magnify.prototype.onMouseMove_ = function(e) {
  var self = this;
  if (!self.get("active")) {
    self.setPosition();
  } else {
    var px = self.getMap().getEventCoordinate(e);
    if (!self.external_) self.setPosition(px);
    self.mgview_.setCenter(px);
    if (!self._elt.querySelector('canvas') || self._elt.querySelector('canvas').style.display =="none") self.mgmap_.updateSize();
  }
};
/** View has changed
 * @private
 */
ol.Overlay.Magnify.prototype.setView_ = function(e) {
  if (!this.get("active")) {
    this.setPosition();
    return;
  }
  if (!e) {
    // refresh all
    this.setView_({key:'rotation'});
    this.setView_({key:'resolution'});
    return;
  }
  // Set the view params
  switch (e.key) {
    case 'rotation':
      this.mgview_.setRotation(this.getMap().getView().getRotation());
      break;
    case 'zoomOffset':
    case 'resolution': {
      var z = Math.max(0,this.getMap().getView().getZoom()+Number(this.get("zoomOffset")));
      this.mgview_.setZoom(z);
      break;
    }
    default: break;
  }
};

/*	Copyright (c) 2018 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/**
 * @classdesc
 * A placemark element to be displayed over the map and attached to a single map
 * location. The placemarks are customized using CSS.
 *
 * @example
var popup = new ol.Overlay.Placemark();
map.addOverlay(popup);
popup.show(coordinate);
popup.hide();
*
* @constructor
* @extends {ol.Overlay}
* @param {} options Extend ol/Overlay/Popup options 
*	@param {String} options.color placemark color
*	@param {String} options.backgroundColor placemark color
*	@param {String} options.contentColor placemark color
*	@param {Number} options.radius placemark radius in pixel
*	@param {String} options.popupClass the a class of the overlay to style the popup.
*	@param {function|undefined} options.onclose: callback function when popup is closed
*	@param {function|undefined} options.onshow callback function when popup is shown
* @api stable
*/
ol.Overlay.Placemark = function (options) {
  options = options || {};
  options.popupClass = (options.popupClass || '') + ' placemark anim'
  options.positioning = 'bottom-center',
  ol.Overlay.Popup.call(this, 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);
};
ol.ext.inherits(ol.Overlay.Placemark, ol.Overlay.Popup);
/**
 * 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).
 */
ol.Overlay.Placemark.prototype.show = function(coordinate, html) {
  if (coordinate===true) {
    coordinate = this.getPosition();
  }
  this.hide();
  ol.Overlay.Popup.prototype.show.apply(this, [coordinate, html]);
};
/**
 * Set the placemark color.
 * @param {string} color
 */
ol.Overlay.Placemark.prototype.setColor = function(color) {
  this.element.style.color = color;
};
/**
 * Set the placemark background color.
 * @param {string} color
 */
ol.Overlay.Placemark.prototype.setBackgroundColor = function(color) {
  this._elt.style.backgroundColor = color;
};
/**
 * Set the placemark content color.
 * @param {string} color
 */
ol.Overlay.Placemark.prototype.setContentColor = function(color) {
  var c = this.element.getElementsByClassName('ol-popup-content')[0];
  if (c) c.style.color = color;
};
/**
 * Set the placemark class.
 * @param {string} name
 */
ol.Overlay.Placemark.prototype.setClassName = function(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
 */
ol.Overlay.Placemark.prototype.setRadius = function(size) {
  this.element.style.fontSize = size + 'px';
};

/*	Copyright (c) 2018 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** Template attributes for popup
 * @typedef {Object} TemplateAttributes
 * @property {string} title
 * @property {function} format a function that takes an attribute and a feature and returns the formated attribute
 * @property {string} before string to instert before the attribute (prefix)
 * @property {string} after string to instert after the attribute (sudfix)
 * @property {boolean|function} visible boolean or a function (feature, value) that decides the visibility of a attribute entry
 */
/** Template 
 * @typedef {Object} Template
 * @property {string|function} title title of the popup, attribute name or a function that takes a feature and returns the title
 * @property {Object.<TemplateAttributes>} attributes a list of template attributes 
 */
/**
 * A popup element to be displayed on a feature.
 *
 * @constructor
 * @extends {ol.Overlay.Popup}
 * @fires show
 * @fires hide
 * @fires select
 * @param {} options Extend Popup options 
 *  @param {String} options.popupClass the a class of the overlay to style the popup.
 *  @param {bool} options.closeBox popup has a close box, default false.
 *  @param {function|undefined} options.onclose: callback function when popup is closed
 *  @param {function|undefined} options.onshow callback function when popup is shown
 *  @param {Number|Array<number>} options.offsetBox an offset box
 *  @param {ol.OverlayPositioning | string | undefined} options.positionning 
 *    the 'auto' positioning var the popup choose its positioning to stay on the map.
 *  @param {Template|function} options.template A template with a list of properties to use in the popup or a function that takes a feature and returns a Template
 *  @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 = function (options) {
  options = options || {};
  ol.Overlay.Popup.call(this, 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));
  }
};
ol.ext.inherits(ol.Overlay.PopupFeature, ol.Overlay.Popup);
/** Set the template
 * @param {Template} template A template with a list of properties to use in the popup
 */
ol.Overlay.PopupFeature.prototype.setTemplate = function(template) {
  this._template = template;
  this._attributeObject(this._template);
}
/**
 * @private
 */
 ol.Overlay.PopupFeature.prototype._attributeObject = function (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
 */
ol.Overlay.PopupFeature.prototype.show = function(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();
    }
    ol.Overlay.Popup.prototype.show.call(this, coordinate, html);
  } else {
    this.hide();
  }
};
/**
 * @private
 */
ol.Overlay.PopupFeature.prototype._getHtml = function(feature) {
  if (!feature) return '';
  var html = ol.ext.element.create('DIV', { className: 'ol-popupfeature' });
  if (this.get('canFix')) {
    ol.ext.element.create('I', { className:'ol-fix', parent: html })
      .addEventListener('click', function(){
        this.element.classList.toggle('ol-fixed');
      }.bind(this));
  }
  var template = this._template;
  // calculate template
  if (typeof(template) === 'function') {
    template = template(feature, this._count, this._features.length);
  } else if (!template || !template.attributes) {
    template = template || {};
    template. attributes = {};
    for (var i in feature.getProperties()) if (i!='geometry') {
      template.attributes[i] = i;
    }
  }  
  // Display title
  if (template.title) {
    var title;
    if (typeof template.title === 'function') {
      title = template.title(feature);
    } else {
      title = feature.get(template.title);
    }
    ol.ext.element.create('H1', { html:title, parent: html });
  }
  // Display properties in a table
  if (template.attributes) {
    var tr, table = ol.ext.element.create('TABLE', { parent: html });
    var atts = this._attributeObject(template);
    var featureAtts = feature.getProperties();
    for (var att in atts) {
      if (featureAtts.hasOwnProperty(att)) {
        var a = atts[att];
        var content, val = featureAtts[att];
        // Get calculated value
        if (typeof(a.format)==='function') {
          val = a.format(val, feature);
        }
        // Is entry visible?
        var visible = true;
        if (typeof(a.visible)==='boolean') {
          visible = a.visible;
        } else if (typeof(a.visible)==='function') {
          visible = a.visible(feature, val);
        }
        if (visible) {
          tr = ol.ext.element.create('TR', { parent: table });
          ol.ext.element.create('TD', { html: a.title || 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 {
            content = (a.before||'') + val + (a.after||'');
            var maxc = this.get('maxChar') || 200;
            if (typeof(content) === 'string' && content.length>maxc) content = content.substr(0,maxc)+'[...]';
          }
          // Add value
          ol.ext.element.create('TD', {
            html: content,
            parent: tr
          });
        }
      }
    }
  }
  // Zoom button
  ol.ext.element.create('BUTTON', { className: 'ol-zoombt', parent: html })
    .addEventListener('click', function() {
      if (feature.getGeometry().getType()==='Point') {
        this.getMap().getView().animate({
          center: feature.getGeometry().getFirstCoordinate(),
          zoom:  Math.max(this.getMap().getView().getZoom(), 18)
        });
      } else  {
        var ext = feature.getGeometry().getExtent();
        this.getMap().getView().fit(ext, { duration:1000 });
      }
    }.bind(this));
  // Counter
  if (!this.get('keepSelection') && this._features.length > 1) {
    var div = ol.ext.element.create('DIV', { className: 'ol-count', parent: html });
    ol.ext.element.create('DIV', { 
      className: 'ol-prev', 
      parent: div,
      click: function() {
        this._count--;
        if (this._count<1) this._count = this._features.length;
        html = this._getHtml(this._features[this._count-1]);
        setTimeout(function() { 
          ol.Overlay.Popup.prototype.show.call(this, this.getPosition(), html); 
        }.bind(this), 350 );
      }.bind(this)
    });
    ol.ext.element.create('TEXT', { html:this._count+'/'+this._features.length, parent: div });
    ol.ext.element.create('DIV', { 
      className: 'ol-next', 
      parent: div,
      click: function() {
        this._count++;
        if (this._count>this._features.length) this._count = 1;
        html = this._getHtml(this._features[this._count-1]);
        setTimeout(function() { 
          ol.Overlay.Popup.prototype.show.call(this, this.getPosition(), html); 
        }.bind(this), 350 );
      }.bind(this)
    });
  }
  // Use select interaction
  if (this._select && !this.get('keepSelection')) {
    this._noselect = true;
    this._select.getFeatures().clear();
    this._select.getFeatures().push(feature);
    this._noselect = false;
  }
  this.dispatchEvent({ type: 'select', feature: feature, index: this._count })
  return html;
};
/** Fix the popup
 * @param {boolean} fix
 */
ol.Overlay.PopupFeature.prototype.setFix = function (fix) {
  if (fix) this.element.classList.add('ol-fixed');
  else this.element.classList.remove('ol-fixed');
};
/** Is a popup fixed
 * @return {boolean} 
 */
ol.Overlay.PopupFeature.prototype.getFix = function () {
  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 = function (options) {
  options = options || {};
  options.popupClass = options.popupClass || options.className || 'tooltips black';
  options.positioning = options.positioning || 'center-left';
  options.stopEvent = !!(options.stopEvent);
  ol.Overlay.Popup.call(this, 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)
  });
};
ol.ext.inherits(ol.Overlay.Tooltip, ol.Overlay.Popup);
/**
 * Set the map instance the control is associated with
 * and add its controls associated to this map.
 * @param {_ol_Map_} map The map instance.
 */
ol.Overlay.Tooltip.prototype.setMap = function (map) {
  if (this.getMap()) this.getMap().removeInteraction(this._interaction);
  ol.Overlay.Popup.prototype.setMap.call(this, 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
 */
ol.Overlay.Tooltip.prototype.getHTML = function(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
 */
ol.Overlay.Tooltip.prototype.setInfo = function(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
 */
ol.Overlay.Tooltip.prototype.removeFeature = function() {
  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
 */
ol.Overlay.Tooltip.prototype.formatArea = function(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
 */
ol.Overlay.Tooltip.prototype.formatLength = function(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
 */
ol.Overlay.Tooltip.prototype.setFeature = function(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.Dijskra = function (options) {
  options = options || {};
  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 = [];
  ol.Object.call (this);
  this.set ('epsilon', options.epsilon || 1E-6);
};
ol.ext.inherits(ol.graph.Dijskra, ol.Object);
/** 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
 */
ol.graph.Dijskra.prototype.weight = function(/* 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
 */
ol.graph.Dijskra.prototype.direction = function(/* feature */) {
  return 2;
};
/** Calculate the length of an edge
 * @param {ol/Feature|ol/geom/LineString} geom
 * @return {number}
 * @api
 */
ol.graph.Dijskra.prototype.getLength = function(geom) {
  if (geom.getGeometry) geom = geom.getGeometry();
  return geom.getLength();
};
/** Get the nodes source concerned in the calculation
 * @return {ol/source/Vector}
 */
ol.graph.Dijskra.prototype.getNodeSource = function() {
  return this.nodes;
};
/** Get all features at a coordinate
 * @param {ol/coordinate} coord
 * @return {Array<ol/Feature>}
 */
ol.graph.Dijskra.prototype.getEdges = function(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
 */
ol.graph.Dijskra.prototype.getNode = function(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
 */
ol.graph.Dijskra.prototype.addNode = function(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
 */
ol.graph.Dijskra.prototype.closestCoordinate = function(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
 */
ol.graph.Dijskra.prototype.path = function(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
 */
ol.graph.Dijskra.prototype.resume = function() {
  if (this.running) return;
  if (this.candidat.length) {
    this.running = true;
    this.nb = 0;
    this._resume();
  }
};
/** Pause 
 */
ol.graph.Dijskra.prototype.pause = function() {
  if (!this.running) return;
  this.nb = -1;
};
/** Get the current 'best way'.
 * This may be used to animate while calculating.
 * @return {Array<ol/Feature>}
 */
ol.graph.Dijskra.prototype.getBestWay = function() {
  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
 */
ol.graph.Dijskra.prototype._resume = function() {
  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
 */
ol.graph.Dijskra.prototype.getRoute = function(node) {
  var route = [];
  while (node) {
    route.unshift(node.get('from'));
    node = node.get('prev');
  }
  route.shift();
  return route;
};

/** French Geoportail alti coding
 * @param {ol.geom.Geometry} geom
 * @param {Object} options
 *  @param {ol/proj~ProjectionLike} [options.projection='EPSG:3857'] geometry projection, default 'EPSG:3857'
 *  @param {string} [options.apiKey='essentiels'] Geoportail API key
 *  @param {number} [options.sampling=0] number of resulting point, max 5000, if none keep input points or use samplingDist
 *  @param {number} [options.samplingDist=0] distance for sampling the line or use sampling if lesser
 *  @param {string} options.success a function that takes the resulting XYZ geometry
 *  @param {string} options.error
 */
ol.geom.GPAltiCode = function(geom, options) {
  options = options || {};
  var typeGeom = geom.getType();
  if (typeGeom !== 'Point' && typeGeom !== 'LineString') {
    console.warn('[GPAltiCode] '+typeGeom+' not supported...')
    return;
  }
  var proj = options.projection || 'EPSG:3857';
  var sampling = options.sampling || 0;
  if (options.samplingDist) {
    var d = geom.getLength();
    sampling = Math.max(sampling, Math.round(d / options.samplingDist));
  }
  if (sampling > 5000) sampling = 5000;
  if (sampling < 2) sampling = 0;
  geom = geom.clone().transform(proj, 'EPSG:4326');
  var g, lon = [], lat = [];
  switch (typeGeom) {
    case 'Point': {
      g = [geom.getCoordinates()];
      break;
    }
    case 'LineString': {
      g = geom.getCoordinates();
      break;
    }
    default: return;
  }
  if (sampling <= g.length) sampling = 0;
  g.forEach(function(p) {
    lon.push(Math.round(p[0]*1000000)/1000000);
    lat.push(Math.round(p[1]*1000000)/1000000);
  });
  // Get elevation
  var param = 'lon='+lon.join('|')+'&lat='+lat.join('|');
  if (sampling) param += '&sampling='+sampling;
  ol.ext.Ajax.get({
    url: 'https://wxs.ign.fr/'+(options.apiKey || 'essentiels')+'/alti/rest/'+(lon.length>1 ? 'elevationLine' : 'elevation')+'.json?'+param,
    success: function(res) {
      var pts = [];
      res.elevations.forEach(function(e, i) {
        if (sampling) {
          pts.push([e.lon, e.lat, e.z]);
        } else {
          pts.push([g[i][0], g[i][1], e.z]);
        }
      });
      if (typeGeom==='Point') pts = pts[0];
      var result = ol.geom.createFromType(typeGeom, pts);
      result.transform('EPSG:4326', proj);
      if (typeof(options.success) === 'function') options.success(result);
    },
    error: function(e) {
      if (typeof(options.error) === 'function') options.error(e);
    }
  });
}
/** Calculate elevation on coordinates or on a set of coordinates
 * @param {ol.coordinate|Array<ol.coordinate>} coord coordinate or an array of coordinates
 * @param {Object} options
 *  @param {ol/proj~ProjectionLike} [options.projection='EPSG:3857'] geometry projection, default 'EPSG:3857'
 *  @param {string} [options.apiKey='essentiels'] Geoportail API key
 *  @param {number} [options.sampling=0] number of resulting point, max 5000, if none keep input points or use samplingDist
 *  @param {number} [options.samplingDist=0] distance for sampling the line or use sampling if lesser
 *  @param {string} options.success a function that takes the resulting XYZ coordinates
 *  @param {string} options.error
 */
ol.coordinate.GPAltiCode = function(coord, options) {
  options = options || {};
  var unique = !coord[0].length;
  var g = unique ? new ol.geom.Point(coord) : new ol.geom.LineString(coord);
  ol.geom.GPAltiCode(g, {
    projection: options.projection,
    apiKey: options.apiKey,
    sampling: options.sampling,
    samplingDist: options.samplingDist,
    success: function(g) {
      if (typeof(options.success) === 'function') {
        options.success(g.getCoordinates())
      }
    },
    error: options.error
  })
}

/*	Copyright (c) 2016 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
  Usefull function to handle geometric operations
*/
/** Distance beetween 2 points
 *	Usefull geometric functions
 * @param {ol.Coordinate} p1 first point
 * @param {ol.Coordinate} p2 second point
 * @return {number} distance
 */
ol.coordinate.dist2d = function(p1, p2) {
  var dx = p1[0]-p2[0];
  var dy = p1[1]-p2[1];
  return Math.sqrt(dx*dx+dy*dy);
}
/** 2 points are equal
 *	Usefull geometric functions
 * @param {ol.Coordinate} p1 first point
 * @param {ol.Coordinate} p2 second point
 * @return {boolean}
 */
ol.coordinate.equal = function(p1, p2) {
  return (p1[0]==p2[0] && p1[1]==p2[1]);
}
/** Get center coordinate of a feature
 * @param {ol.Feature} f
 * @return {ol.coordinate} the center
 */
ol.coordinate.getFeatureCenter = function(f) {
  return ol.coordinate.getGeomCenter (f.getGeometry());
};
/** Get center coordinate of a geometry
* @param {ol.geom.Geometry} geom
* @return {ol.Coordinate} the center
*/
ol.coordinate.getGeomCenter = function(geom) {
  switch (geom.getType()) {
    case 'Point': 
      return geom.getCoordinates();
    case "MultiPolygon":
      geom = geom.getPolygon(0);
      // fallthrough
    case "Polygon":
      return geom.getInteriorPoint().getCoordinates();
    default:
      return geom.getClosestPoint(ol.extent.getCenter(geom.getExtent()));
  }
};
/** Offset a polyline
 * @param {Array<ol.Coordinate>} coords
 * @param {number} offset
 * @return {Array<ol.Coordinate>} resulting coord
 * @see http://stackoverflow.com/a/11970006/796832
 * @see https://drive.google.com/viewerng/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxqa2dhZGdldHN0b3JlfGd4OjQ4MzI5M2Y0MjNmNzI2MjY
 */
ol.coordinate.offsetCoords = function (coords, offset) {
  var path = [];
  var N = coords.length-1;
  var max = N;
  var mi, mi1, li, li1, ri, ri1, si, si1, Xi1, Yi1;
  var p0, p1, p2;
  var isClosed = ol.coordinate.equal(coords[0],coords[N]);
  if (!isClosed) {
    p0 = coords[0];
    p1 = coords[1];
    p2 = [
      p0[0] + (p1[1] - p0[1]) / ol.coordinate.dist2d(p0,p1) *offset,
      p0[1] - (p1[0] - p0[0]) / ol.coordinate.dist2d(p0,p1) *offset
    ];
    path.push(p2);
    coords.push(coords[N])
    N++;
    max--;
  }
  for (var i = 0; i < max; i++) {
    p0 = coords[i];
    p1 = coords[(i+1) % N];
    p2 = coords[(i+2) % N];
    mi = (p1[1] - p0[1])/(p1[0] - p0[0]);
    mi1 = (p2[1] - p1[1])/(p2[0] - p1[0]);
    // Prevent alignements
    if (Math.abs(mi-mi1) > 1e-10) {
      li = Math.sqrt((p1[0] - p0[0])*(p1[0] - p0[0])+(p1[1] - p0[1])*(p1[1] - p0[1]));
      li1 = Math.sqrt((p2[0] - p1[0])*(p2[0] - p1[0])+(p2[1] - p1[1])*(p2[1] - p1[1]));
      ri = p0[0] + offset*(p1[1] - p0[1])/li;
      ri1 = p1[0] + offset*(p2[1] - p1[1])/li1;
      si = p0[1] - offset*(p1[0] - p0[0])/li;
      si1 = p1[1] - offset*(p2[0] - p1[0])/li1;
      Xi1 = (mi1*ri1-mi*ri+si-si1) / (mi1-mi);
      Yi1 = (mi*mi1*(ri1-ri)+mi1*si-mi*si1) / (mi1-mi);
      // Correction for vertical lines
      if(p1[0] - p0[0] == 0) {
        Xi1 = p1[0] + offset*(p1[1] - p0[1])/Math.abs(p1[1] - p0[1]);
        Yi1 = mi1*Xi1 - mi1*ri1 + si1;
      }
      if (p2[0] - p1[0] == 0 ) {
        Xi1 = p2[0] + offset*(p2[1] - p1[1])/Math.abs(p2[1] - p1[1]);
        Yi1 = mi*Xi1 - mi*ri + si;
      }
      path.push([Xi1, Yi1]);
    }
  }
  if (isClosed) {
    path.push(path[0]);
  } else {
    coords.pop();
    p0 = coords[coords.length-1];
    p1 = coords[coords.length-2];
    p2 = [
      p0[0] - (p1[1] - p0[1]) / ol.coordinate.dist2d(p0,p1) *offset,
      p0[1] + (p1[0] - p0[0]) / ol.coordinate.dist2d(p0,p1) *offset
    ];
    path.push(p2);
  }
  return path;
}
/** Find the segment a point belongs to
 * @param {ol.Coordinate} pt
 * @param {Array<ol.Coordinate>} coords
 * @return {} the index (-1 if not found) and the segment
 */
ol.coordinate.findSegment = function (pt, coords) {
  for (var i=0; i<coords.length-1; i++) {
    var p0 = coords[i];
    var p1 = coords[i+1];
    if (ol.coordinate.equal(pt, p0) || ol.coordinate.equal(pt, p1)) {
      return { index:1, segment: [p0,p1] };
    } else {
      var d0 = ol.coordinate.dist2d(p0,p1);
      var v0 = [ (p1[0] - p0[0]) / d0, (p1[1] - p0[1]) / d0 ];
      var d1 = ol.coordinate.dist2d(p0,pt);
      var v1 = [ (pt[0] - p0[0]) / d1, (pt[1] - p0[1]) / d1 ];
      if (Math.abs(v0[0]*v1[1] - v0[1]*v1[0]) < 1e-10) {
        return { index:1, segment: [p0,p1] };
      }
    }
  }
  return { index: -1 };
};
/**
 * Split a Polygon geom with horizontal lines
 * @param {Array<ol.Coordinate>} geom
 * @param {number} y the y to split
 * @param {number} n contour index
 * @return {Array<Array<ol.Coordinate>>}
 */
ol.coordinate.splitH = function (geom, y, n) {
  var x, abs;
  var list = [];
  for (var i=0; i<geom.length-1; i++) {
    // Hole separator?
    if (!geom[i].length || !geom[i+1].length) continue;
    // Intersect
    if (geom[i][1]<=y && geom[i+1][1]>y || geom[i][1]>=y && geom[i+1][1]<y) {
      abs = (y-geom[i][1]) / (geom[i+1][1]-geom[i][1]);
      x = abs * (geom[i+1][0]-geom[i][0]) + geom[i][0];
      list.push ({ contour: n, index: i, pt: [x,y], abs: abs });
    }
  }
  // Sort x
  list.sort(function(a,b) { return a.pt[0] - b.pt[0] });
  // Horizontal segment
  var result = [];
  for (var j=0; j<list.length-1; j += 2) {
    result.push([list[j], list[j+1]])
  }
  return result;
};
/** Create a geometry given a type and coordinates */
ol.geom.createFromType = function (type, coordinates) {
  switch (type) {
    case 'LineString': return new ol.geom.LineString(coordinates);
    case 'LinearRing': return new ol.geom.LinearRing(coordinates);
    case 'MultiLineString': return new ol.geom.MultiLineString(coordinates);
    case 'MultiPoint': return new ol.geom.MultiPoint(coordinates);
    case 'MultiPolygon': return new ol.geom.MultiPolygon(coordinates);
    case 'Point': return new ol.geom.Point(coordinates);
    case 'Polygon': return new ol.geom.Polygon(coordinates);
    default:
      console.error('[createFromType] Unsupported type: '+type);
      return null;
  }
};
/** Intersect 2 lines
 * @param {Arrar<ol.coordinate>} d1
 * @param {Arrar<ol.coordinate>} d2
 */
ol.coordinate.getIntersectionPoint = function (d1, d2) {
  var d1x = d1[1][0] - d1[0][0];
  var d1y = d1[1][1] - d1[0][1];
  var d2x = d2[1][0] - d2[0][0];
  var d2y = d2[1][1] - d2[0][1];
  var det = d1x * d2y - d1y * d2x;
  if (det != 0) {
    var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det;
    return [d2[0][0] + k*d2x, d2[0][1] + k*d2y];
  } else {
    return false;
  }
};
ol.extent.intersection;
(function() {
// Split at x
function splitX(pts, x) {
  var pt;
  for (var i=pts.length-1; i>0; i--) {
    if ((pts[i][0]>x && pts[i-1][0]<x) || (pts[i][0]<x && pts[i-1][0]>x)) {
      pt = [ x, (x - pts[i][0]) / (pts[i-1][0]-pts[i][0]) * (pts[i-1][1]-pts[i][1]) + pts[i][1]];
      pts.splice(i, 0, pt);
    }
  }
}
// Split at y
function splitY(pts, y) {
  var pt;
  for (var i=pts.length-1; i>0; i--) {
    if ((pts[i][1]>y && pts[i-1][1]<y) || (pts[i][1]<y && pts[i-1][1]>y)) {
      pt = [ (y - pts[i][1]) / (pts[i-1][1]-pts[i][1]) * (pts[i-1][0]-pts[i][0]) + pts[i][0], y];
      pts.splice(i, 0, pt);
    }
  }
}
/** Fast polygon intersection with an extent (used for area calculation)
 * @param {ol.extent.Extent} extent
 * @param {ol.geom.Polygon|ol.geom.MultiPolygon} polygon
 * @returns {ol.geom.Polygon|ol.geom.MultiPolygon|null} return null if not a polygon geometry
 */
ol.extent.intersection = function(extent, polygon) {
  var poly = (polygon.getType() === 'Polygon');
  if (!poly && polygon.getType() !== 'MultiPolygon') return null;
  var geom = polygon.getCoordinates();
  if (poly) geom = [geom];
  geom.forEach(function(g) {
    g.forEach(function(c) {
      splitX(c, extent[0]);
      splitX(c, extent[2]);
      splitY(c, extent[1]);
      splitY(c, extent[3]);
    });
  })
  // Snap geom to the extent 
  geom.forEach(function(g) {
    g.forEach(function(c) {
      c.forEach(function(p) {
        if (p[0]<extent[0]) p[0] = extent[0];
        else if (p[0]>extent[2]) p[0] = extent[2];
        if (p[1]<extent[1]) p[1] = extent[1];
        else if (p[1]>extent[3]) p[1] = extent[3];
      })
    })
  })
  if (poly) {
    return new ol.geom.Polygon(geom[0]);
  } else {
    return new ol.geom.MultiPolygon(geom);
  }
};
})();
/** Add points along a segment
 * @param {ol.Coordinate} p1 
 * @param {ol.Coordinate} p2 
 * @param {number} d 
 * @param {boolean} start include starting point, default true
 * @returns {Array<ol.Coordinate>}
 */
ol.coordinate.sampleAt = function(p1, p2, d, start) {
  var pts = [];
  if (start!==false) pts.push(p1);
  var dl = ol.coordinate.dist2d(p1,p2);
  if (dl) {
    var nb = Math.round(dl/d);
    if (nb>1) {
      var dx = (p2[0]-p1[0]) / nb;
      var dy = (p2[1]-p1[1]) / nb;
      for (var i=1; i<nb; i++) {
        pts.push([p1[0] + dx*i, p1[1] + dy*i])
      }
    }
  }
  pts.push(p2);
  return pts;
};
/** Sample a 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 });
};
// import('ol-ext/geom/Scribble')
/** Geohash encoding/decoding and associated functions
 * (c) Chris Veness 2014-2019 / MIT Licence
 * https://github.com/chrisveness/latlon-geohash
 */
ol.geohash = {
  // (geohash-specific) Base32 map
  base32: '0123456789bcdefghjkmnpqrstuvwxyz'
};
/** Encodes latitude/longitude to geohash, either to specified precision or to automatically
 * evaluated precision.
 * @param   {ol.coordinate} lonlat Longitude, Latitude in degrees.
 * @param   {number} [precision] Number of characters in resulting geohash.
 * @returns {string} Geohash of supplied latitude/longitude.
 */
ol.geohash.fromLonLat = function(lonlat, precision) {
  var lon = lonlat[0];
  var lat = lonlat[1];
  // infer precision?
  if (!precision) {
    // refine geohash until it matches precision of supplied lat/lon
    for (var p=1; p<=12; p++) {
        var hash = ol.geohash.fromLonLat([lon, lat], p);
        var posn = ol.geohash.toLonLat(hash);
        if (posn.lat==lat && posn.lon==lon) return hash;
    }
    precision = 12; // set to maximum
  }
  if (precision < 1 || precision > 12) precision = 12;
  var idx = 0; // index into base32 map
  var bit = 0; // each char holds 5 bits
  var evenBit = true;
  var geohash = '';
  var latMin =  -90, latMax =  90;
  var lonMin = -180, lonMax = 180;
  while (geohash.length < precision) {
    if (evenBit) {
      // bisect E-W longitude
      var lonMid = (lonMin + lonMax) / 2;
      if (lon >= lonMid) {
        idx = idx*2 + 1;
        lonMin = lonMid;
      } else {
        idx = idx*2;
        lonMax = lonMid;
      }
    } else {
      // bisect N-S latitude
      var latMid = (latMin + latMax) / 2;
      if (lat >= latMid) {
        idx = idx*2 + 1;
        latMin = latMid;
      } else {
        idx = idx*2;
        latMax = latMid;
      }
    }
    evenBit = !evenBit;
    if (++bit == 5) {
      // 5 bits gives us a character: append it and start over
      geohash += ol.geohash.base32.charAt(idx);
      bit = 0;
      idx = 0;
    }
  }
  return geohash;
};
/** Decode geohash to latitude/longitude 
 * (location is approximate centre of geohash cell, to reasonable precision).
 * @param   {string} geohash - Geohash string to be converted to latitude/longitude.
 * @returns {ol.coordinate}
 */
ol.geohash.toLonLat = function(geohash) {
  var extent = ol.geohash.getExtent(geohash); // <-- the hard work
  // now just determine the centre of the cell...
  var latMin = extent[1], lonMin = extent[0];
  var latMax = extent[3], lonMax = extent[2];
  // cell centre
  var lat = (latMin + latMax)/2;
  var lon = (lonMin + lonMax)/2;
  // round to close to centre without excessive precision: ⌊2-log10(Δ°)⌋ decimal places
  lat = lat.toFixed(Math.floor(2-Math.log(latMax-latMin)/Math.LN10));
  lon = lon.toFixed(Math.floor(2-Math.log(lonMax-lonMin)/Math.LN10));
  return [Number(lon), Number(lat)];
};
/** Returns SW/NE latitude/longitude bounds of specified geohash.
 * @param   {string} geohash Cell that bounds are required of.
 * @returns {ol.extent | false} 
 */
ol.geohash.getExtent = function(geohash) {
  if (!geohash) return false;
  geohash = geohash.toLowerCase();
  var evenBit = true;
  var latMin =  -90, latMax =  90;
  var lonMin = -180, lonMax = 180;
  for (var i=0; i<geohash.length; i++) {
    var chr = geohash.charAt(i);
    var idx = ol.geohash.base32.indexOf(chr);
    if (idx == -1) return false;
    for (var n=4; n>=0; n--) {
      var bitN = idx >> n & 1;
      if (evenBit) {
        // longitude
        var lonMid = (lonMin+lonMax) / 2;
        if (bitN == 1) {
          lonMin = lonMid;
        } else {
          lonMax = lonMid;
        }
      } else {
        // latitude
        var latMid = (latMin+latMax) / 2;
        if (bitN == 1) {
          latMin = latMid;
        } else {
          latMax = latMid;
        }
      }
      evenBit = !evenBit;
    }
  }
  return [lonMin, latMin, lonMax, latMax];
};
/** Determines adjacent cell in given direction.
 * @param   {string} geohash Geohash cel
 * @param   {string} direction direction as char : N/S/E/W.
 * @returns {string|false} 
 */
ol.geohash.getAdjacent = function (geohash, direction) {
  // based on github.com/davetroy/geohash-js
  geohash = geohash.toLowerCase();
  direction = direction.toLowerCase();
  if (!geohash) return false;
  if ('nsew'.indexOf(direction) == -1) return false;
  var neighbour = {
      n: [ 'p0r21436x8zb9dcf5h7kjnmqesgutwvy', 'bc01fg45238967deuvhjyznpkmstqrwx' ],
      s: [ '14365h7k9dcfesgujnmqp0r2twvyx8zb', '238967debc01fg45kmstqrwxuvhjyznp' ],
      e: [ 'bc01fg45238967deuvhjyznpkmstqrwx', 'p0r21436x8zb9dcf5h7kjnmqesgutwvy' ],
      w: [ '238967debc01fg45kmstqrwxuvhjyznp', '14365h7k9dcfesgujnmqp0r2twvyx8zb' ],
  };
  var border = {
      n: [ 'prxz',     'bcfguvyz' ],
      s: [ '028b',     '0145hjnp' ],
      e: [ 'bcfguvyz', 'prxz'     ],
      w: [ '0145hjnp', '028b'     ],
  };
  var lastCh = geohash.slice(-1);    // last character of hash
  var parent = geohash.slice(0, -1); // hash without last character
  var type = geohash.length % 2;
  // check for edge-cases which don't share common prefix
  if (border[direction][type].indexOf(lastCh) != -1 && parent != '') {
    parent = ol.geohash.getAdjacent(parent, direction);
  }
  // append letter for direction to parent
  return parent + ol.geohash.base32.charAt(neighbour[direction][type].indexOf(lastCh));
}
/** Returns all 8 adjacent cells to specified geohash.
 * @param   {string} geohash Geohash neighbours are required of.
 * @returns {{n,ne,e,se,s,sw,w,nw: string}}
 */
ol.geohash.getNeighbours = function(geohash) {
  return {
    'n':  ol.geohash.getAdjacent(geohash, 'n'),
    'ne': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 'n'), 'e'),
    'e':  ol.geohash.getAdjacent(geohash, 'e'),
    'se': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 's'), 'e'),
    's':  ol.geohash.getAdjacent(geohash, 's'),
    'sw': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 's'), 'w'),
    'w':  ol.geohash.getAdjacent(geohash, 'w'),
    'nw': ol.geohash.getAdjacent(ol.geohash.getAdjacent(geohash, 'n'), 'w'),
  };
}

/** Compute great circle bearing of two points.
 * @See http://www.movable-type.co.uk/scripts/latlong.html for the original code
 * @param {ol.coordinate} origin origin in lonlat
 * @param {ol.coordinate} destination destination in lonlat
 * @return {number} bearing angle in radian
 */
ol.sphere.greatCircleBearing = function(origin, destination) {
  var toRad = Math.PI/180;
  var ori = [ origin[0]*toRad, origin[1]*toRad ];
  var dest = [ destination[0]*toRad, destination[1]*toRad ];
  var bearing = Math.atan2(
    Math.sin(dest[0] - ori[0]) * Math.cos(dest[1]),
    Math.cos(ori[1]) * Math.sin(dest[1]) - Math.sin(ori[1]) * Math.cos(dest[1]) * Math.cos(dest[0] - ori[0])
  );
  return bearing;
};
/** 
 * Computes the destination point given an initial point, a distance and a bearing
 * @See http://www.movable-type.co.uk/scripts/latlong.html for the original code
 * @param {ol.coordinate} origin stating point in lonlat coords
 * @param {number} distance
 * @param {number} bearing bearing angle in radian
 * @param {*} options
 *  @param {booelan} normalize normalize longitude beetween -180/180, deafulet true
 *  @param {number|undefined} options.radius sphere radius, default 6371008.8
 */
ol.sphere.computeDestinationPoint = function(origin, distance, bearing, options) {
  options = options || {};
  var toRad = Math.PI/180;
  var radius = options.radius || 6371008.8;
  var phi1 = origin[1] * toRad;
  var lambda1 = origin[0] * toRad;
  var delta = distance / radius;
  var phi2 = Math.asin(
    Math.sin(phi1) * Math.cos(delta) +
    Math.cos(phi1) * Math.sin(delta) * Math.cos(bearing)
  );
  var lambda2 = lambda1 +
    Math.atan2(
      Math.sin(bearing) * Math.sin(delta) * Math.cos(phi1),
      Math.cos(delta) - Math.sin(phi1) * Math.sin(phi2)
    );
  var lon = lambda2 / toRad;
  // normalise to >=-180 and <=180° 
  if (options.normalize!==false && (lon < -180 || lon > 180)) {
    lon = ((lon * 540) % 360) - 180;
  }
  return [ lon, phi2 / toRad ];
};
/** Calculate a track along the great circle given an origin and a destination
 * @param {ol.coordinate} origin origin in lonlat
 * @param {ol.coordinate} destination destination in lonlat
 * @param {number} distance distance between point along the track in meter, default 1km (1000)
 * @param {number|undefined} radius sphere radius, default 6371008.8
 * @return {Array<ol.coordinate>}
 */
ol.sphere.greatCircleTrack = function(origin, destination, options) {
  options = options || {};
  var bearing = ol.sphere.greatCircleBearing(origin, destination);
  var dist = ol.sphere.getDistance(origin, destination, options.radius);
  var distance = options.distance || 1000;
  var d = distance;
  var geom = [origin];
  while (d < dist) {
    geom.push(ol.sphere.computeDestinationPoint(origin, d, bearing, { radius: options.radius, normalize: false }));
    d += distance;
  }
  var pt = ol.sphere.computeDestinationPoint(origin, dist, bearing, { radius: options.radius, normalize: false });
  if (Math.abs(pt[0]-destination[0]) > 1) {
    if (pt[0] > destination[0]) destination[0] += 360;
    else destination[0] -= 360;
  } 
  geom.push(destination);
  return geom;
};
/** Get map scale factor
 * @param {ol.Map} map
 * @param {number} [dpi=96] dpi, default 96
 * @return {number}
 */
ol.sphere.getMapScale = function (map, dpi) {
  var view = map.getView();
  var proj = view.getProjection();
  var center = view.getCenter();
  var px = map.getPixelFromCoordinate(center);
  px[1] += 1;
  var coord = map.getCoordinateFromPixel(px);
  var d = ol.sphere.getDistance(
    ol.proj.transform(center, proj, 'EPSG:4326'),
    ol.proj.transform(coord, proj, 'EPSG:4326'));
  d *= (dpi||96) /.0254
  return d;
};
/** Set map scale factor
 * @param {ol.Map} map
 * @param {number|string} scale the scale factor or a scale string as 1/xxx
 * @param {number} [dpi=96] dpi, default 96
 * @return {number} scale factor
 */
ol.sphere.setMapScale = function (map, scale, dpi) {
  if (map && scale) {
    var fac = scale;
    if (typeof(scale)==='string') {
      fac = scale.split('/')[1];
      if (!fac) fac = scale;
      fac = fac.replace(/[^\d]/g,'');
      fac = parseInt(fac);
    }
    if (!fac) return;
    // Calculate new resolution
    var view = map.getView();
    var proj = view.getProjection();
    var center = view.getCenter();
    var px = map.getPixelFromCoordinate(center);
    px[1] += 1;
    var coord = map.getCoordinateFromPixel(px);
    var d = ol.sphere.getDistance(
      ol.proj.transform(center, proj, 'EPSG:4326'),
      ol.proj.transform(coord, proj, 'EPSG:4326'));
    d *= (dpi || 96) /.0254
    view.setResolution(view.getResolution()*fac/d);
    return fac;
  }
};

/*	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 = function (options)
{	options = options || {};
	ol.Object.call (this, options);
	// Options
	this.size_ = options.size||80000;
	this.origin_ = options.origin || [0,0];
	this.layout_ = this.layout[options.layout] || this.layout.pointy;
};
ol.ext.inherits(ol.HexGrid, ol.Object);
/** 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))
	]
};
/** Set layout
* @param {HexagonLayout | undefined} layout name, default pointy
*/
ol.HexGrid.prototype.setLayout = function (layout)
{	this.layout_ = this.layout[layout] || this.layout.pointy;
	this.changed();
}
/** Get layout
* @return {HexagonLayout} layout name
*/
ol.HexGrid.prototype.getLayout = function ()
{	return (this.layout_[9]!=0 ? 'pointy' : 'flat');
}
/** Set hexagon origin
* @param {ol.Coordinate} coord origin
*/
ol.HexGrid.prototype.setOrigin = function (coord)
{	this.origin_ = coord;
	this.changed();
}
/** Get hexagon origin
* @return {ol.Coordinate} coord origin
*/
ol.HexGrid.prototype.getOrigin = function ()
{	return this.origin_;
}
/** Set hexagon size
* @param {number} hexagon size
*/
ol.HexGrid.prototype.setSize = function (s) {
	this.size_ = s || 80000;
	this.changed();
}
/** Get hexagon size
* @return {number} hexagon size
*/
ol.HexGrid.prototype.getSize = function () {
	return this.size_;
}
/** Convert cube to axial coords
* @param {ol.Coordinate} c cube coordinate
* @return {ol.Coordinate} axial coordinate
*/
ol.HexGrid.prototype.cube2hex = function (c)
{	return [c[0], c[2]];
};
/** Convert axial to cube coords
* @param {ol.Coordinate} h axial coordinate
* @return {ol.Coordinate} cube coordinate
*/
ol.HexGrid.prototype.hex2cube = function(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
*/
ol.HexGrid.prototype.hex2offset = function (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
*/
ol.HexGrid.prototype.offset2hex = function(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
* /
ol.HexGrid.prototype.cube2offset = function(c)
{	return hex2offset(cube2hex(c));
};
/** Convert cube to offset coords
* @param {ol.Coordinate} o offset coordinate
* @return {ol.Coordinate} cube coordinate
* /
ol.HexGrid.prototype.offset2cube = function (o)
{	return hex2cube(offset2Hex(o));
};
/** Round cube coords
* @param {ol.Coordinate} h cube coordinate
* @return {ol.Coordinate} rounded cube coordinate
*/
ol.HexGrid.prototype.cube_round = function(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
*/
ol.HexGrid.prototype.hex_round = function(h)
{	return this.cube2hex( this.cube_round( this.hex2cube(h )) );
};
/** Get hexagon corners
*/
ol.HexGrid.prototype.hex_corner = function(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>}
*/
ol.HexGrid.prototype.getHexagonAtCoord = function (coord)
{	return (this.getHexagon(this.coord2hex(coord)));
};
/** Get hexagon coordinates at hex
* @param {ol.Coordinate} hex
* @return {Arrary<ol.Coordinate>}
*/
ol.HexGrid.prototype.getHexagon = function (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}
*/
ol.HexGrid.prototype.hex2coord = function (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}
*/
ol.HexGrid.prototype.coord2hex = function (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
*/
ol.HexGrid.prototype.cube_distance = function (a, b)
{	//return ( (Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2])) / 2 );
	return ( Math.max (Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1]), Math.abs(a[2] - b[2])) );
};
(function(){
/** Line interpolation
*/
function lerp(a, b, t)
{	// for floats
    return a + (b - a) * t;
}
function cube_lerp(a, b, t)
{	// for hexes
    return [ 
		lerp (a[0]+1e-6, b[0], t), 
		lerp (a[1]+1e-6, b[1], t),
		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
*/
ol.HexGrid.prototype.cube_line = function (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 ( cube_lerp(a, b, i/d) ) );
	}
    return results;
};
})();
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] ]
};
/** 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
*/
ol.HexGrid.prototype.hex_neighbors = function (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
*/
ol.HexGrid.prototype.cube_neighbors = function (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;
	}
};

/*	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 = function (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);
  }
  ol.Object.call (this, options);
  // Options
  var size = Math.max(200, Math.round((options.size||0)/200) * 200);
  this.set('size', size);
};
ol.ext.inherits (ol.InseeGrid, ol.Object);
/** Grid extent (in EPSG:3035)
 */
ol.InseeGrid.extent = [3200000,2000000,4300000,3140000];
/** Get the grid extent
 * @param {ol.proj.ProjLike} [proj='EPSG:3857']
 */
ol.InseeGrid.prototype.getExtent = function (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']
 */
ol.InseeGrid.prototype.getGridAtCoordinate = function (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;
};

/*	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 
*/
/**
 * @requires ol.style.Circle
 * @requires ol.structs.IHasChecksum
 */
/**
 * @classdesc
 * Set chart style for vector features.
 *
 * @constructor
 * @param {} options
 *	@param {String} options.type Chart type: pie,pie3D, donut or bar
 *	@param {number} options.radius Chart radius/size, default 20
 *	@param {number} options.rotation Rotation in radians (positive rotation clockwise). Default is 0.
 *	@param {bool} options.snapToPixel use integral numbers of pixels, default true
 *	@param {_ol_style_Stroke_} options.stroke stroke style
 *	@param {String|Array<ol.color>} options.colors predefined color set "classic","dark","pale","pastel","neon" / array of color string, default classic
 *	@param {Array<number>} options.displacement
 *	@param {number} options.offsetX X offset in px (deprecated, use displacement)
 *	@param {number} options.offsetY Y offset in px (deprecated, use displacement)
 *	@param {number} options.animation step in an animation sequence [0,1]
 *	@param {number} options.max maximum value for bar chart
 * @see [Statistic charts example](../../examples/style/map.style.chart.html)
 * @extends {ol.style.RegularShape}
 * @implements {ol.structs.IHasChecksum}
 * @api
 */
ol.style.Chart = function(opt_options) {
  var options = opt_options || {};
  var strokeWidth = 0;
  if (opt_options.stroke) strokeWidth = opt_options.stroke.getWidth();
  ol.style.RegularShape.call (this, {
    radius: options.radius + strokeWidth, 
    fill: new ol.style.Fill({color: [0,0,0]}),
    rotation: options.rotation,
    displacement: options.displacement,
    snapToPixel: options.snapToPixel
  });
  if (options.scale) this.setScale(options.scale);
  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_();
};
ol.ext.inherits(ol.style.Chart, ol.style.RegularShape);
/** 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"]
}
/**
 * Clones the style. 
 * @return {ol.style.Chart}
 */
ol.style.Chart.prototype.clone = function() {
  var s = new ol.style.Chart({
    type: this._type,
    radius: this._radius,
    rotation: this.getRotation(),
    scale: this.getScale(),
    data: this.getData(),
    snapToPixel: this.getSnapToPixel ? this.getSnapToPixel() : false,
    stroke: this._stroke,
    colors: this._colors,
    offsetX: this._offset[0],
    offsetY: this._offset[1],
    animation: this._animation
  });
  s.setScale(this.getScale());
  s.setOpacity(this.getOpacity());
  return s;
};
/** Get data associatied with the chart
*/
ol.style.Chart.prototype.getData = function() {
  return this._data;
}
/** Set data associatied with the chart
*	@param {Array<number>}
*/
ol.style.Chart.prototype.setData = function(data) {
  this._data = data;
  this.renderChart_();
}
/** Get symbol radius
*/
ol.style.Chart.prototype.getRadius = function() {
  return this._radius;
}
/** Set symbol radius
*	@param {number} symbol radius
*	@param {number} donut ratio
*/
ol.style.Chart.prototype.setRadius = function(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]
*/
ol.style.Chart.prototype.setAnimation = function(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
*/
ol.style.Chart.prototype.renderChart_ = function(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];
  }
};

/*	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 {olx.style.FillPatternOption=}  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 {olx.style.fillPattern} 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} 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.FillPattern = function(options) {
  if (!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');
      }
    }
  }
  ol.style.Fill.call (this, { color: pattern });
};
ol.ext.inherits(ol.style.FillPattern, ol.style.Fill);
/**
 * Clones the style. 
 * @return {ol.style.FillPattern}
 */
ol.style.FillPattern.prototype.clone = function() {
  var s = ol.style.Fill.prototype.clone.call(this);
  s.canvas_ = this.canvas_;
  return s;
};
/** Get canvas used as pattern
*	@return {canvas}
*/
ol.style.FillPattern.prototype.getImage = function() {
  return this.canvas_;
}
/** Get pattern
*	@param {olx.style.FillPatternOption}
*/
ol.style.FillPattern.prototype.getPattern_ = function(options) {
  var pat = ol.style.FillPattern.prototype.patterns[options.pattern]
    || ol.style.FillPattern.prototype.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
}
/** Static fuction to add char patterns
 * @param {title} 
 * @param {*} 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"]
 */
ol.style.FillPattern.addPattern = function (title, options) {
  if (!options) options={};
  ol.style.FillPattern.prototype.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
  }
}
/** Patterns definitions
 * @see pattern generator http://www.imagico.de/map/jsdotpattern.php
 */
ol.style.FillPattern.prototype.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 = function(options) {
  if (!options) options = {};
  ol.style.Style.call (this, { 
    renderer: this._render.bind(this),
    stroke: options.stroke,
    text: options.text,
    zIndex: options.zIndex,
    geometry: options.geometry
  });
  // 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;
};
ol.ext.inherits(ol.style.FlowLine, ol.style.Style);
/** Set the initial width
 * @param {number} width width, default 0
 */
ol.style.FlowLine.prototype.setWidth = function(width) {
  this._width = width || 0;
};
/** Set the final width
 * @param {number} width width, default 0
 */
ol.style.FlowLine.prototype.setWidth2 = function(width) {
  this._width2 = width;
};
/** Get offset at start or end
 * @param {number} where 0=start, 1=end
 * @return {number} width
 */
ol.style.FlowLine.prototype.getOffset = function(where) {
  return this._offset[where];
};
/** Add an offset at start or end
 * @param {number} width
 * @param {number} where 0=start, 1=end
 */
ol.style.FlowLine.prototype.setOffset = function(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
 */
ol.style.FlowLine.prototype.setLineCap = function(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} 
 */
ol.style.FlowLine.prototype.getWidth = function(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
 */
ol.style.FlowLine.prototype.setColor = function(color) {
  try{
    this._color = ol.color.asArray(color);
  } catch(e) {
    this._color = [0,0,0,1];
  }
};
/** Set the final color
 * @param {ol.colorLike} color
 */
ol.style.FlowLine.prototype.setColor2 = function(color) {
  try {
    this._color2 = ol.color.asArray(color);
  } catch(e) {
    this._color2 = null;    
  }
};
/** Set the arrow color
 * @param {ol.colorLike} color
 */
ol.style.FlowLine.prototype.setArrowColor = function(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} 
 */
ol.style.FlowLine.prototype.getColor = function(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
 */
ol.style.FlowLine.prototype.getArrow = function() {
  return this._arrow;
};
/** Set arrow
 * @param {number} n -1 | 0 | 1 | 2, default: 0
 */
ol.style.FlowLine.prototype.setArrow = function(n) {
  this._arrow = parseInt(n);
  if (this._arrow < -1 || this._arrow > 2) this._arrow = 0;
}
/** getArrowSize
 * @return {ol.size}
 */
ol.style.FlowLine.prototype.getArrowSize = function() {
  return this._arrowSize || [16,16];
};
/** setArrowSize
 * @param {number|ol.size} size
 */
ol.style.FlowLine.prototype.setArrowSize = function(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
 */
ol.style.FlowLine.prototype.drawArrow = function (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
 */
ol.style.FlowLine.prototype._render = function(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)
 */
ol.style.FlowLine.prototype._splitAsize = function(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
 */
ol.style.FlowLine.prototype._splitInto = function(geom, nb, min) {
  var i, p;
  var dt = this._noOverlap ? 1 : .9;
  // Split geom into equal length geoms
  var geoms = [];
  var dl, l = 0;
  for (i=1; p=geom[i]; i++) {
    l += ol.coordinate.dist2d(geom[i-1], p);
  }
  var length = Math.max (min||2, l/(nb||255));
  var p0 = geom[0];
  l = 0;
  var g = [p0];
  i = 1;
  p = geom[1];
  while (i < geom.length) {
    var dx = p[0]-p0[0];
    var dy = p[1]-p0[1];
    dl = Math.sqrt(dx*dx + dy*dy);
    if (l+dl > length) {
      var d = (length-l) / dl;
      g.push([ 
        p0[0] + dx * d,  
        p0[1] + dy * d 
      ]);
      geoms.push(g);
      p0 =[ 
        p0[0] + dx * d*dt,  
        p0[1] + dy * d*dt
      ];
      g = [p0];
      l = 0;
    } else {
      l += dl;
      p0 = p;
      g.push(p0);
      i++;
      p = geom[i];
    }
  }
  geoms.push(g);
  return geoms;
}

/*	Copyright (c) 2015 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/**
 * @classdesc
 * A marker style to use with font symbols.
 *
 * @constructor
 * @param {} options Options.
 *  @param {string} [options.color] default #000
 *  @param {string} options.glyph the glyph name or a char to display as symbol. 
 *    The name must be added using the {@link ol.style.FontSymbol.addDefs} function.
 *  @param {string} [options.text] a text to display as a glyph
 *  @param {string} [options.font] font to use with the text option
 *  @param {string} options.form 
 * 	  none|circle|poi|bubble|marker|coma|shield|blazon|bookmark|hexagon|diamond|triangle|sign|ban|lozenge|square
 * 	  a form that will enclose the glyph, default none
 *  @param {number} options.radius
 *  @param {number} options.rotation
 *  @param {boolean} options.rotateWithView
 *  @param {number} [options.opacity=1]
 *  @param {number} [options.fontSize=1] size of the font compare to the radius, fontSize greater than 1 will exceed the symbol extent
 *  @param {string} [options.fontStyle] the font style (bold, italic, bold italic, etc), default none
 *  @param {boolean} options.gradient true to display a gradient on the symbol
 *  @param {number} [options.offsetX=0] default 0
 *  @param {number} [options.offsetY=0] default 0
 *  @param {_ol_style_Fill_} options.fill
 *  @param {_ol_style_Stroke_} options.stroke
 * @extends {ol.style.RegularShape}
 * @implements {ol.structs.IHasChecksum}
 * @api
 */
ol.style.FontSymbol = function(options) {
  options = options || {};
  var strokeWidth = 0;
  if (options.stroke) {
    strokeWidth = options.stroke.getWidth();
  }
  ol.style.RegularShape.call (this, { 
    radius: options.radius, 
    fill:options.fill,
    rotation:options.rotation, 
    rotateWithView: options.rotateWithView 
  });
  if (typeof(options.opacity)=="number") this.setOpacity(options.opacity);
  this.color_ = options.color;
  this.fontSize_ = options.fontSize || 1;
  this.fontStyle_ = options.fontStyle || '';
  this.stroke_ = options.stroke;
  this.fill_ = options.fill;
  this.radius_ = options.radius -strokeWidth;
  this.form_ = options.form || "none";
  this.gradient_ = options.gradient;
  this.offset_ = [options.offsetX ? options.offsetX :0, options.offsetY ? options.offsetY :0];
  if (options.glyph) this.glyph_ = this.getGlyph(options.glyph);
  else this.glyph_ = this.getTextGlyph(options.text||'', options.font);
  this.renderMarker_();
};
ol.ext.inherits(ol.style.FontSymbol, ol.style.RegularShape);
/** 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;
  }
}
/** Font defs
 */
ol.style.FontSymbol.prototype.defs = { 'fonts':{}, 'glyphs':{} };
/** Static function : add new font defs 
 * @param {String|Object} font the font desciption
 * @param {} 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.
 */
ol.style.FontSymbol.addDefs = function(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.prototype.defs.fonts[fontname] = thefont;
  for (var i in glyphs) {
    var g = glyphs[i];
    if (typeof(g) === 'string' && g.length==1) g = { char: g };
    ol.style.FontSymbol.prototype.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}
 */
ol.style.FontSymbol.prototype.clone = function() {
  var g = new ol.style.FontSymbol({
    glyph: '',
    color: this.color_,
    fontSize: this.fontSize_,
    fontStyle: this.fontStyle_,
    stroke: this.stroke_,
    fill: this.fill_,
    radius: this.radius_ + (this.stroke_ ? this.stroke_.getWidth():0),
    form: this.form_,
    gradient: this.gradient_,
    offsetX: this.offset_[0],
    offsetY: this.offset_[1],
    opacity: this.getOpacity(),
    rotation: this.getRotation(),
    rotateWithView: this.getRotateWithView()
  });
  g.setScale(this.getScale());
  g.glyph_ = this.glyph_;
  g.renderMarker_();
  return g;
};
/** Get the fill style for the symbol.
 * @return {ol.style.Fill} Fill style.
 * @api
 */
ol.style.FontSymbol.prototype.getFill = function() {
  return this.fill_;
};
/** Get the stroke style for the symbol.
 * @return {_ol_style_Stroke_} Stroke style.
 * @api
 */
ol.style.FontSymbol.prototype.getStroke = function() {
  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
 */
ol.style.FontSymbol.prototype.getGlyph = function(name) {
  if (name) return ol.style.FontSymbol.prototype.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
 */
ol.style.FontSymbol.prototype.getTextGlyph = function(text, font) {
  return { font: font || 'sans-serif', char:String(text), theme:'none', name:'none', search:'' };
};
/**
 * Get the glyph name.
 * @return {string} the name
 * @api
 */
ol.style.FontSymbol.prototype.getGlyphName = function() {
  for (var i in ol.style.FontSymbol.prototype.defs.glyphs) {
    if (ol.style.FontSymbol.prototype.defs.glyphs[i] === this.glyph_) return i;
  }
  return '';
};
/**
 * Get the stroke style for the symbol.
 * @return {_ol_style_Stroke_} Stroke style.
 * @api
 */
ol.style.FontSymbol.prototype.getFontInfo = function(glyph) {
  return ol.style.FontSymbol.prototype.defs.fonts[glyph.font];
}
/** @private
 */
ol.style.FontSymbol.prototype.renderMarker_ = function(pixelratio) {
  if (!pixelratio) {
    if (this.getPixelRatio) {
      pixelratio = window.devicePixelRatio;
      this.renderMarker_(pixelratio);
      if (this.getPixelRatio && pixelratio!==1) this.renderMarker_(1); 
    } else {
      this.renderMarker_(1);
    }
    return;
  }
  var strokeStyle;
  var strokeWidth = 0;
  if (this.stroke_) {
    strokeStyle = ol.color.asString(this.stroke_.getColor());
    strokeWidth = this.stroke_.getWidth();
  }
  // get canvas
  var canvas = this.getImage(pixelratio);
  //console.log(this.getImage().width+" / "+(2 * (this.radius_ + strokeWidth) + 1));
  /** @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.setDisplacement) {
    this.setDisplacement([this.offset_[0], -this.offset_[1]])
  } else {
    var a = this.getAnchor();
    a[0] = canvas.width / 2 - this.offset_[0];
    a[1] = canvas.width / 2 - this.offset_[1];
  }
};
/**
 * @private
 * @param {ol.style.FontSymbol.RenderOptions} renderOptions
 * @param {CanvasRenderingContext2D} context
 */
ol.style.FontSymbol.prototype.drawPath_ = function(renderOptions, context) {
  var s = 2*this.radius_+renderOptions.strokeWidth+1;
  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.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).
 */
ol.style.FontSymbol.prototype.drawMarker_ = function(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
 */
ol.style.FontSymbol.prototype.getChecksum = function() {
  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];
};

/*	Copyright (c) 2015 Jean-Marc VIGLINO, 
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*
*  Photo style for vector features
*/
/**
 * @classdesc
 * Set Photo style for vector features.
 *
 * @constructor
 * @param {} options
 *  @param { default | square | circle | anchored | folio } options.kind
 *  @param {boolean} options.crop crop within square, default is false
 *  @param {Number} options.radius symbol size
 *  @param {boolean} options.shadow drop a shadow
 *  @param {ol.style.Stroke} options.stroke
 *  @param {String} options.src image src
 *  @param {String} options.crossOrigin The crossOrigin attribute for loaded images. Note that you must provide a crossOrigin value if you want to access pixel data with the Canvas renderer.
 *  @param {Number} options.offsetX Horizontal offset in pixels. Default is 0.
 *  @param {Number} options.offsetY Vertical offset in pixels. Default is 0.
 *  @param {function} options.onload callback when image is loaded (to redraw the layer)
 * @extends {ol.style.RegularShape}
 * @implements {ol.structs.IHasChecksum}
 * @api
 */
ol.style.Photo = function(options) {
  options = options || {};
  this.sanchor_ = options.kind=="anchored" ? 8:0;
  this._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);
  ol.style.RegularShape.call (this, {
    radius: options.radius + strokeWidth + this.sanchor_/2 + this._shadow/2, 
    points: 0,
    // 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
  });
  // Hack to get the hit detection Image (v4.6.5 ?)
  if (!this.getHitDetectionImage) {
    var img = this.getImage();
    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;
  if (typeof(options.opacity)=='number') this.setOpacity(options.opacity);
  if (typeof(options.rotation)=='number') this.setRotation(options.rotation);
  this.renderPhoto_();
};
ol.ext.inherits(ol.style.Photo, ol.style.RegularShape);
/** Set photo offset
 * @param {ol.pixel} offset
 */
ol.style.Photo.prototype.setOffset = function(offset) {
  this._offset = [offset[0]||0, offset[1]||0];
  this.renderPhoto_();
};
/**
 * Clones the style. 
 * @return {ol.style.Photo}
 */
ol.style.Photo.prototype.clone = function() {
  var i = new ol.style.Photo({
    stroke: this._stroke,
    fill: this._fill,
    shadow: this._shadow,
    crop: this._crop,
    crossOrigin: this._crossOrigin,
    kind: this._kind,
    radius: this._radius,
    src: this._src,
    offsetX: this._offset[0],
    offsetY: this._offset[1],
    opacity: this.getOpacity(),
    rotation: this.getRotation()
  });
  i.renderPhoto_();
  return i;
};
/**
 * 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;
};
/**
 * Draw the form without the image
 * @private
 */
ol.style.Photo.prototype.drawBack_ = function(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();
};
/**
 * @private
 */
ol.style.Photo.prototype.renderPhoto_ = function(pixelratio) {
  if (!pixelratio) {
    if (this.getPixelRatio) {
      pixelratio = window.devicePixelRatio;
      this.renderPhoto_(pixelratio);
    } else {
      this.renderPhoto_(1);
    }
    return;
  }
  var strokeStyle;
  var strokeWidth = 0;
  if (this._stroke) {
    strokeStyle = ol.color.asString(this._stroke.getColor());
    strokeWidth = this._stroke.getWidth();
  }
  var canvas = this.getImage(pixelratio);
  // Draw hitdetection image
  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();
  // 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_(img);
  } else {
    img.onload = function() {
      self.drawImage_(img);
      // Force change (?!)
      // self.setScale(1);
      if (self._onload) self._onload();
    };
  }
  // Set anchor
  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];
  }
};
/**
 * Draw an timage when loaded
 * @private
 */
ol.style.Photo.prototype.drawImage_ = function(img) {
  var pixelratio = window.devicePixelRatio;
  var canvas = this.getImage(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();
};

/*	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 = function(options) {
  if (!options) options = {};
  ol.style.Style.call (this, { 
    renderer: this._render.bind(this),
    zIndex: options.zIndex,
    geometry: options.geometry
  });
  this.setStroke(options.stroke);
  this.setFill(options.fill);
  this.setScale(options.scale);
};
ol.ext.inherits(ol.style.Profile, ol.style.Style);
/** Set style stroke
 * @param {ol.style.Stroke}
 */
ol.style.Profile.prototype.setStroke = function(stroke) {
  this._stroke = stroke || new ol.style.Stroke({ color: '#fff', width: 1 });
}
/** Get style stroke
 * @return {ol.style.Stroke}
 */
ol.style.Profile.prototype.getStroke = function() {
  return this._stroke;
}
/** Set style stroke
 * @param {ol.style.Fill}
 */
ol.style.Profile.prototype.setFill = function(fill) {
  this._fill = fill || new ol.style.Fill({ color: 'rgba(255,255,255,.3' });
}
/** Get style stroke
 * @return {ol.style.Fill}
 */
ol.style.Profile.prototype.getFill = function() {
  return this._fill;
}
/** Set z scale
 * @param {number}
 */
ol.style.Profile.prototype.setScale = function(sc) {
  this._scale = sc || .2;
}
/** Get z scale
 * @return {number}
 */
ol.style.Profile.prototype.getScale = function() {
  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
 */
ol.style.Profile.prototype._render = function(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 */
ol.style.Profile.prototype._renderLine = function(geom, g, l, e) {
  var i, p, ctx = e.context;
  var cos = Math.cos(e.rotation)
  var sin = Math.sin(e.rotation)
  // var a = e.pixelRatio / e.resolution;
  var a = ol.coordinate.dist2d(geom[0],geom[1]) / ol.coordinate.dist2d(g[0],g[1])
  var dx = geom[0][0] - g[0][0] * a *cos - g[0][1] * a *sin ;
  var dy = geom[0][1] - g[0][0] * a * sin + g[0][1] * a * cos;
  geom = l.getCoordinates();
  var dz = Infinity;
  for (i=0; p=geom[i]; i++) {
    var x = dx + p[0] * a * cos + p[1] * a * sin;
    var y = dy + p[0] * a * sin - p[1] * a * cos;
    dz = Math.min(dz, p[2]);
    geom[i] = [x, y, p[2]];
  }
  ctx.save();
    ctx.fillStyle = ol.color.asString(this.getFill().getColor());
    ctx.strokeStyle = ol.color.asString(this.getStroke().getColor());
    ctx.strokeWidth = 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.
*	@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);
	}
};
//NB: (Not confirmed)To use this module, you just have to :
//   import('ol-ext/layer/getpreview')
/*	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 {number} options.offsetX x offset, default 0
 * 	@param {number} options.offsetY y offset, default 0
 * @extends {ol.style.RegularShape}
 * @api
 */
ol.style.Shadow = function(options) {
  options = options || {};
  if (!options.fill) options.fill = new ol.style.Fill({ color: "rgba(0,0,0,0.5)" });
  ol.style.RegularShape.call (this,{ radius: options.radius, fill: options.fill });
  this._fill = options.fill;
  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];
  this.renderShadow_();
};
ol.ext.inherits(ol.style.Shadow, ol.style.RegularShape);
/**
 * Clones the style. 
 * @return {ol.style.Shadow}
 */
ol.style.Shadow.prototype.clone = function() {
  var s = new ol.style.Shadow({
    fill: this._fill,
    radius: this._radius,
    blur: this._blur,
    offsetX: this._offset[0],
    offsetY: this._offset[1]
  });
  s.setScale(this.getScale());
  s.setOpacity(this.getOpacity());
  return s;
};
/**
 * @private
 */
ol.style.Shadow.prototype.renderShadow_ = function(pixelratio) {	
  if (!pixelratio) {
    if (this.getPixelRatio) {
      pixelratio = window.devicePixelRatio;
      this.renderShadow_(pixelratio);
      if (this.getPixelRatio && pixelratio!==1) this.renderShadow_(1); 
    } else {
      this.renderShadow_(1);
    }
    return;
  }
  var radius = this._radius;
  var canvas = this.getImage(pixelratio);
  // Remove the circle on the canvas
  var context = (canvas.getContext('2d'));
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.save();
  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
  var a = this.getAnchor();
  a[0] = canvas.width /2 - this._offset[0];
  a[1] = canvas.height /2 - this._offset[1];
}

/*	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 {olx.style.fillPattern} 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} 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 = function(options)
{	if (!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');
			}
		}
	}
	options.color = pattern;
	ol.style.Stroke.call (this, options);
};
ol.ext.inherits(ol.style.StrokePattern, ol.style.Stroke);
/**
 * Clones the style. 
 * @return {ol.style.StrokePattern}
 */
ol.style.StrokePattern.prototype.clone = function() {
	var s = ol.style.Fill.prototype.clone.call(this);
	s.canvas_ = this.canvas_;
	return s;
};
/** Get canvas used as pattern
*	@return {canvas}
*/
ol.style.StrokePattern.prototype.getImage = function()
{	return this.canvas_;
}
/** Get pattern
*	@param {olx.style.FillPatternOption}
*/
ol.style.StrokePattern.prototype.getPattern_ = function(options)
{	var pat = ol.style.FillPattern.prototype.patterns[options.pattern]
		|| ol.style.FillPattern.prototype.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));
})();
