+ // @section
+ // @aka TileLayer options
+ options: {
+ // @option minZoom: Number = 0
+ // The minimum zoom level down to which this layer will be displayed (inclusive).
+ minZoom: 0,
+
+ // @option maxZoom: Number = 18
+ // The maximum zoom level up to which this layer will be displayed (inclusive).
+ maxZoom: 18,
+
+ // @option subdomains: String|String[] = 'abc'
+ // Subdomains of the tile service. Can be passed in the form of one string (where each letter is a subdomain name) or an array of strings.
+ subdomains: 'abc',
+
+ // @option errorTileUrl: String = ''
+ // URL to the tile image to show in place of the tile that failed to load.
+ errorTileUrl: '',
+
+ // @option zoomOffset: Number = 0
+ // The zoom number used in tile URLs will be offset with this value.
+ zoomOffset: 0,
+
+ // @option tms: Boolean = false
+ // If `true`, inverses Y axis numbering for tiles (turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services).
+ tms: false,
+
+ // @option zoomReverse: Boolean = false
+ // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
+ zoomReverse: false,
+
+ // @option detectRetina: Boolean = false
+ // If `true` and user is on a retina display, it will request four tiles of half the specified size and a bigger zoom level in place of one to utilize the high resolution.
+ detectRetina: false,
+
+ // @option crossOrigin: Boolean|String = false
+ // Whether the crossOrigin attribute will be added to the tiles.
+ // If a String is provided, all tiles will have their crossOrigin attribute set to the String provided. This is needed if you want to access tile pixel data.
+ // Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
+ crossOrigin: false
+ },
+
+ initialize: function (url, options) {
+
+ this._url = url;
+
+ options = setOptions(this, options);
+
+ // detecting retina displays, adjusting tileSize and zoom levels
+ if (options.detectRetina && retina && options.maxZoom > 0) {
+
+ options.tileSize = Math.floor(options.tileSize / 2);
+
+ if (!options.zoomReverse) {
+ options.zoomOffset++;
+ options.maxZoom--;
+ } else {
+ options.zoomOffset--;
+ options.minZoom++;
+ }
+
+ options.minZoom = Math.max(0, options.minZoom);
+ }
+
+ if (typeof options.subdomains === 'string') {
+ options.subdomains = options.subdomains.split('');
+ }
+
+ // for https://github.com/Leaflet/Leaflet/issues/137
+ if (!android) {
+ this.on('tileunload', this._onTileRemove);
+ }
+ },
+
+ // @method setUrl(url: String, noRedraw?: Boolean): this
+ // Updates the layer's URL template and redraws it (unless `noRedraw` is set to `true`).
+ // If the URL does not change, the layer will not be redrawn unless
+ // the noRedraw parameter is set to false.
+ setUrl: function (url, noRedraw) {
+ if (this._url === url && noRedraw === undefined) {
+ noRedraw = true;
+ }
+
+ this._url = url;
+
+ if (!noRedraw) {
+ this.redraw();
+ }
+ return this;
+ },
+
+ // @method createTile(coords: Object, done?: Function): HTMLElement
+ // Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
+ // to return an `<img>` HTML element with the appropriate image URL given `coords`. The `done`
+ // callback is called when the tile has been loaded.
+ createTile: function (coords, done) {
+ var tile = document.createElement('img');
+
+ on(tile, 'load', bind(this._tileOnLoad, this, done, tile));
+ on(tile, 'error', bind(this._tileOnError, this, done, tile));
+
+ if (this.options.crossOrigin || this.options.crossOrigin === '') {
+ tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
+ }
+
+ /*
+ Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons
+ http://www.w3.org/TR/WCAG20-TECHS/H67
+ */
+ tile.alt = '';
+
+ /*
+ Set role="presentation" to force screen readers to ignore this
+ https://www.w3.org/TR/wai-aria/roles#textalternativecomputation
+ */
+ tile.setAttribute('role', 'presentation');
+
+ tile.src = this.getTileUrl(coords);
+
+ return tile;
+ },
+
+ // @section Extension methods
+ // @uninheritable
+ // Layers extending `TileLayer` might reimplement the following method.
+ // @method getTileUrl(coords: Object): String
+ // Called only internally, returns the URL for a tile given its coordinates.
+ // Classes extending `TileLayer` can override this function to provide custom tile URL naming schemes.
+ getTileUrl: function (coords) {
+ var data = {
+ r: retina ? '@2x' : '',
+ s: this._getSubdomain(coords),
+ x: coords.x,
+ y: coords.y,
+ z: this._getZoomForUrl()
+ };
+ if (this._map && !this._map.options.crs.infinite) {
+ var invertedY = this._globalTileRange.max.y - coords.y;
+ if (this.options.tms) {
+ data['y'] = invertedY;
+ }
+ data['-y'] = invertedY;
+ }
+
+ return template(this._url, extend(data, this.options));
+ },
+
+ _tileOnLoad: function (done, tile) {
+ // For https://github.com/Leaflet/Leaflet/issues/3332
+ if (ielt9) {
+ setTimeout(bind(done, this, null, tile), 0);
+ } else {
+ done(null, tile);
+ }
+ },
+
+ _tileOnError: function (done, tile, e) {
+ var errorUrl = this.options.errorTileUrl;
+ if (errorUrl && tile.getAttribute('src') !== errorUrl) {
+ tile.src = errorUrl;
+ }
+ done(e, tile);
+ },
+
+ _onTileRemove: function (e) {
+ e.tile.onload = null;
+ },
+
+ _getZoomForUrl: function () {
+ var zoom = this._tileZoom,
+ maxZoom = this.options.maxZoom,
+ zoomReverse = this.options.zoomReverse,
+ zoomOffset = this.options.zoomOffset;
+
+ if (zoomReverse) {
+ zoom = maxZoom - zoom;
+ }
+
+ return zoom + zoomOffset;
+ },
+
+ _getSubdomain: function (tilePoint) {
+ var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length;
+ return this.options.subdomains[index];
+ },
+
+ // stops loading all tiles in the background layer
+ _abortLoading: function () {
+ var i, tile;
+ for (i in this._tiles) {
+ if (this._tiles[i].coords.z !== this._tileZoom) {
+ tile = this._tiles[i].el;
+
+ tile.onload = falseFn;
+ tile.onerror = falseFn;
+
+ if (!tile.complete) {
+ tile.src = emptyImageUrl;
+ remove(tile);
+ delete this._tiles[i];
+ }
+ }
+ }
+ },
+
+ _removeTile: function (key) {
+ var tile = this._tiles[key];
+ if (!tile) { return; }
+
+ // Cancels any pending http requests associated with the tile
+ // unless we're on Android's stock browser,
+ // see https://github.com/Leaflet/Leaflet/issues/137
+ if (!androidStock) {
+ tile.el.setAttribute('src', emptyImageUrl);
+ }
+
+ return GridLayer.prototype._removeTile.call(this, key);
+ },
+
+ _tileReady: function (coords, err, tile) {
+ if (!this._map || (tile && tile.getAttribute('src') === emptyImageUrl)) {
+ return;
+ }
+
+ return GridLayer.prototype._tileReady.call(this, coords, err, tile);
+ }
+});
+
+
+// @factory L.tilelayer(urlTemplate: String, options?: TileLayer options)
+// Instantiates a tile layer object given a `URL template` and optionally an options object.
+
+function tileLayer(url, options) {
+ return new TileLayer(url, options);
+}
+
+/*
+ * @class TileLayer.WMS
+ * @inherits TileLayer
+ * @aka L.TileLayer.WMS
+ * Used to display [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services as tile layers on the map. Extends `TileLayer`.
+ *
+ * @example
+ *
+ * ```js
+ * var nexrad = L.tileLayer.wms("http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", {
+ * layers: 'nexrad-n0r-900913',
+ * format: 'image/png',
+ * transparent: true,
+ * attribution: "Weather data © 2012 IEM Nexrad"
+ * });
+ * ```
+ */
+
+var TileLayerWMS = TileLayer.extend({
+
+ // @section
+ // @aka TileLayer.WMS options
+ // If any custom options not documented here are used, they will be sent to the
+ // WMS server as extra parameters in each request URL. This can be useful for
+ // [non-standard vendor WMS parameters](http://docs.geoserver.org/stable/en/user/services/wms/vendor.html).
+ defaultWmsParams: {
+ service: 'WMS',
+ request: 'GetMap',
+
+ // @option layers: String = ''
+ // **(required)** Comma-separated list of WMS layers to show.
+ layers: '',
+
+ // @option styles: String = ''
+ // Comma-separated list of WMS styles.
+ styles: '',
+
+ // @option format: String = 'image/jpeg'
+ // WMS image format (use `'image/png'` for layers with transparency).
+ format: 'image/jpeg',
+
+ // @option transparent: Boolean = false
+ // If `true`, the WMS service will return images with transparency.
+ transparent: false,
+
+ // @option version: String = '1.1.1'
+ // Version of the WMS service to use
+ version: '1.1.1'
+ },
+
+ options: {
+ // @option crs: CRS = null
+ // Coordinate Reference System to use for the WMS requests, defaults to
+ // map CRS. Don't change this if you're not sure what it means.
+ crs: null,
+
+ // @option uppercase: Boolean = false
+ // If `true`, WMS request parameter keys will be uppercase.
+ uppercase: false
+ },
+
+ initialize: function (url, options) {
+
+ this._url = url;
+
+ var wmsParams = extend({}, this.defaultWmsParams);
+
+ // all keys that are not TileLayer options go to WMS params
+ for (var i in options) {
+ if (!(i in this.options)) {
+ wmsParams[i] = options[i];
+ }
+ }
+
+ options = setOptions(this, options);
+
+ var realRetina = options.detectRetina && retina ? 2 : 1;
+ var tileSize = this.getTileSize();
+ wmsParams.width = tileSize.x * realRetina;
+ wmsParams.height = tileSize.y * realRetina;
+
+ this.wmsParams = wmsParams;
+ },
+
+ onAdd: function (map) {
+
+ this._crs = this.options.crs || map.options.crs;
+ this._wmsVersion = parseFloat(this.wmsParams.version);
+
+ var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
+ this.wmsParams[projectionKey] = this._crs.code;
+
+ TileLayer.prototype.onAdd.call(this, map);
+ },
+
+ getTileUrl: function (coords) {
+
+ var tileBounds = this._tileCoordsToNwSe(coords),
+ crs = this._crs,
+ bounds = toBounds(crs.project(tileBounds[0]), crs.project(tileBounds[1])),
+ min = bounds.min,
+ max = bounds.max,
+ bbox = (this._wmsVersion >= 1.3 && this._crs === EPSG4326 ?
+ [min.y, min.x, max.y, max.x] :
+ [min.x, min.y, max.x, max.y]).join(','),
+ url = TileLayer.prototype.getTileUrl.call(this, coords);
+ return url +
+ getParamString(this.wmsParams, url, this.options.uppercase) +
+ (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox;
+ },
+
+ // @method setParams(params: Object, noRedraw?: Boolean): this
+ // Merges an object with the new parameters and re-requests tiles on the current screen (unless `noRedraw` was set to true).
+ setParams: function (params, noRedraw) {
+
+ extend(this.wmsParams, params);
+
+ if (!noRedraw) {
+ this.redraw();
+ }
+
+ return this;
+ }
+});
+
+
+// @factory L.tileLayer.wms(baseUrl: String, options: TileLayer.WMS options)
+// Instantiates a WMS tile layer object given a base URL of the WMS service and a WMS parameters/options object.
+function tileLayerWMS(url, options) {
+ return new TileLayerWMS(url, options);
+}
+
+TileLayer.WMS = TileLayerWMS;
+tileLayer.wms = tileLayerWMS;
+
+/*
+ * @class Renderer
+ * @inherits Layer
+ * @aka L.Renderer
+ *
+ * Base class for vector renderer implementations (`SVG`, `Canvas`). Handles the
+ * DOM container of the renderer, its bounds, and its zoom animation.
+ *
+ * A `Renderer` works as an implicit layer group for all `Path`s - the renderer
+ * itself can be added or removed to the map. All paths use a renderer, which can
+ * be implicit (the map will decide the type of renderer and use it automatically)
+ * or explicit (using the [`renderer`](#path-renderer) option of the path).
+ *
+ * Do not use this class directly, use `SVG` and `Canvas` instead.
+ *
+ * @event update: Event
+ * Fired when the renderer updates its bounds, center and zoom, for example when
+ * its map has moved
+ */
+
+var Renderer = Layer.extend({
+
+ // @section
+ // @aka Renderer options
+ options: {
+ // @option padding: Number = 0.1
+ // How much to extend the clip area around the map view (relative to its size)
+ // e.g. 0.1 would be 10% of map view in each direction
+ padding: 0.1,
+
+ // @option tolerance: Number = 0
+ // How much to extend click tolerance round a path/object on the map
+ tolerance : 0
+ },
+
+ initialize: function (options) {
+ setOptions(this, options);
+ stamp(this);
+ this._layers = this._layers || {};
+ },
+
+ onAdd: function () {
+ if (!this._container) {
+ this._initContainer(); // defined by renderer implementations
+
+ if (this._zoomAnimated) {
+ addClass(this._container, 'leaflet-zoom-animated');
+ }
+ }
+
+ this.getPane().appendChild(this._container);
+ this._update();
+ this.on('update', this._updatePaths, this);
+ },
+
+ onRemove: function () {
+ this.off('update', this._updatePaths, this);
+ this._destroyContainer();
+ },
+
+ getEvents: function () {
+ var events = {
+ viewreset: this._reset,
+ zoom: this._onZoom,
+ moveend: this._update,
+ zoomend: this._onZoomEnd
+ };
+ if (this._zoomAnimated) {
+ events.zoomanim = this._onAnimZoom;
+ }
+ return events;
+ },
+
+ _onAnimZoom: function (ev) {
+ this._updateTransform(ev.center, ev.zoom);
+ },
+
+ _onZoom: function () {
+ this._updateTransform(this._map.getCenter(), this._map.getZoom());
+ },
+
+ _updateTransform: function (center, zoom) {
+ var scale = this._map.getZoomScale(zoom, this._zoom),
+ position = getPosition(this._container),
+ viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
+ currentCenterPoint = this._map.project(this._center, zoom),
+ destCenterPoint = this._map.project(center, zoom),
+ centerOffset = destCenterPoint.subtract(currentCenterPoint),
+
+ topLeftOffset = viewHalf.multiplyBy(-scale).add(position).add(viewHalf).subtract(centerOffset);
+
+ if (any3d) {
+ setTransform(this._container, topLeftOffset, scale);
+ } else {
+ setPosition(this._container, topLeftOffset);
+ }
+ },
+
+ _reset: function () {
+ this._update();
+ this._updateTransform(this._center, this._zoom);
+
+ for (var id in this._layers) {
+ this._layers[id]._reset();
+ }
+ },
+
+ _onZoomEnd: function () {
+ for (var id in this._layers) {
+ this._layers[id]._project();
+ }
+ },
+
+ _updatePaths: function () {
+ for (var id in this._layers) {
+ this._layers[id]._update();
+ }
+ },
+
+ _update: function () {
+ // Update pixel bounds of renderer container (for positioning/sizing/clipping later)
+ // Subclasses are responsible of firing the 'update' event.
+ var p = this.options.padding,
+ size = this._map.getSize(),
+ min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();
+
+ this._bounds = new Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());
+
+ this._center = this._map.getCenter();
+ this._zoom = this._map.getZoom();
+ }
+});
+
+/*
+ * @class Canvas
+ * @inherits Renderer
+ * @aka L.Canvas
+ *
+ * Allows vector layers to be displayed with [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
+ * Inherits `Renderer`.
+ *
+ * Due to [technical limitations](http://caniuse.com/#search=canvas), Canvas is not
+ * available in all web browsers, notably IE8, and overlapping geometries might
+ * not display properly in some edge cases.
+ *
+ * @example
+ *
+ * Use Canvas by default for all paths in the map:
+ *
+ * ```js
+ * var map = L.map('map', {
+ * renderer: L.canvas()
+ * });
+ * ```
+ *
+ * Use a Canvas renderer with extra padding for specific vector geometries:
+ *
+ * ```js
+ * var map = L.map('map');
+ * var myRenderer = L.canvas({ padding: 0.5 });
+ * var line = L.polyline( coordinates, { renderer: myRenderer } );
+ * var circle = L.circle( center, { renderer: myRenderer } );
+ * ```
+ */
+
+var Canvas = Renderer.extend({
+ getEvents: function () {
+ var events = Renderer.prototype.getEvents.call(this);
+ events.viewprereset = this._onViewPreReset;
+ return events;
+ },
+
+ _onViewPreReset: function () {
+ // Set a flag so that a viewprereset+moveend+viewreset only updates&redraws once
+ this._postponeUpdatePaths = true;
+ },
+
+ onAdd: function () {
+ Renderer.prototype.onAdd.call(this);
+
+ // Redraw vectors since canvas is cleared upon removal,
+ // in case of removing the renderer itself from the map.
+ this._draw();
+ },
+
+ _initContainer: function () {
+ var container = this._container = document.createElement('canvas');
+
+ on(container, 'mousemove', this._onMouseMove, this);
+ on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this);
+ on(container, 'mouseout', this._handleMouseOut, this);
+
+ this._ctx = container.getContext('2d');
+ },
+
+ _destroyContainer: function () {
+ cancelAnimFrame(this._redrawRequest);
+ delete this._ctx;
+ remove(this._container);
+ off(this._container);
+ delete this._container;
+ },
+
+ _updatePaths: function () {
+ if (this._postponeUpdatePaths) { return; }
+
+ var layer;
+ this._redrawBounds = null;
+ for (var id in this._layers) {
+ layer = this._layers[id];
+ layer._update();
+ }
+ this._redraw();
+ },
+
+ _update: function () {
+ if (this._map._animatingZoom && this._bounds) { return; }
+
+ Renderer.prototype._update.call(this);
+
+ var b = this._bounds,
+ container = this._container,
+ size = b.getSize(),
+ m = retina ? 2 : 1;
+
+ setPosition(container, b.min);
+
+ // set canvas size (also clearing it); use double size on retina
+ container.width = m * size.x;
+ container.height = m * size.y;
+ container.style.width = size.x + 'px';
+ container.style.height = size.y + 'px';
+
+ if (retina) {
+ this._ctx.scale(2, 2);
+ }
+
+ // translate so we use the same path coordinates after canvas element moves
+ this._ctx.translate(-b.min.x, -b.min.y);
+
+ // Tell paths to redraw themselves
+ this.fire('update');
+ },
+
+ _reset: function () {
+ Renderer.prototype._reset.call(this);
+
+ if (this._postponeUpdatePaths) {
+ this._postponeUpdatePaths = false;
+ this._updatePaths();
+ }
+ },
+
+ _initPath: function (layer) {
+ this._updateDashArray(layer);
+ this._layers[stamp(layer)] = layer;
+
+ var order = layer._order = {
+ layer: layer,
+ prev: this._drawLast,
+ next: null
+ };
+ if (this._drawLast) { this._drawLast.next = order; }
+ this._drawLast = order;
+ this._drawFirst = this._drawFirst || this._drawLast;
+ },
+
+ _addPath: function (layer) {
+ this._requestRedraw(layer);
+ },
+
+ _removePath: function (layer) {
+ var order = layer._order;
+ var next = order.next;
+ var prev = order.prev;
+
+ if (next) {
+ next.prev = prev;
+ } else {
+ this._drawLast = prev;
+ }
+ if (prev) {
+ prev.next = next;
+ } else {
+ this._drawFirst = next;
+ }
+
+ delete layer._order;
+
+ delete this._layers[stamp(layer)];
+
+ this._requestRedraw(layer);
+ },
+
+ _updatePath: function (layer) {
+ // Redraw the union of the layer's old pixel
+ // bounds and the new pixel bounds.
+ this._extendRedrawBounds(layer);
+ layer._project();
+ layer._update();
+ // The redraw will extend the redraw bounds
+ // with the new pixel bounds.
+ this._requestRedraw(layer);
+ },
+
+ _updateStyle: function (layer) {
+ this._updateDashArray(layer);
+ this._requestRedraw(layer);
+ },
+
+ _updateDashArray: function (layer) {
+ if (typeof layer.options.dashArray === 'string') {
+ var parts = layer.options.dashArray.split(/[, ]+/),
+ dashArray = [],
+ dashValue,
+ i;
+ for (i = 0; i < parts.length; i++) {
+ dashValue = Number(parts[i]);
+ // Ignore dash array containing invalid lengths
+ if (isNaN(dashValue)) { return; }
+ dashArray.push(dashValue);
+ }
+ layer.options._dashArray = dashArray;
+ } else {
+ layer.options._dashArray = layer.options.dashArray;
+ }
+ },
+
+ _requestRedraw: function (layer) {
+ if (!this._map) { return; }
+
+ this._extendRedrawBounds(layer);
+ this._redrawRequest = this._redrawRequest || requestAnimFrame(this._redraw, this);
+ },
+
+ _extendRedrawBounds: function (layer) {
+ if (layer._pxBounds) {
+ var padding = (layer.options.weight || 0) + 1;
+ this._redrawBounds = this._redrawBounds || new Bounds();
+ this._redrawBounds.extend(layer._pxBounds.min.subtract([padding, padding]));
+ this._redrawBounds.extend(layer._pxBounds.max.add([padding, padding]));
+ }
+ },
+
+ _redraw: function () {
+ this._redrawRequest = null;
+
+ if (this._redrawBounds) {
+ this._redrawBounds.min._floor();
+ this._redrawBounds.max._ceil();
+ }
+
+ this._clear(); // clear layers in redraw bounds
+ this._draw(); // draw layers
+
+ this._redrawBounds = null;
+ },
+
+ _clear: function () {
+ var bounds = this._redrawBounds;
+ if (bounds) {
+ var size = bounds.getSize();
+ this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y);
+ } else {
+ this._ctx.clearRect(0, 0, this._container.width, this._container.height);
+ }
+ },
+
+ _draw: function () {
+ var layer, bounds = this._redrawBounds;
+ this._ctx.save();
+ if (bounds) {
+ var size = bounds.getSize();
+ this._ctx.beginPath();
+ this._ctx.rect(bounds.min.x, bounds.min.y, size.x, size.y);
+ this._ctx.clip();
+ }
+
+ this._drawing = true;
+
+ for (var order = this._drawFirst; order; order = order.next) {
+ layer = order.layer;
+ if (!bounds || (layer._pxBounds && layer._pxBounds.intersects(bounds))) {
+ layer._updatePath();
+ }
+ }
+
+ this._drawing = false;
+
+ this._ctx.restore(); // Restore state before clipping.
+ },
+
+ _updatePoly: function (layer, closed) {
+ if (!this._drawing) { return; }
+
+ var i, j, len2, p,
+ parts = layer._parts,
+ len = parts.length,
+ ctx = this._ctx;
+
+ if (!len) { return; }
+
+ ctx.beginPath();
+
+ for (i = 0; i < len; i++) {
+ for (j = 0, len2 = parts[i].length; j < len2; j++) {
+ p = parts[i][j];
+ ctx[j ? 'lineTo' : 'moveTo'](p.x, p.y);
+ }
+ if (closed) {
+ ctx.closePath();
+ }
+ }
+
+ this._fillStroke(ctx, layer);
+
+ // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
+ },
+
+ _updateCircle: function (layer) {
+
+ if (!this._drawing || layer._empty()) { return; }
+
+ var p = layer._point,
+ ctx = this._ctx,
+ r = Math.max(Math.round(layer._radius), 1),
+ s = (Math.max(Math.round(layer._radiusY), 1) || r) / r;
+
+ if (s !== 1) {
+ ctx.save();
+ ctx.scale(1, s);
+ }
+
+ ctx.beginPath();
+ ctx.arc(p.x, p.y / s, r, 0, Math.PI * 2, false);
+
+ if (s !== 1) {
+ ctx.restore();
+ }
+
+ this._fillStroke(ctx, layer);
+ },
+
+ _fillStroke: function (ctx, layer) {
+ var options = layer.options;
+
+ if (options.fill) {
+ ctx.globalAlpha = options.fillOpacity;
+ ctx.fillStyle = options.fillColor || options.color;
+ ctx.fill(options.fillRule || 'evenodd');
+ }
+
+ if (options.stroke && options.weight !== 0) {
+ if (ctx.setLineDash) {
+ ctx.setLineDash(layer.options && layer.options._dashArray || []);
+ }
+ ctx.globalAlpha = options.opacity;
+ ctx.lineWidth = options.weight;
+ ctx.strokeStyle = options.color;
+ ctx.lineCap = options.lineCap;
+ ctx.lineJoin = options.lineJoin;
+ ctx.stroke();
+ }
+ },
+
+ // Canvas obviously doesn't have mouse events for individual drawn objects,
+ // so we emulate that by calculating what's under the mouse on mousemove/click manually
+
+ _onClick: function (e) {
+ var point = this._map.mouseEventToLayerPoint(e), layer, clickedLayer;
+
+ for (var order = this._drawFirst; order; order = order.next) {
+ layer = order.layer;
+ if (layer.options.interactive && layer._containsPoint(point) && !this._map._draggableMoved(layer)) {
+ clickedLayer = layer;
+ }
+ }
+ if (clickedLayer) {
+ fakeStop(e);
+ this._fireEvent([clickedLayer], e);
+ }
+ },
+
+ _onMouseMove: function (e) {
+ if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) { return; }
+
+ var point = this._map.mouseEventToLayerPoint(e);
+ this._handleMouseHover(e, point);
+ },
+
+
+ _handleMouseOut: function (e) {
+ var layer = this._hoveredLayer;
+ if (layer) {
+ // if we're leaving the layer, fire mouseout
+ removeClass(this._container, 'leaflet-interactive');
+ this._fireEvent([layer], e, 'mouseout');
+ this._hoveredLayer = null;
+ this._mouseHoverThrottled = false;
+ }
+ },
+
+ _handleMouseHover: function (e, point) {
+ if (this._mouseHoverThrottled) {
+ return;
+ }
+
+ var layer, candidateHoveredLayer;
+
+ for (var order = this._drawFirst; order; order = order.next) {
+ layer = order.layer;
+ if (layer.options.interactive && layer._containsPoint(point)) {
+ candidateHoveredLayer = layer;
+ }
+ }
+
+ if (candidateHoveredLayer !== this._hoveredLayer) {
+ this._handleMouseOut(e);
+
+ if (candidateHoveredLayer) {
+ addClass(this._container, 'leaflet-interactive'); // change cursor
+ this._fireEvent([candidateHoveredLayer], e, 'mouseover');
+ this._hoveredLayer = candidateHoveredLayer;
+ }
+ }
+
+ if (this._hoveredLayer) {
+ this._fireEvent([this._hoveredLayer], e);
+ }
+
+ this._mouseHoverThrottled = true;
+ setTimeout(L.bind(function () {
+ this._mouseHoverThrottled = false;
+ }, this), 32);
+ },
+
+ _fireEvent: function (layers, e, type) {
+ this._map._fireDOMEvent(e, type || e.type, layers);
+ },
+
+ _bringToFront: function (layer) {
+ var order = layer._order;
+
+ if (!order) { return; }
+
+ var next = order.next;
+ var prev = order.prev;
+
+ if (next) {
+ next.prev = prev;
+ } else {
+ // Already last
+ return;
+ }
+ if (prev) {
+ prev.next = next;
+ } else if (next) {
+ // Update first entry unless this is the
+ // single entry
+ this._drawFirst = next;
+ }
+
+ order.prev = this._drawLast;
+ this._drawLast.next = order;
+
+ order.next = null;
+ this._drawLast = order;
+
+ this._requestRedraw(layer);
+ },
+
+ _bringToBack: function (layer) {
+ var order = layer._order;
+
+ if (!order) { return; }
+
+ var next = order.next;
+ var prev = order.prev;
+
+ if (prev) {
+ prev.next = next;
+ } else {
+ // Already first
+ return;
+ }
+ if (next) {
+ next.prev = prev;
+ } else if (prev) {
+ // Update last entry unless this is the
+ // single entry
+ this._drawLast = prev;
+ }
+
+ order.prev = null;
+
+ order.next = this._drawFirst;
+ this._drawFirst.prev = order;
+ this._drawFirst = order;
+
+ this._requestRedraw(layer);
+ }
+});
+
+// @factory L.canvas(options?: Renderer options)
+// Creates a Canvas renderer with the given options.
+function canvas$1(options) {
+ return canvas ? new Canvas(options) : null;
+}
+
+/*
+ * Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
+ */
+
+
+var vmlCreate = (function () {
+ try {
+ document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml');
+ return function (name) {
+ return document.createElement('<lvml:' + name + ' class="lvml">');
+ };
+ } catch (e) {
+ return function (name) {
+ return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">');
+ };
+ }
+})();
+
+
+/*
+ * @class SVG
+ *
+ *
+ * VML was deprecated in 2012, which means VML functionality exists only for backwards compatibility
+ * with old versions of Internet Explorer.
+ */
+
+// mixin to redefine some SVG methods to handle VML syntax which is similar but with some differences
+var vmlMixin = {
+
+ _initContainer: function () {
+ this._container = create$1('div', 'leaflet-vml-container');
+ },
+
+ _update: function () {
+ if (this._map._animatingZoom) { return; }
+ Renderer.prototype._update.call(this);
+ this.fire('update');
+ },
+
+ _initPath: function (layer) {
+ var container = layer._container = vmlCreate('shape');
+
+ addClass(container, 'leaflet-vml-shape ' + (this.options.className || ''));
+
+ container.coordsize = '1 1';
+
+ layer._path = vmlCreate('path');
+ container.appendChild(layer._path);
+
+ this._updateStyle(layer);
+ this._layers[stamp(layer)] = layer;
+ },
+
+ _addPath: function (layer) {
+ var container = layer._container;
+ this._container.appendChild(container);
+
+ if (layer.options.interactive) {
+ layer.addInteractiveTarget(container);
+ }
+ },
+
+ _removePath: function (layer) {
+ var container = layer._container;
+ remove(container);
+ layer.removeInteractiveTarget(container);
+ delete this._layers[stamp(layer)];
+ },
+
+ _updateStyle: function (layer) {
+ var stroke = layer._stroke,
+ fill = layer._fill,
+ options = layer.options,
+ container = layer._container;
+
+ container.stroked = !!options.stroke;
+ container.filled = !!options.fill;
+
+ if (options.stroke) {
+ if (!stroke) {
+ stroke = layer._stroke = vmlCreate('stroke');
+ }
+ container.appendChild(stroke);
+ stroke.weight = options.weight + 'px';
+ stroke.color = options.color;
+ stroke.opacity = options.opacity;
+
+ if (options.dashArray) {
+ stroke.dashStyle = isArray(options.dashArray) ?
+ options.dashArray.join(' ') :
+ options.dashArray.replace(/( *, *)/g, ' ');
+ } else {
+ stroke.dashStyle = '';
+ }
+ stroke.endcap = options.lineCap.replace('butt', 'flat');
+ stroke.joinstyle = options.lineJoin;
+
+ } else if (stroke) {
+ container.removeChild(stroke);
+ layer._stroke = null;
+ }
+
+ if (options.fill) {
+ if (!fill) {
+ fill = layer._fill = vmlCreate('fill');
+ }
+ container.appendChild(fill);
+ fill.color = options.fillColor || options.color;
+ fill.opacity = options.fillOpacity;
+
+ } else if (fill) {
+ container.removeChild(fill);
+ layer._fill = null;
+ }
+ },
+
+ _updateCircle: function (layer) {
+ var p = layer._point.round(),
+ r = Math.round(layer._radius),
+ r2 = Math.round(layer._radiusY || r);
+
+ this._setPath(layer, layer._empty() ? 'M0 0' :
+ 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r2 + ' 0,' + (65535 * 360));
+ },
+
+ _setPath: function (layer, path) {
+ layer._path.v = path;
+ },
+
+ _bringToFront: function (layer) {
+ toFront(layer._container);
+ },
+
+ _bringToBack: function (layer) {
+ toBack(layer._container);
+ }
+};
+
+var create$2 = vml ? vmlCreate : svgCreate;
+
+/*
+ * @class SVG
+ * @inherits Renderer
+ * @aka L.SVG
+ *
+ * Allows vector layers to be displayed with [SVG](https://developer.mozilla.org/docs/Web/SVG).
+ * Inherits `Renderer`.
+ *
+ * Due to [technical limitations](http://caniuse.com/#search=svg), SVG is not
+ * available in all web browsers, notably Android 2.x and 3.x.
+ *
+ * Although SVG is not available on IE7 and IE8, these browsers support
+ * [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language)
+ * (a now deprecated technology), and the SVG renderer will fall back to VML in
+ * this case.
+ *
+ * @example
+ *
+ * Use SVG by default for all paths in the map:
+ *
+ * ```js
+ * var map = L.map('map', {
+ * renderer: L.svg()
+ * });
+ * ```
+ *
+ * Use a SVG renderer with extra padding for specific vector geometries:
+ *
+ * ```js
+ * var map = L.map('map');
+ * var myRenderer = L.svg({ padding: 0.5 });
+ * var line = L.polyline( coordinates, { renderer: myRenderer } );
+ * var circle = L.circle( center, { renderer: myRenderer } );
+ * ```
+ */
+
+var SVG = Renderer.extend({
+
+ getEvents: function () {
+ var events = Renderer.prototype.getEvents.call(this);
+ events.zoomstart = this._onZoomStart;
+ return events;
+ },
+
+ _initContainer: function () {
+ this._container = create$2('svg');
+
+ // makes it possible to click through svg root; we'll reset it back in individual paths
+ this._container.setAttribute('pointer-events', 'none');
+
+ this._rootGroup = create$2('g');
+ this._container.appendChild(this._rootGroup);
+ },
+
+ _destroyContainer: function () {
+ remove(this._container);
+ off(this._container);
+ delete this._container;
+ delete this._rootGroup;
+ delete this._svgSize;
+ },
+
+ _onZoomStart: function () {
+ // Drag-then-pinch interactions might mess up the center and zoom.
+ // In this case, the easiest way to prevent this is re-do the renderer
+ // bounds and padding when the zooming starts.
+ this._update();
+ },
+
+ _update: function () {
+ if (this._map._animatingZoom && this._bounds) { return; }
+
+ Renderer.prototype._update.call(this);
+
+ var b = this._bounds,
+ size = b.getSize(),
+ container = this._container;
+
+ // set size of svg-container if changed
+ if (!this._svgSize || !this._svgSize.equals(size)) {
+ this._svgSize = size;
+ container.setAttribute('width', size.x);
+ container.setAttribute('height', size.y);
+ }
+
+ // movement: update container viewBox so that we don't have to change coordinates of individual layers
+ setPosition(container, b.min);
+ container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' '));
+
+ this.fire('update');
+ },
+
+ // methods below are called by vector layers implementations
+
+ _initPath: function (layer) {
+ var path = layer._path = create$2('path');
+
+ // @namespace Path
+ // @option className: String = null
+ // Custom class name set on an element. Only for SVG renderer.
+ if (layer.options.className) {
+ addClass(path, layer.options.className);
+ }
+
+ if (layer.options.interactive) {
+ addClass(path, 'leaflet-interactive');
+ }
+
+ this._updateStyle(layer);
+ this._layers[stamp(layer)] = layer;
+ },
+
+ _addPath: function (layer) {
+ if (!this._rootGroup) { this._initContainer(); }
+ this._rootGroup.appendChild(layer._path);
+ layer.addInteractiveTarget(layer._path);
+ },
+
+ _removePath: function (layer) {
+ remove(layer._path);
+ layer.removeInteractiveTarget(layer._path);
+ delete this._layers[stamp(layer)];
+ },
+
+ _updatePath: function (layer) {
+ layer._project();
+ layer._update();
+ },
+
+ _updateStyle: function (layer) {
+ var path = layer._path,
+ options = layer.options;
+
+ if (!path) { return; }
+
+ if (options.stroke) {
+ path.setAttribute('stroke', options.color);
+ path.setAttribute('stroke-opacity', options.opacity);
+ path.setAttribute('stroke-width', options.weight);
+ path.setAttribute('stroke-linecap', options.lineCap);
+ path.setAttribute('stroke-linejoin', options.lineJoin);
+
+ if (options.dashArray) {
+ path.setAttribute('stroke-dasharray', options.dashArray);
+ } else {
+ path.removeAttribute('stroke-dasharray');
+ }
+
+ if (options.dashOffset) {
+ path.setAttribute('stroke-dashoffset', options.dashOffset);
+ } else {
+ path.removeAttribute('stroke-dashoffset');
+ }
+ } else {
+ path.setAttribute('stroke', 'none');
+ }
+
+ if (options.fill) {
+ path.setAttribute('fill', options.fillColor || options.color);
+ path.setAttribute('fill-opacity', options.fillOpacity);
+ path.setAttribute('fill-rule', options.fillRule || 'evenodd');
+ } else {
+ path.setAttribute('fill', 'none');
+ }
+ },
+
+ _updatePoly: function (layer, closed) {
+ this._setPath(layer, pointsToPath(layer._parts, closed));
+ },
+
+ _updateCircle: function (layer) {
+ var p = layer._point,
+ r = Math.max(Math.round(layer._radius), 1),
+ r2 = Math.max(Math.round(layer._radiusY), 1) || r,
+ arc = 'a' + r + ',' + r2 + ' 0 1,0 ';
+
+ // drawing a circle with two half-arcs
+ var d = layer._empty() ? 'M0 0' :
+ 'M' + (p.x - r) + ',' + p.y +
+ arc + (r * 2) + ',0 ' +
+ arc + (-r * 2) + ',0 ';
+
+ this._setPath(layer, d);
+ },
+
+ _setPath: function (layer, path) {
+ layer._path.setAttribute('d', path);
+ },
+
+ // SVG does not have the concept of zIndex so we resort to changing the DOM order of elements
+ _bringToFront: function (layer) {
+ toFront(layer._path);
+ },
+
+ _bringToBack: function (layer) {
+ toBack(layer._path);
+ }
+});
+
+if (vml) {
+ SVG.include(vmlMixin);
+}
+
+// @namespace SVG
+// @factory L.svg(options?: Renderer options)
+// Creates a SVG renderer with the given options.
+function svg$1(options) {
+ return svg || vml ? new SVG(options) : null;
+}
+
+Map.include({
+ // @namespace Map; @method getRenderer(layer: Path): Renderer
+ // Returns the instance of `Renderer` that should be used to render the given
+ // `Path`. It will ensure that the `renderer` options of the map and paths
+ // are respected, and that the renderers do exist on the map.
+ getRenderer: function (layer) {
+ // @namespace Path; @option renderer: Renderer
+ // Use this specific instance of `Renderer` for this path. Takes
+ // precedence over the map's [default renderer](#map-renderer).
+ var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
+
+ if (!renderer) {
+ renderer = this._renderer = this._createRenderer();
+ }
+
+ if (!this.hasLayer(renderer)) {
+ this.addLayer(renderer);
+ }
+ return renderer;
+ },
+
+ _getPaneRenderer: function (name) {
+ if (name === 'overlayPane' || name === undefined) {
+ return false;
+ }
+
+ var renderer = this._paneRenderers[name];
+ if (renderer === undefined) {
+ renderer = this._createRenderer({pane: name});
+ this._paneRenderers[name] = renderer;
+ }
+ return renderer;
+ },
+
+ _createRenderer: function (options) {
+ // @namespace Map; @option preferCanvas: Boolean = false
+ // Whether `Path`s should be rendered on a `Canvas` renderer.
+ // By default, all `Path`s are rendered in a `SVG` renderer.
+ return (this.options.preferCanvas && canvas$1(options)) || svg$1(options);
+ }
+});
+
+/*
+ * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object.
+ */
+
+/*
+ * @class Rectangle
+ * @aka L.Rectangle
+ * @inherits Polygon
+ *
+ * A class for drawing rectangle overlays on a map. Extends `Polygon`.
+ *
+ * @example
+ *
+ * ```js
+ * // define rectangle geographical bounds
+ * var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
+ *
+ * // create an orange rectangle
+ * L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(map);
+ *
+ * // zoom the map to the rectangle bounds
+ * map.fitBounds(bounds);
+ * ```
+ *
+ */
+
+
+var Rectangle = Polygon.extend({
+ initialize: function (latLngBounds, options) {
+ Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
+ },
+
+ // @method setBounds(latLngBounds: LatLngBounds): this
+ // Redraws the rectangle with the passed bounds.
+ setBounds: function (latLngBounds) {
+ return this.setLatLngs(this._boundsToLatLngs(latLngBounds));
+ },
+
+ _boundsToLatLngs: function (latLngBounds) {
+ latLngBounds = toLatLngBounds(latLngBounds);
+ return [
+ latLngBounds.getSouthWest(),
+ latLngBounds.getNorthWest(),
+ latLngBounds.getNorthEast(),
+ latLngBounds.getSouthEast()
+ ];
+ }
+});
+
+
+// @factory L.rectangle(latLngBounds: LatLngBounds, options?: Polyline options)
+function rectangle(latLngBounds, options) {
+ return new Rectangle(latLngBounds, options);
+}
+
+SVG.create = create$2;
+SVG.pointsToPath = pointsToPath;
+
+GeoJSON.geometryToLayer = geometryToLayer;
+GeoJSON.coordsToLatLng = coordsToLatLng;
+GeoJSON.coordsToLatLngs = coordsToLatLngs;
+GeoJSON.latLngToCoords = latLngToCoords;
+GeoJSON.latLngsToCoords = latLngsToCoords;
+GeoJSON.getFeature = getFeature;
+GeoJSON.asFeature = asFeature;
+
+/*
+ * L.Handler.BoxZoom is used to add shift-drag zoom interaction to the map
+ * (zoom to a selected bounding box), enabled by default.
+ */
+
+// @namespace Map
+// @section Interaction Options
+Map.mergeOptions({
+ // @option boxZoom: Boolean = true
+ // Whether the map can be zoomed to a rectangular area specified by
+ // dragging the mouse while pressing the shift key.
+ boxZoom: true
+});
+
+var BoxZoom = Handler.extend({
+ initialize: function (map) {
+ this._map = map;
+ this._container = map._container;
+ this._pane = map._panes.overlayPane;
+ this._resetStateTimeout = 0;
+ map.on('unload', this._destroy, this);
+ },
+
+ addHooks: function () {
+ on(this._container, 'mousedown', this._onMouseDown, this);
+ },
+
+ removeHooks: function () {
+ off(this._container, 'mousedown', this._onMouseDown, this);
+ },
+
+ moved: function () {
+ return this._moved;
+ },
+
+ _destroy: function () {
+ remove(this._pane);
+ delete this._pane;
+ },
+
+ _resetState: function () {
+ this._resetStateTimeout = 0;
+ this._moved = false;
+ },
+
+ _clearDeferredResetState: function () {
+ if (this._resetStateTimeout !== 0) {
+ clearTimeout(this._resetStateTimeout);
+ this._resetStateTimeout = 0;
+ }
+ },
+
+ _onMouseDown: function (e) {
+ if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
+
+ // Clear the deferred resetState if it hasn't executed yet, otherwise it
+ // will interrupt the interaction and orphan a box element in the container.
+ this._clearDeferredResetState();
+ this._resetState();
+
+ disableTextSelection();
+ disableImageDrag();
+
+ this._startPoint = this._map.mouseEventToContainerPoint(e);
+
+ on(document, {
+ contextmenu: stop,
+ mousemove: this._onMouseMove,
+ mouseup: this._onMouseUp,
+ keydown: this._onKeyDown
+ }, this);
+ },
+
+ _onMouseMove: function (e) {
+ if (!this._moved) {
+ this._moved = true;
+
+ this._box = create$1('div', 'leaflet-zoom-box', this._container);
+ addClass(this._container, 'leaflet-crosshair');
+
+ this._map.fire('boxzoomstart');
+ }
+
+ this._point = this._map.mouseEventToContainerPoint(e);
+
+ var bounds = new Bounds(this._point, this._startPoint),
+ size = bounds.getSize();
+
+ setPosition(this._box, bounds.min);
+
+ this._box.style.width = size.x + 'px';
+ this._box.style.height = size.y + 'px';
+ },
+
+ _finish: function () {
+ if (this._moved) {
+ remove(this._box);
+ removeClass(this._container, 'leaflet-crosshair');
+ }
+
+ enableTextSelection();
+ enableImageDrag();
+
+ off(document, {
+ contextmenu: stop,
+ mousemove: this._onMouseMove,
+ mouseup: this._onMouseUp,
+ keydown: this._onKeyDown
+ }, this);
+ },
+
+ _onMouseUp: function (e) {
+ if ((e.which !== 1) && (e.button !== 1)) { return; }
+
+ this._finish();
+
+ if (!this._moved) { return; }
+ // Postpone to next JS tick so internal click event handling
+ // still see it as "moved".
+ this._clearDeferredResetState();
+ this._resetStateTimeout = setTimeout(bind(this._resetState, this), 0);
+
+ var bounds = new LatLngBounds(
+ this._map.containerPointToLatLng(this._startPoint),
+ this._map.containerPointToLatLng(this._point));
+
+ this._map
+ .fitBounds(bounds)
+ .fire('boxzoomend', {boxZoomBounds: bounds});
+ },
+
+ _onKeyDown: function (e) {
+ if (e.keyCode === 27) {
+ this._finish();
+ }
+ }
+});