+ if (!this._container) {
+ this._initLayout();
+ }
+
+ if (map._fadeAnimated) {
+ setOpacity(this._container, 0);
+ }
+
+ clearTimeout(this._removeTimeout);
+ this.getPane().appendChild(this._container);
+ this.update();
+
+ if (map._fadeAnimated) {
+ setOpacity(this._container, 1);
+ }
+
+ this.bringToFront();
+ },
+
+ onRemove: function (map) {
+ if (map._fadeAnimated) {
+ setOpacity(this._container, 0);
+ this._removeTimeout = setTimeout(bind(remove, undefined, this._container), 200);
+ } else {
+ remove(this._container);
+ }
+ },
+
+ // @namespace Popup
+ // @method getLatLng: LatLng
+ // Returns the geographical point of popup.
+ getLatLng: function () {
+ return this._latlng;
+ },
+
+ // @method setLatLng(latlng: LatLng): this
+ // Sets the geographical point where the popup will open.
+ setLatLng: function (latlng) {
+ this._latlng = toLatLng(latlng);
+ if (this._map) {
+ this._updatePosition();
+ this._adjustPan();
+ }
+ return this;
+ },
+
+ // @method getContent: String|HTMLElement
+ // Returns the content of the popup.
+ getContent: function () {
+ return this._content;
+ },
+
+ // @method setContent(htmlContent: String|HTMLElement|Function): this
+ // Sets the HTML content of the popup. If a function is passed the source layer will be passed to the function. The function should return a `String` or `HTMLElement` to be used in the popup.
+ setContent: function (content) {
+ this._content = content;
+ this.update();
+ return this;
+ },
+
+ // @method getElement: String|HTMLElement
+ // Alias for [getContent()](#popup-getcontent)
+ getElement: function () {
+ return this._container;
+ },
+
+ // @method update: null
+ // Updates the popup content, layout and position. Useful for updating the popup after something inside changed, e.g. image loaded.
+ update: function () {
+ if (!this._map) { return; }
+
+ this._container.style.visibility = 'hidden';
+
+ this._updateContent();
+ this._updateLayout();
+ this._updatePosition();
+
+ this._container.style.visibility = '';
+
+ this._adjustPan();
+ },
+
+ getEvents: function () {
+ var events = {
+ zoom: this._updatePosition,
+ viewreset: this._updatePosition
+ };
+
+ if (this._zoomAnimated) {
+ events.zoomanim = this._animateZoom;
+ }
+ return events;
+ },
+
+ // @method isOpen: Boolean
+ // Returns `true` when the popup is visible on the map.
+ isOpen: function () {
+ return !!this._map && this._map.hasLayer(this);
+ },
+
+ // @method bringToFront: this
+ // Brings this popup in front of other popups (in the same map pane).
+ bringToFront: function () {
+ if (this._map) {
+ toFront(this._container);
+ }
+ return this;
+ },
+
+ // @method bringToBack: this
+ // Brings this popup to the back of other popups (in the same map pane).
+ bringToBack: function () {
+ if (this._map) {
+ toBack(this._container);
+ }
+ return this;
+ },
+
+ _updateContent: function () {
+ if (!this._content) { return; }
+
+ var node = this._contentNode;
+ var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content;
+
+ if (typeof content === 'string') {
+ node.innerHTML = content;
+ } else {
+ while (node.hasChildNodes()) {
+ node.removeChild(node.firstChild);
+ }
+ node.appendChild(content);
+ }
+ this.fire('contentupdate');
+ },
+
+ _updatePosition: function () {
+ if (!this._map) { return; }
+
+ var pos = this._map.latLngToLayerPoint(this._latlng),
+ offset = toPoint(this.options.offset),
+ anchor = this._getAnchor();
+
+ if (this._zoomAnimated) {
+ setPosition(this._container, pos.add(anchor));
+ } else {
+ offset = offset.add(pos).add(anchor);
+ }
+
+ var bottom = this._containerBottom = -offset.y,
+ left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x;
+
+ // bottom position the popup in case the height of the popup changes (images loading etc)
+ this._container.style.bottom = bottom + 'px';
+ this._container.style.left = left + 'px';
+ },
+
+ _getAnchor: function () {
+ return [0, 0];
+ }
+
+});
+
+/*
+ * @class Popup
+ * @inherits DivOverlay
+ * @aka L.Popup
+ * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to
+ * open popups while making sure that only one popup is open at one time
+ * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want.
+ *
+ * @example
+ *
+ * If you want to just bind a popup to marker click and then open it, it's really easy:
+ *
+ * ```js
+ * marker.bindPopup(popupContent).openPopup();
+ * ```
+ * Path overlays like polylines also have a `bindPopup` method.
+ * Here's a more complicated way to open a popup on a map:
+ *
+ * ```js
+ * var popup = L.popup()
+ * .setLatLng(latlng)
+ * .setContent('<p>Hello world!<br />This is a nice popup.</p>')
+ * .openOn(map);
+ * ```
+ */
+
+
+// @namespace Popup
+var Popup = DivOverlay.extend({
+
+ // @section
+ // @aka Popup options
+ options: {
+ // @option maxWidth: Number = 300
+ // Max width of the popup, in pixels.
+ maxWidth: 300,
+
+ // @option minWidth: Number = 50
+ // Min width of the popup, in pixels.
+ minWidth: 50,
+
+ // @option maxHeight: Number = null
+ // If set, creates a scrollable container of the given height
+ // inside a popup if its content exceeds it.
+ maxHeight: null,
+
+ // @option autoPan: Boolean = true
+ // Set it to `false` if you don't want the map to do panning animation
+ // to fit the opened popup.
+ autoPan: true,
+
+ // @option autoPanPaddingTopLeft: Point = null
+ // The margin between the popup and the top left corner of the map
+ // view after autopanning was performed.
+ autoPanPaddingTopLeft: null,
+
+ // @option autoPanPaddingBottomRight: Point = null
+ // The margin between the popup and the bottom right corner of the map
+ // view after autopanning was performed.
+ autoPanPaddingBottomRight: null,
+
+ // @option autoPanPadding: Point = Point(5, 5)
+ // Equivalent of setting both top left and bottom right autopan padding to the same value.
+ autoPanPadding: [5, 5],
+
+ // @option keepInView: Boolean = false
+ // Set it to `true` if you want to prevent users from panning the popup
+ // off of the screen while it is open.
+ keepInView: false,
+
+ // @option closeButton: Boolean = true
+ // Controls the presence of a close button in the popup.
+ closeButton: true,
+
+ // @option autoClose: Boolean = true
+ // Set it to `false` if you want to override the default behavior of
+ // the popup closing when another popup is opened.
+ autoClose: true,
+
+ // @option closeOnEscapeKey: Boolean = true
+ // Set it to `false` if you want to override the default behavior of
+ // the ESC key for closing of the popup.
+ closeOnEscapeKey: true,
+
+ // @option closeOnClick: Boolean = *
+ // Set it if you want to override the default behavior of the popup closing when user clicks
+ // on the map. Defaults to the map's [`closePopupOnClick`](#map-closepopuponclick) option.
+
+ // @option className: String = ''
+ // A custom CSS class name to assign to the popup.
+ className: ''
+ },
+
+ // @namespace Popup
+ // @method openOn(map: Map): this
+ // Adds the popup to the map and closes the previous one. The same as `map.openPopup(popup)`.
+ openOn: function (map) {
+ map.openPopup(this);
+ return this;
+ },
+
+ onAdd: function (map) {
+ DivOverlay.prototype.onAdd.call(this, map);
+
+ // @namespace Map
+ // @section Popup events
+ // @event popupopen: PopupEvent
+ // Fired when a popup is opened in the map
+ map.fire('popupopen', {popup: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Popup events
+ // @event popupopen: PopupEvent
+ // Fired when a popup bound to this layer is opened
+ this._source.fire('popupopen', {popup: this}, true);
+ // For non-path layers, we toggle the popup when clicking
+ // again the layer, so prevent the map to reopen it.
+ if (!(this._source instanceof Path)) {
+ this._source.on('preclick', stopPropagation);
+ }
+ }
+ },
+
+ onRemove: function (map) {
+ DivOverlay.prototype.onRemove.call(this, map);
+
+ // @namespace Map
+ // @section Popup events
+ // @event popupclose: PopupEvent
+ // Fired when a popup in the map is closed
+ map.fire('popupclose', {popup: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Popup events
+ // @event popupclose: PopupEvent
+ // Fired when a popup bound to this layer is closed
+ this._source.fire('popupclose', {popup: this}, true);
+ if (!(this._source instanceof Path)) {
+ this._source.off('preclick', stopPropagation);
+ }
+ }
+ },
+
+ getEvents: function () {
+ var events = DivOverlay.prototype.getEvents.call(this);
+
+ if (this.options.closeOnClick !== undefined ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
+ events.preclick = this._close;
+ }
+
+ if (this.options.keepInView) {
+ events.moveend = this._adjustPan;
+ }
+
+ return events;
+ },
+
+ _close: function () {
+ if (this._map) {
+ this._map.closePopup(this);
+ }
+ },
+
+ _initLayout: function () {
+ var prefix = 'leaflet-popup',
+ container = this._container = create$1('div',
+ prefix + ' ' + (this.options.className || '') +
+ ' leaflet-zoom-animated');
+
+ var wrapper = this._wrapper = create$1('div', prefix + '-content-wrapper', container);
+ this._contentNode = create$1('div', prefix + '-content', wrapper);
+
+ disableClickPropagation(wrapper);
+ disableScrollPropagation(this._contentNode);
+ on(wrapper, 'contextmenu', stopPropagation);
+
+ this._tipContainer = create$1('div', prefix + '-tip-container', container);
+ this._tip = create$1('div', prefix + '-tip', this._tipContainer);
+
+ if (this.options.closeButton) {
+ var closeButton = this._closeButton = create$1('a', prefix + '-close-button', container);
+ closeButton.href = '#close';
+ closeButton.innerHTML = '×';
+
+ on(closeButton, 'click', this._onCloseButtonClick, this);
+ }
+ },
+
+ _updateLayout: function () {
+ var container = this._contentNode,
+ style = container.style;
+
+ style.width = '';
+ style.whiteSpace = 'nowrap';
+
+ var width = container.offsetWidth;
+ width = Math.min(width, this.options.maxWidth);
+ width = Math.max(width, this.options.minWidth);
+
+ style.width = (width + 1) + 'px';
+ style.whiteSpace = '';
+
+ style.height = '';
+
+ var height = container.offsetHeight,
+ maxHeight = this.options.maxHeight,
+ scrolledClass = 'leaflet-popup-scrolled';
+
+ if (maxHeight && height > maxHeight) {
+ style.height = maxHeight + 'px';
+ addClass(container, scrolledClass);
+ } else {
+ removeClass(container, scrolledClass);
+ }
+
+ this._containerWidth = this._container.offsetWidth;
+ },
+
+ _animateZoom: function (e) {
+ var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center),
+ anchor = this._getAnchor();
+ setPosition(this._container, pos.add(anchor));
+ },
+
+ _adjustPan: function () {
+ if (!this.options.autoPan) { return; }
+ if (this._map._panAnim) { this._map._panAnim.stop(); }
+
+ var map = this._map,
+ marginBottom = parseInt(getStyle(this._container, 'marginBottom'), 10) || 0,
+ containerHeight = this._container.offsetHeight + marginBottom,
+ containerWidth = this._containerWidth,
+ layerPos = new Point(this._containerLeft, -containerHeight - this._containerBottom);
+
+ layerPos._add(getPosition(this._container));
+
+ var containerPos = map.layerPointToContainerPoint(layerPos),
+ padding = toPoint(this.options.autoPanPadding),
+ paddingTL = toPoint(this.options.autoPanPaddingTopLeft || padding),
+ paddingBR = toPoint(this.options.autoPanPaddingBottomRight || padding),
+ size = map.getSize(),
+ dx = 0,
+ dy = 0;
+
+ if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
+ dx = containerPos.x + containerWidth - size.x + paddingBR.x;
+ }
+ if (containerPos.x - dx - paddingTL.x < 0) { // left
+ dx = containerPos.x - paddingTL.x;
+ }
+ if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
+ dy = containerPos.y + containerHeight - size.y + paddingBR.y;
+ }
+ if (containerPos.y - dy - paddingTL.y < 0) { // top
+ dy = containerPos.y - paddingTL.y;
+ }
+
+ // @namespace Map
+ // @section Popup events
+ // @event autopanstart: Event
+ // Fired when the map starts autopanning when opening a popup.
+ if (dx || dy) {
+ map
+ .fire('autopanstart')
+ .panBy([dx, dy]);
+ }
+ },
+
+ _onCloseButtonClick: function (e) {
+ this._close();
+ stop(e);
+ },
+
+ _getAnchor: function () {
+ // Where should we anchor the popup on the source layer?
+ return toPoint(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]);
+ }
+
+});
+
+// @namespace Popup
+// @factory L.popup(options?: Popup options, source?: Layer)
+// Instantiates a `Popup` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the popup with a reference to the Layer to which it refers.
+var popup = function (options, source) {
+ return new Popup(options, source);
+};
+
+
+/* @namespace Map
+ * @section Interaction Options
+ * @option closePopupOnClick: Boolean = true
+ * Set it to `false` if you don't want popups to close when user clicks the map.
+ */
+Map.mergeOptions({
+ closePopupOnClick: true
+});
+
+
+// @namespace Map
+// @section Methods for Layers and Controls
+Map.include({
+ // @method openPopup(popup: Popup): this
+ // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability).
+ // @alternative
+ // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this
+ // Creates a popup with the specified content and options and opens it in the given point on a map.
+ openPopup: function (popup, latlng, options) {
+ if (!(popup instanceof Popup)) {
+ popup = new Popup(options).setContent(popup);
+ }
+
+ if (latlng) {
+ popup.setLatLng(latlng);
+ }
+
+ if (this.hasLayer(popup)) {
+ return this;
+ }
+
+ if (this._popup && this._popup.options.autoClose) {
+ this.closePopup();
+ }
+
+ this._popup = popup;
+ return this.addLayer(popup);
+ },
+
+ // @method closePopup(popup?: Popup): this
+ // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
+ closePopup: function (popup) {
+ if (!popup || popup === this._popup) {
+ popup = this._popup;
+ this._popup = null;
+ }
+ if (popup) {
+ this.removeLayer(popup);
+ }
+ return this;
+ }
+});
+
+/*
+ * @namespace Layer
+ * @section Popup methods example
+ *
+ * All layers share a set of methods convenient for binding popups to it.
+ *
+ * ```js
+ * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map);
+ * layer.openPopup();
+ * layer.closePopup();
+ * ```
+ *
+ * Popups will also be automatically opened when the layer is clicked on and closed when the layer is removed from the map or another popup is opened.
+ */
+
+// @section Popup methods
+Layer.include({
+
+ // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
+ // Binds a popup to the layer with the passed `content` and sets up the
+ // necessary event listeners. If a `Function` is passed it will receive
+ // the layer as the first argument and should return a `String` or `HTMLElement`.
+ bindPopup: function (content, options) {
+
+ if (content instanceof Popup) {
+ setOptions(content, options);
+ this._popup = content;
+ content._source = this;
+ } else {
+ if (!this._popup || options) {
+ this._popup = new Popup(options, this);
+ }
+ this._popup.setContent(content);
+ }
+
+ if (!this._popupHandlersAdded) {
+ this.on({
+ click: this._openPopup,
+ keypress: this._onKeyPress,
+ remove: this.closePopup,
+ move: this._movePopup
+ });
+ this._popupHandlersAdded = true;
+ }
+
+ return this;
+ },
+
+ // @method unbindPopup(): this
+ // Removes the popup previously bound with `bindPopup`.
+ unbindPopup: function () {
+ if (this._popup) {
+ this.off({
+ click: this._openPopup,
+ keypress: this._onKeyPress,
+ remove: this.closePopup,
+ move: this._movePopup
+ });
+ this._popupHandlersAdded = false;
+ this._popup = null;
+ }
+ return this;
+ },
+
+ // @method openPopup(latlng?: LatLng): this
+ // Opens the bound popup at the specified `latlng` or at the default popup anchor if no `latlng` is passed.
+ openPopup: function (layer, latlng) {
+ if (!(layer instanceof Layer)) {
+ latlng = layer;
+ layer = this;
+ }
+
+ if (layer instanceof FeatureGroup) {
+ for (var id in this._layers) {
+ layer = this._layers[id];
+ break;
+ }
+ }
+
+ if (!latlng) {
+ latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
+ }
+
+ if (this._popup && this._map) {
+ // set popup source to this layer
+ this._popup._source = layer;
+
+ // update the popup (content, layout, ect...)
+ this._popup.update();
+
+ // open the popup on the map
+ this._map.openPopup(this._popup, latlng);
+ }
+
+ return this;
+ },
+
+ // @method closePopup(): this
+ // Closes the popup bound to this layer if it is open.
+ closePopup: function () {
+ if (this._popup) {
+ this._popup._close();
+ }
+ return this;
+ },
+
+ // @method togglePopup(): this
+ // Opens or closes the popup bound to this layer depending on its current state.
+ togglePopup: function (target) {
+ if (this._popup) {
+ if (this._popup._map) {
+ this.closePopup();
+ } else {
+ this.openPopup(target);
+ }
+ }
+ return this;
+ },
+
+ // @method isPopupOpen(): boolean
+ // Returns `true` if the popup bound to this layer is currently open.
+ isPopupOpen: function () {
+ return (this._popup ? this._popup.isOpen() : false);
+ },
+
+ // @method setPopupContent(content: String|HTMLElement|Popup): this
+ // Sets the content of the popup bound to this layer.
+ setPopupContent: function (content) {
+ if (this._popup) {
+ this._popup.setContent(content);
+ }
+ return this;
+ },
+
+ // @method getPopup(): Popup
+ // Returns the popup bound to this layer.
+ getPopup: function () {
+ return this._popup;
+ },
+
+ _openPopup: function (e) {
+ var layer = e.layer || e.target;
+
+ if (!this._popup) {
+ return;
+ }
+
+ if (!this._map) {
+ return;
+ }
+
+ // prevent map click
+ stop(e);
+
+ // if this inherits from Path its a vector and we can just
+ // open the popup at the new location
+ if (layer instanceof Path) {
+ this.openPopup(e.layer || e.target, e.latlng);
+ return;
+ }
+
+ // otherwise treat it like a marker and figure out
+ // if we should toggle it open/closed
+ if (this._map.hasLayer(this._popup) && this._popup._source === layer) {
+ this.closePopup();
+ } else {
+ this.openPopup(layer, e.latlng);
+ }
+ },
+
+ _movePopup: function (e) {
+ this._popup.setLatLng(e.latlng);
+ },
+
+ _onKeyPress: function (e) {
+ if (e.originalEvent.keyCode === 13) {
+ this._openPopup(e);
+ }
+ }
+});
+
+/*
+ * @class Tooltip
+ * @inherits DivOverlay
+ * @aka L.Tooltip
+ * Used to display small texts on top of map layers.
+ *
+ * @example
+ *
+ * ```js
+ * marker.bindTooltip("my tooltip text").openTooltip();
+ * ```
+ * Note about tooltip offset. Leaflet takes two options in consideration
+ * for computing tooltip offsetting:
+ * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip.
+ * Add a positive x offset to move the tooltip to the right, and a positive y offset to
+ * move it to the bottom. Negatives will move to the left and top.
+ * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You
+ * should adapt this value if you use a custom icon.
+ */
+
+
+// @namespace Tooltip
+var Tooltip = DivOverlay.extend({
+
+ // @section
+ // @aka Tooltip options
+ options: {
+ // @option pane: String = 'tooltipPane'
+ // `Map pane` where the tooltip will be added.
+ pane: 'tooltipPane',
+
+ // @option offset: Point = Point(0, 0)
+ // Optional offset of the tooltip position.
+ offset: [0, 0],
+
+ // @option direction: String = 'auto'
+ // Direction where to open the tooltip. Possible values are: `right`, `left`,
+ // `top`, `bottom`, `center`, `auto`.
+ // `auto` will dynamically switch between `right` and `left` according to the tooltip
+ // position on the map.
+ direction: 'auto',
+
+ // @option permanent: Boolean = false
+ // Whether to open the tooltip permanently or only on mouseover.
+ permanent: false,
+
+ // @option sticky: Boolean = false
+ // If true, the tooltip will follow the mouse instead of being fixed at the feature center.
+ sticky: false,
+
+ // @option interactive: Boolean = false
+ // If true, the tooltip will listen to the feature events.
+ interactive: false,
+
+ // @option opacity: Number = 0.9
+ // Tooltip container opacity.
+ opacity: 0.9
+ },
+
+ onAdd: function (map) {
+ DivOverlay.prototype.onAdd.call(this, map);
+ this.setOpacity(this.options.opacity);
+
+ // @namespace Map
+ // @section Tooltip events
+ // @event tooltipopen: TooltipEvent
+ // Fired when a tooltip is opened in the map.
+ map.fire('tooltipopen', {tooltip: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Tooltip events
+ // @event tooltipopen: TooltipEvent
+ // Fired when a tooltip bound to this layer is opened.
+ this._source.fire('tooltipopen', {tooltip: this}, true);
+ }
+ },
+
+ onRemove: function (map) {
+ DivOverlay.prototype.onRemove.call(this, map);
+
+ // @namespace Map
+ // @section Tooltip events
+ // @event tooltipclose: TooltipEvent
+ // Fired when a tooltip in the map is closed.
+ map.fire('tooltipclose', {tooltip: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Tooltip events
+ // @event tooltipclose: TooltipEvent
+ // Fired when a tooltip bound to this layer is closed.
+ this._source.fire('tooltipclose', {tooltip: this}, true);
+ }
+ },
+
+ getEvents: function () {
+ var events = DivOverlay.prototype.getEvents.call(this);
+
+ if (touch && !this.options.permanent) {
+ events.preclick = this._close;
+ }
+
+ return events;
+ },
+
+ _close: function () {
+ if (this._map) {
+ this._map.closeTooltip(this);
+ }
+ },
+
+ _initLayout: function () {
+ var prefix = 'leaflet-tooltip',
+ className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
+
+ this._contentNode = this._container = create$1('div', className);
+ },
+
+ _updateLayout: function () {},
+
+ _adjustPan: function () {},
+
+ _setPosition: function (pos) {
+ var map = this._map,
+ container = this._container,
+ centerPoint = map.latLngToContainerPoint(map.getCenter()),
+ tooltipPoint = map.layerPointToContainerPoint(pos),
+ direction = this.options.direction,
+ tooltipWidth = container.offsetWidth,
+ tooltipHeight = container.offsetHeight,
+ offset = toPoint(this.options.offset),
+ anchor = this._getAnchor();
+
+ if (direction === 'top') {
+ pos = pos.add(toPoint(-tooltipWidth / 2 + offset.x, -tooltipHeight + offset.y + anchor.y, true));
+ } else if (direction === 'bottom') {
+ pos = pos.subtract(toPoint(tooltipWidth / 2 - offset.x, -offset.y, true));
+ } else if (direction === 'center') {
+ pos = pos.subtract(toPoint(tooltipWidth / 2 + offset.x, tooltipHeight / 2 - anchor.y + offset.y, true));
+ } else if (direction === 'right' || direction === 'auto' && tooltipPoint.x < centerPoint.x) {
+ direction = 'right';
+ pos = pos.add(toPoint(offset.x + anchor.x, anchor.y - tooltipHeight / 2 + offset.y, true));
+ } else {
+ direction = 'left';
+ pos = pos.subtract(toPoint(tooltipWidth + anchor.x - offset.x, tooltipHeight / 2 - anchor.y - offset.y, true));
+ }
+
+ removeClass(container, 'leaflet-tooltip-right');
+ removeClass(container, 'leaflet-tooltip-left');
+ removeClass(container, 'leaflet-tooltip-top');
+ removeClass(container, 'leaflet-tooltip-bottom');
+ addClass(container, 'leaflet-tooltip-' + direction);
+ setPosition(container, pos);
+ },
+
+ _updatePosition: function () {
+ var pos = this._map.latLngToLayerPoint(this._latlng);
+ this._setPosition(pos);
+ },
+
+ setOpacity: function (opacity) {
+ this.options.opacity = opacity;
+
+ if (this._container) {
+ setOpacity(this._container, opacity);
+ }
+ },
+
+ _animateZoom: function (e) {
+ var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
+ this._setPosition(pos);
+ },
+
+ _getAnchor: function () {
+ // Where should we anchor the tooltip on the source layer?
+ return toPoint(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
+ }
+
+});
+
+// @namespace Tooltip
+// @factory L.tooltip(options?: Tooltip options, source?: Layer)
+// Instantiates a Tooltip object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the tooltip with a reference to the Layer to which it refers.
+var tooltip = function (options, source) {
+ return new Tooltip(options, source);
+};
+
+// @namespace Map
+// @section Methods for Layers and Controls
+Map.include({
+
+ // @method openTooltip(tooltip: Tooltip): this
+ // Opens the specified tooltip.
+ // @alternative
+ // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this
+ // Creates a tooltip with the specified content and options and open it.
+ openTooltip: function (tooltip, latlng, options) {
+ if (!(tooltip instanceof Tooltip)) {
+ tooltip = new Tooltip(options).setContent(tooltip);
+ }
+
+ if (latlng) {
+ tooltip.setLatLng(latlng);
+ }
+
+ if (this.hasLayer(tooltip)) {
+ return this;
+ }
+
+ return this.addLayer(tooltip);
+ },
+
+ // @method closeTooltip(tooltip?: Tooltip): this
+ // Closes the tooltip given as parameter.
+ closeTooltip: function (tooltip) {
+ if (tooltip) {
+ this.removeLayer(tooltip);
+ }
+ return this;
+ }
+
+});
+
+/*
+ * @namespace Layer
+ * @section Tooltip methods example
+ *
+ * All layers share a set of methods convenient for binding tooltips to it.
+ *
+ * ```js
+ * var layer = L.Polygon(latlngs).bindTooltip('Hi There!').addTo(map);
+ * layer.openTooltip();
+ * layer.closeTooltip();
+ * ```
+ */
+
+// @section Tooltip methods
+Layer.include({
+
+ // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this
+ // Binds a tooltip to the layer with the passed `content` and sets up the
+ // necessary event listeners. If a `Function` is passed it will receive
+ // the layer as the first argument and should return a `String` or `HTMLElement`.
+ bindTooltip: function (content, options) {
+
+ if (content instanceof Tooltip) {
+ setOptions(content, options);
+ this._tooltip = content;
+ content._source = this;
+ } else {
+ if (!this._tooltip || options) {
+ this._tooltip = new Tooltip(options, this);
+ }
+ this._tooltip.setContent(content);
+
+ }
+
+ this._initTooltipInteractions();
+
+ if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) {
+ this.openTooltip();
+ }
+
+ return this;
+ },
+
+ // @method unbindTooltip(): this
+ // Removes the tooltip previously bound with `bindTooltip`.
+ unbindTooltip: function () {
+ if (this._tooltip) {
+ this._initTooltipInteractions(true);
+ this.closeTooltip();
+ this._tooltip = null;
+ }
+ return this;
+ },
+
+ _initTooltipInteractions: function (remove$$1) {
+ if (!remove$$1 && this._tooltipHandlersAdded) { return; }
+ var onOff = remove$$1 ? 'off' : 'on',
+ events = {
+ remove: this.closeTooltip,
+ move: this._moveTooltip
+ };
+ if (!this._tooltip.options.permanent) {
+ events.mouseover = this._openTooltip;
+ events.mouseout = this.closeTooltip;
+ if (this._tooltip.options.sticky) {
+ events.mousemove = this._moveTooltip;
+ }
+ if (touch) {
+ events.click = this._openTooltip;
+ }
+ } else {
+ events.add = this._openTooltip;
+ }
+ this[onOff](events);
+ this._tooltipHandlersAdded = !remove$$1;
+ },
+
+ // @method openTooltip(latlng?: LatLng): this
+ // Opens the bound tooltip at the specified `latlng` or at the default tooltip anchor if no `latlng` is passed.
+ openTooltip: function (layer, latlng) {
+ if (!(layer instanceof Layer)) {
+ latlng = layer;
+ layer = this;
+ }
+
+ if (layer instanceof FeatureGroup) {
+ for (var id in this._layers) {
+ layer = this._layers[id];
+ break;
+ }
+ }
+
+ if (!latlng) {
+ latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
+ }
+
+ if (this._tooltip && this._map) {
+
+ // set tooltip source to this layer
+ this._tooltip._source = layer;
+
+ // update the tooltip (content, layout, ect...)
+ this._tooltip.update();
+
+ // open the tooltip on the map
+ this._map.openTooltip(this._tooltip, latlng);
+
+ // Tooltip container may not be defined if not permanent and never
+ // opened.
+ if (this._tooltip.options.interactive && this._tooltip._container) {
+ addClass(this._tooltip._container, 'leaflet-clickable');
+ this.addInteractiveTarget(this._tooltip._container);
+ }
+ }
+
+ return this;
+ },
+
+ // @method closeTooltip(): this
+ // Closes the tooltip bound to this layer if it is open.
+ closeTooltip: function () {
+ if (this._tooltip) {
+ this._tooltip._close();
+ if (this._tooltip.options.interactive && this._tooltip._container) {
+ removeClass(this._tooltip._container, 'leaflet-clickable');
+ this.removeInteractiveTarget(this._tooltip._container);
+ }
+ }
+ return this;
+ },
+
+ // @method toggleTooltip(): this
+ // Opens or closes the tooltip bound to this layer depending on its current state.
+ toggleTooltip: function (target) {
+ if (this._tooltip) {
+ if (this._tooltip._map) {
+ this.closeTooltip();
+ } else {
+ this.openTooltip(target);
+ }
+ }
+ return this;
+ },
+
+ // @method isTooltipOpen(): boolean
+ // Returns `true` if the tooltip bound to this layer is currently open.
+ isTooltipOpen: function () {
+ return this._tooltip.isOpen();
+ },
+
+ // @method setTooltipContent(content: String|HTMLElement|Tooltip): this
+ // Sets the content of the tooltip bound to this layer.
+ setTooltipContent: function (content) {
+ if (this._tooltip) {
+ this._tooltip.setContent(content);
+ }
+ return this;
+ },
+
+ // @method getTooltip(): Tooltip
+ // Returns the tooltip bound to this layer.
+ getTooltip: function () {
+ return this._tooltip;
+ },
+
+ _openTooltip: function (e) {
+ var layer = e.layer || e.target;
+
+ if (!this._tooltip || !this._map) {
+ return;
+ }
+ this.openTooltip(layer, this._tooltip.options.sticky ? e.latlng : undefined);
+ },
+
+ _moveTooltip: function (e) {
+ var latlng = e.latlng, containerPoint, layerPoint;
+ if (this._tooltip.options.sticky && e.originalEvent) {
+ containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent);
+ layerPoint = this._map.containerPointToLayerPoint(containerPoint);
+ latlng = this._map.layerPointToLatLng(layerPoint);
+ }
+ this._tooltip.setLatLng(latlng);
+ }
+});
+
+/*
+ * @class DivIcon
+ * @aka L.DivIcon
+ * @inherits Icon
+ *
+ * Represents a lightweight icon for markers that uses a simple `<div>`
+ * element instead of an image. Inherits from `Icon` but ignores the `iconUrl` and shadow options.
+ *
+ * @example
+ * ```js
+ * var myIcon = L.divIcon({className: 'my-div-icon'});
+ * // you can set .my-div-icon styles in CSS
+ *
+ * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
+ * ```
+ *
+ * By default, it has a 'leaflet-div-icon' CSS class and is styled as a little white square with a shadow.
+ */
+
+var DivIcon = Icon.extend({
+ options: {
+ // @section
+ // @aka DivIcon options
+ iconSize: [12, 12], // also can be set through CSS
+
+ // iconAnchor: (Point),
+ // popupAnchor: (Point),
+
+ // @option html: String = ''
+ // Custom HTML code to put inside the div element, empty by default.
+ html: false,
+
+ // @option bgPos: Point = [0, 0]
+ // Optional relative position of the background, in pixels
+ bgPos: null,
+
+ className: 'leaflet-div-icon'
+ },
+
+ createIcon: function (oldIcon) {
+ var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'),
+ options = this.options;
+
+ div.innerHTML = options.html !== false ? options.html : '';
+
+ if (options.bgPos) {
+ var bgPos = toPoint(options.bgPos);
+ div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px';
+ }
+ this._setIconStyles(div, 'icon');
+
+ return div;
+ },
+
+ createShadow: function () {
+ return null;
+ }
+});
+
+// @factory L.divIcon(options: DivIcon options)
+// Creates a `DivIcon` instance with the given options.
+function divIcon(options) {
+ return new DivIcon(options);
+}
+
+Icon.Default = IconDefault;
+
+/*
+ * @class GridLayer
+ * @inherits Layer
+ * @aka L.GridLayer
+ *
+ * Generic class for handling a tiled grid of HTML elements. This is the base class for all tile layers and replaces `TileLayer.Canvas`.
+ * GridLayer can be extended to create a tiled grid of HTML elements like `<canvas>`, `<img>` or `<div>`. GridLayer will handle creating and animating these DOM elements for you.
+ *
+ *
+ * @section Synchronous usage
+ * @example
+ *
+ * To create a custom layer, extend GridLayer and implement the `createTile()` method, which will be passed a `Point` object with the `x`, `y`, and `z` (zoom level) coordinates to draw your tile.
+ *
+ * ```js
+ * var CanvasLayer = L.GridLayer.extend({
+ * createTile: function(coords){
+ * // create a <canvas> element for drawing
+ * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
+ *
+ * // setup tile width and height according to the options
+ * var size = this.getTileSize();
+ * tile.width = size.x;
+ * tile.height = size.y;
+ *
+ * // get a canvas context and draw something on it using coords.x, coords.y and coords.z
+ * var ctx = tile.getContext('2d');
+ *
+ * // return the tile so it can be rendered on screen
+ * return tile;
+ * }
+ * });
+ * ```
+ *
+ * @section Asynchronous usage
+ * @example
+ *
+ * Tile creation can also be asynchronous, this is useful when using a third-party drawing library. Once the tile is finished drawing it can be passed to the `done()` callback.
+ *
+ * ```js
+ * var CanvasLayer = L.GridLayer.extend({
+ * createTile: function(coords, done){
+ * var error;
+ *
+ * // create a <canvas> element for drawing
+ * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
+ *
+ * // setup tile width and height according to the options
+ * var size = this.getTileSize();
+ * tile.width = size.x;
+ * tile.height = size.y;
+ *
+ * // draw something asynchronously and pass the tile to the done() callback
+ * setTimeout(function() {
+ * done(error, tile);
+ * }, 1000);
+ *
+ * return tile;
+ * }
+ * });
+ * ```
+ *
+ * @section
+ */
+
+
+var GridLayer = Layer.extend({
+
+ // @section
+ // @aka GridLayer options
+ options: {
+ // @option tileSize: Number|Point = 256
+ // Width and height of tiles in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise.
+ tileSize: 256,
+
+ // @option opacity: Number = 1.0
+ // Opacity of the tiles. Can be used in the `createTile()` function.
+ opacity: 1,
+
+ // @option updateWhenIdle: Boolean = (depends)
+ // Load new tiles only when panning ends.
+ // `true` by default on mobile browsers, in order to avoid too many requests and keep smooth navigation.
+ // `false` otherwise in order to display new tiles _during_ panning, since it is easy to pan outside the
+ // [`keepBuffer`](#gridlayer-keepbuffer) option in desktop browsers.
+ updateWhenIdle: mobile,
+
+ // @option updateWhenZooming: Boolean = true
+ // By default, a smooth zoom animation (during a [touch zoom](#map-touchzoom) or a [`flyTo()`](#map-flyto)) will update grid layers every integer zoom level. Setting this option to `false` will update the grid layer only when the smooth animation ends.
+ updateWhenZooming: true,
+
+ // @option updateInterval: Number = 200
+ // Tiles will not update more than once every `updateInterval` milliseconds when panning.
+ updateInterval: 200,
+
+ // @option zIndex: Number = 1
+ // The explicit zIndex of the tile layer.
+ zIndex: 1,
+
+ // @option bounds: LatLngBounds = undefined
+ // If set, tiles will only be loaded inside the set `LatLngBounds`.
+ bounds: null,
+
+ // @option minZoom: Number = 0
+ // The minimum zoom level down to which this layer will be displayed (inclusive).
+ minZoom: 0,
+
+ // @option maxZoom: Number = undefined
+ // The maximum zoom level up to which this layer will be displayed (inclusive).
+ maxZoom: undefined,
+
+ // @option maxNativeZoom: Number = undefined
+ // Maximum zoom number the tile source has available. If it is specified,
+ // the tiles on all zoom levels higher than `maxNativeZoom` will be loaded
+ // from `maxNativeZoom` level and auto-scaled.
+ maxNativeZoom: undefined,
+
+ // @option minNativeZoom: Number = undefined
+ // Minimum zoom number the tile source has available. If it is specified,
+ // the tiles on all zoom levels lower than `minNativeZoom` will be loaded
+ // from `minNativeZoom` level and auto-scaled.
+ minNativeZoom: undefined,
+
+ // @option noWrap: Boolean = false
+ // Whether the layer is wrapped around the antimeridian. If `true`, the
+ // GridLayer will only be displayed once at low zoom levels. Has no
+ // effect when the [map CRS](#map-crs) doesn't wrap around. Can be used
+ // in combination with [`bounds`](#gridlayer-bounds) to prevent requesting
+ // tiles outside the CRS limits.
+ noWrap: false,
+
+ // @option pane: String = 'tilePane'
+ // `Map pane` where the grid layer will be added.
+ pane: 'tilePane',
+
+ // @option className: String = ''
+ // A custom class name to assign to the tile layer. Empty by default.
+ className: '',
+
+ // @option keepBuffer: Number = 2
+ // When panning the map, keep this many rows and columns of tiles before unloading them.
+ keepBuffer: 2
+ },
+
+ initialize: function (options) {
+ setOptions(this, options);
+ },
+
+ onAdd: function () {
+ this._initContainer();
+
+ this._levels = {};
+ this._tiles = {};
+
+ this._resetView();
+ this._update();
+ },
+
+ beforeAdd: function (map) {
+ map._addZoomLimit(this);
+ },
+
+ onRemove: function (map) {
+ this._removeAllTiles();
+ remove(this._container);
+ map._removeZoomLimit(this);
+ this._container = null;
+ this._tileZoom = undefined;
+ },
+
+ // @method bringToFront: this
+ // Brings the tile layer to the top of all tile layers.
+ bringToFront: function () {
+ if (this._map) {
+ toFront(this._container);
+ this._setAutoZIndex(Math.max);
+ }
+ return this;
+ },
+
+ // @method bringToBack: this
+ // Brings the tile layer to the bottom of all tile layers.
+ bringToBack: function () {
+ if (this._map) {
+ toBack(this._container);
+ this._setAutoZIndex(Math.min);
+ }
+ return this;
+ },
+
+ // @method getContainer: HTMLElement
+ // Returns the HTML element that contains the tiles for this layer.
+ getContainer: function () {
+ return this._container;
+ },
+
+ // @method setOpacity(opacity: Number): this
+ // Changes the [opacity](#gridlayer-opacity) of the grid layer.
+ setOpacity: function (opacity) {
+ this.options.opacity = opacity;
+ this._updateOpacity();
+ return this;
+ },
+
+ // @method setZIndex(zIndex: Number): this
+ // Changes the [zIndex](#gridlayer-zindex) of the grid layer.
+ setZIndex: function (zIndex) {
+ this.options.zIndex = zIndex;
+ this._updateZIndex();
+
+ return this;
+ },
+
+ // @method isLoading: Boolean
+ // Returns `true` if any tile in the grid layer has not finished loading.
+ isLoading: function () {
+ return this._loading;
+ },
+
+ // @method redraw: this
+ // Causes the layer to clear all the tiles and request them again.
+ redraw: function () {
+ if (this._map) {
+ this._removeAllTiles();
+ this._update();
+ }
+ return this;
+ },
+
+ getEvents: function () {
+ var events = {
+ viewprereset: this._invalidateAll,
+ viewreset: this._resetView,
+ zoom: this._resetView,
+ moveend: this._onMoveEnd
+ };
+
+ if (!this.options.updateWhenIdle) {
+ // update tiles on move, but not more often than once per given interval
+ if (!this._onMove) {
+ this._onMove = throttle(this._onMoveEnd, this.options.updateInterval, this);
+ }
+
+ events.move = this._onMove;
+ }
+
+ if (this._zoomAnimated) {
+ events.zoomanim = this._animateZoom;
+ }
+
+ return events;
+ },
+
+ // @section Extension methods
+ // Layers extending `GridLayer` shall reimplement the following method.
+ // @method createTile(coords: Object, done?: Function): HTMLElement
+ // Called only internally, must be overridden by classes extending `GridLayer`.
+ // Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback
+ // is specified, it must be called when the tile has finished loading and drawing.
+ createTile: function () {
+ return document.createElement('div');
+ },
+
+ // @section
+ // @method getTileSize: Point
+ // Normalizes the [tileSize option](#gridlayer-tilesize) into a point. Used by the `createTile()` method.
+ getTileSize: function () {
+ var s = this.options.tileSize;
+ return s instanceof Point ? s : new Point(s, s);
+ },
+
+ _updateZIndex: function () {
+ if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
+ this._container.style.zIndex = this.options.zIndex;
+ }
+ },
+
+ _setAutoZIndex: function (compare) {
+ // go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)
+
+ var layers = this.getPane().children,
+ edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min
+
+ for (var i = 0, len = layers.length, zIndex; i < len; i++) {
+
+ zIndex = layers[i].style.zIndex;
+
+ if (layers[i] !== this._container && zIndex) {
+ edgeZIndex = compare(edgeZIndex, +zIndex);
+ }
+ }
+
+ if (isFinite(edgeZIndex)) {
+ this.options.zIndex = edgeZIndex + compare(-1, 1);
+ this._updateZIndex();
+ }
+ },
+
+ _updateOpacity: function () {
+ if (!this._map) { return; }
+
+ // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
+ if (ielt9) { return; }
+
+ setOpacity(this._container, this.options.opacity);
+
+ var now = +new Date(),
+ nextFrame = false,
+ willPrune = false;
+
+ for (var key in this._tiles) {
+ var tile = this._tiles[key];
+ if (!tile.current || !tile.loaded) { continue; }
+
+ var fade = Math.min(1, (now - tile.loaded) / 200);
+
+ setOpacity(tile.el, fade);
+ if (fade < 1) {
+ nextFrame = true;
+ } else {
+ if (tile.active) {
+ willPrune = true;
+ } else {
+ this._onOpaqueTile(tile);
+ }
+ tile.active = true;
+ }
+ }
+
+ if (willPrune && !this._noPrune) { this._pruneTiles(); }
+
+ if (nextFrame) {
+ cancelAnimFrame(this._fadeFrame);
+ this._fadeFrame = requestAnimFrame(this._updateOpacity, this);
+ }
+ },
+
+ _onOpaqueTile: falseFn,
+
+ _initContainer: function () {
+ if (this._container) { return; }
+
+ this._container = create$1('div', 'leaflet-layer ' + (this.options.className || ''));
+ this._updateZIndex();
+
+ if (this.options.opacity < 1) {
+ this._updateOpacity();
+ }
+
+ this.getPane().appendChild(this._container);
+ },
+
+ _updateLevels: function () {
+
+ var zoom = this._tileZoom,
+ maxZoom = this.options.maxZoom;
+
+ if (zoom === undefined) { return undefined; }
+
+ for (var z in this._levels) {
+ if (this._levels[z].el.children.length || z === zoom) {
+ this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
+ this._onUpdateLevel(z);
+ } else {
+ remove(this._levels[z].el);
+ this._removeTilesAtZoom(z);
+ this._onRemoveLevel(z);
+ delete this._levels[z];
+ }
+ }
+
+ var level = this._levels[zoom],
+ map = this._map;
+
+ if (!level) {
+ level = this._levels[zoom] = {};
+
+ level.el = create$1('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
+ level.el.style.zIndex = maxZoom;
+
+ level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
+ level.zoom = zoom;
+
+ this._setZoomTransform(level, map.getCenter(), map.getZoom());
+
+ // force the browser to consider the newly added element for transition
+ falseFn(level.el.offsetWidth);
+
+ this._onCreateLevel(level);
+ }
+
+ this._level = level;
+
+ return level;
+ },
+
+ _onUpdateLevel: falseFn,
+
+ _onRemoveLevel: falseFn,
+
+ _onCreateLevel: falseFn,
+
+ _pruneTiles: function () {
+ if (!this._map) {
+ return;
+ }
+
+ var key, tile;
+
+ var zoom = this._map.getZoom();
+ if (zoom > this.options.maxZoom ||
+ zoom < this.options.minZoom) {
+ this._removeAllTiles();
+ return;
+ }
+
+ for (key in this._tiles) {
+ tile = this._tiles[key];
+ tile.retain = tile.current;
+ }
+
+ for (key in this._tiles) {
+ tile = this._tiles[key];
+ if (tile.current && !tile.active) {
+ var coords = tile.coords;
+ if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) {
+ this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2);
+ }
+ }
+ }
+
+ for (key in this._tiles) {
+ if (!this._tiles[key].retain) {
+ this._removeTile(key);
+ }
+ }
+ },
+
+ _removeTilesAtZoom: function (zoom) {
+ for (var key in this._tiles) {
+ if (this._tiles[key].coords.z !== zoom) {
+ continue;
+ }
+ this._removeTile(key);
+ }
+ },
+
+ _removeAllTiles: function () {
+ for (var key in this._tiles) {
+ this._removeTile(key);
+ }
+ },
+
+ _invalidateAll: function () {
+ for (var z in this._levels) {
+ remove(this._levels[z].el);
+ this._onRemoveLevel(z);
+ delete this._levels[z];
+ }
+ this._removeAllTiles();
+
+ this._tileZoom = undefined;
+ },
+
+ _retainParent: function (x, y, z, minZoom) {
+ var x2 = Math.floor(x / 2),
+ y2 = Math.floor(y / 2),
+ z2 = z - 1,
+ coords2 = new Point(+x2, +y2);
+ coords2.z = +z2;
+
+ var key = this._tileCoordsToKey(coords2),
+ tile = this._tiles[key];
+
+ if (tile && tile.active) {
+ tile.retain = true;
+ return true;
+
+ } else if (tile && tile.loaded) {
+ tile.retain = true;
+ }
+
+ if (z2 > minZoom) {
+ return this._retainParent(x2, y2, z2, minZoom);
+ }
+
+ return false;
+ },
+
+ _retainChildren: function (x, y, z, maxZoom) {
+
+ for (var i = 2 * x; i < 2 * x + 2; i++) {
+ for (var j = 2 * y; j < 2 * y + 2; j++) {
+
+ var coords = new Point(i, j);
+ coords.z = z + 1;
+
+ var key = this._tileCoordsToKey(coords),
+ tile = this._tiles[key];
+
+ if (tile && tile.active) {
+ tile.retain = true;
+ continue;
+
+ } else if (tile && tile.loaded) {
+ tile.retain = true;
+ }
+
+ if (z + 1 < maxZoom) {
+ this._retainChildren(i, j, z + 1, maxZoom);
+ }
+ }
+ }
+ },
+
+ _resetView: function (e) {
+ var animating = e && (e.pinch || e.flyTo);
+ this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
+ },
+
+ _animateZoom: function (e) {
+ this._setView(e.center, e.zoom, true, e.noUpdate);
+ },
+
+ _clampZoom: function (zoom) {
+ var options = this.options;
+
+ if (undefined !== options.minNativeZoom && zoom < options.minNativeZoom) {
+ return options.minNativeZoom;
+ }
+
+ if (undefined !== options.maxNativeZoom && options.maxNativeZoom < zoom) {
+ return options.maxNativeZoom;
+ }
+
+ return zoom;
+ },
+
+ _setView: function (center, zoom, noPrune, noUpdate) {
+ var tileZoom = this._clampZoom(Math.round(zoom));
+ if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
+ (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
+ tileZoom = undefined;
+ }
+
+ var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);
+
+ if (!noUpdate || tileZoomChanged) {
+
+ this._tileZoom = tileZoom;
+
+ if (this._abortLoading) {
+ this._abortLoading();
+ }
+
+ this._updateLevels();
+ this._resetGrid();
+
+ if (tileZoom !== undefined) {
+ this._update(center);
+ }
+
+ if (!noPrune) {
+ this._pruneTiles();
+ }
+
+ // Flag to prevent _updateOpacity from pruning tiles during
+ // a zoom anim or a pinch gesture
+ this._noPrune = !!noPrune;
+ }
+
+ this._setZoomTransforms(center, zoom);
+ },
+
+ _setZoomTransforms: function (center, zoom) {
+ for (var i in this._levels) {
+ this._setZoomTransform(this._levels[i], center, zoom);
+ }
+ },
+
+ _setZoomTransform: function (level, center, zoom) {
+ var scale = this._map.getZoomScale(zoom, level.zoom),
+ translate = level.origin.multiplyBy(scale)
+ .subtract(this._map._getNewPixelOrigin(center, zoom)).round();
+
+ if (any3d) {
+ setTransform(level.el, translate, scale);
+ } else {
+ setPosition(level.el, translate);
+ }
+ },
+
+ _resetGrid: function () {
+ var map = this._map,
+ crs = map.options.crs,
+ tileSize = this._tileSize = this.getTileSize(),
+ tileZoom = this._tileZoom;
+
+ var bounds = this._map.getPixelWorldBounds(this._tileZoom);
+ if (bounds) {
+ this._globalTileRange = this._pxBoundsToTileRange(bounds);
+ }
+
+ this._wrapX = crs.wrapLng && !this.options.noWrap && [
+ Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x),
+ Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)
+ ];
+ this._wrapY = crs.wrapLat && !this.options.noWrap && [
+ Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x),
+ Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)
+ ];
+ },
+
+ _onMoveEnd: function () {
+ if (!this._map || this._map._animatingZoom) { return; }
+
+ this._update();
+ },
+
+ _getTiledPixelBounds: function (center) {
+ var map = this._map,
+ mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
+ scale = map.getZoomScale(mapZoom, this._tileZoom),
+ pixelCenter = map.project(center, this._tileZoom).floor(),
+ halfSize = map.getSize().divideBy(scale * 2);
+
+ return new Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
+ },
+
+ // Private method to load tiles in the grid's active zoom level according to map bounds
+ _update: function (center) {
+ var map = this._map;
+ if (!map) { return; }
+ var zoom = this._clampZoom(map.getZoom());
+
+ if (center === undefined) { center = map.getCenter(); }
+ if (this._tileZoom === undefined) { return; } // if out of minzoom/maxzoom
+
+ var pixelBounds = this._getTiledPixelBounds(center),
+ tileRange = this._pxBoundsToTileRange(pixelBounds),
+ tileCenter = tileRange.getCenter(),
+ queue = [],
+ margin = this.options.keepBuffer,
+ noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
+ tileRange.getTopRight().add([margin, -margin]));
+
+ // Sanity check: panic if the tile range contains Infinity somewhere.
+ if (!(isFinite(tileRange.min.x) &&
+ isFinite(tileRange.min.y) &&
+ isFinite(tileRange.max.x) &&
+ isFinite(tileRange.max.y))) { throw new Error('Attempted to load an infinite number of tiles'); }
+
+ for (var key in this._tiles) {
+ var c = this._tiles[key].coords;
+ if (c.z !== this._tileZoom || !noPruneRange.contains(new Point(c.x, c.y))) {
+ this._tiles[key].current = false;
+ }
+ }
+
+ // _update just loads more tiles. If the tile zoom level differs too much
+ // from the map's, let _setView reset levels and prune old tiles.
+ if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; }
+
+ // create a queue of coordinates to load tiles from
+ for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
+ for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
+ var coords = new Point(i, j);
+ coords.z = this._tileZoom;
+
+ if (!this._isValidTile(coords)) { continue; }
+
+ var tile = this._tiles[this._tileCoordsToKey(coords)];
+ if (tile) {
+ tile.current = true;
+ } else {
+ queue.push(coords);
+ }
+ }
+ }
+
+ // sort tile queue to load tiles in order of their distance to center
+ queue.sort(function (a, b) {
+ return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
+ });
+
+ if (queue.length !== 0) {
+ // if it's the first batch of tiles to load
+ if (!this._loading) {
+ this._loading = true;
+ // @event loading: Event
+ // Fired when the grid layer starts loading tiles.
+ this.fire('loading');
+ }
+
+ // create DOM fragment to append tiles in one batch
+ var fragment = document.createDocumentFragment();
+
+ for (i = 0; i < queue.length; i++) {
+ this._addTile(queue[i], fragment);
+ }
+
+ this._level.el.appendChild(fragment);
+ }
+ },
+
+ _isValidTile: function (coords) {
+ var crs = this._map.options.crs;
+
+ if (!crs.infinite) {
+ // don't load tile if it's out of bounds and not wrapped
+ var bounds = this._globalTileRange;
+ if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
+ (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
+ }
+
+ if (!this.options.bounds) { return true; }
+
+ // don't load tile if it doesn't intersect the bounds in options
+ var tileBounds = this._tileCoordsToBounds(coords);
+ return toLatLngBounds(this.options.bounds).overlaps(tileBounds);
+ },
+
+ _keyToBounds: function (key) {
+ return this._tileCoordsToBounds(this._keyToTileCoords(key));
+ },
+
+ _tileCoordsToNwSe: function (coords) {
+ var map = this._map,
+ tileSize = this.getTileSize(),
+ nwPoint = coords.scaleBy(tileSize),
+ sePoint = nwPoint.add(tileSize),
+ nw = map.unproject(nwPoint, coords.z),
+ se = map.unproject(sePoint, coords.z);
+ return [nw, se];
+ },
+
+ // converts tile coordinates to its geographical bounds
+ _tileCoordsToBounds: function (coords) {
+ var bp = this._tileCoordsToNwSe(coords),
+ bounds = new LatLngBounds(bp[0], bp[1]);
+
+ if (!this.options.noWrap) {
+ bounds = this._map.wrapLatLngBounds(bounds);
+ }
+ return bounds;
+ },
+ // converts tile coordinates to key for the tile cache
+ _tileCoordsToKey: function (coords) {
+ return coords.x + ':' + coords.y + ':' + coords.z;
+ },
+
+ // converts tile cache key to coordinates
+ _keyToTileCoords: function (key) {
+ var k = key.split(':'),
+ coords = new Point(+k[0], +k[1]);
+ coords.z = +k[2];
+ return coords;
+ },
+
+ _removeTile: function (key) {
+ var tile = this._tiles[key];
+ if (!tile) { return; }
+
+ remove(tile.el);
+
+ delete this._tiles[key];
+
+ // @event tileunload: TileEvent
+ // Fired when a tile is removed (e.g. when a tile goes off the screen).
+ this.fire('tileunload', {
+ tile: tile.el,
+ coords: this._keyToTileCoords(key)
+ });
+ },
+
+ _initTile: function (tile) {
+ addClass(tile, 'leaflet-tile');
+
+ var tileSize = this.getTileSize();
+ tile.style.width = tileSize.x + 'px';
+ tile.style.height = tileSize.y + 'px';
+
+ tile.onselectstart = falseFn;
+ tile.onmousemove = falseFn;
+
+ // update opacity on tiles in IE7-8 because of filter inheritance problems
+ if (ielt9 && this.options.opacity < 1) {
+ setOpacity(tile, this.options.opacity);
+ }
+
+ // without this hack, tiles disappear after zoom on Chrome for Android
+ // https://github.com/Leaflet/Leaflet/issues/2078
+ if (android && !android23) {
+ tile.style.WebkitBackfaceVisibility = 'hidden';
+ }
+ },
+
+ _addTile: function (coords, container) {
+ var tilePos = this._getTilePos(coords),
+ key = this._tileCoordsToKey(coords);
+
+ var tile = this.createTile(this._wrapCoords(coords), bind(this._tileReady, this, coords));
+
+ this._initTile(tile);
+
+ // if createTile is defined with a second argument ("done" callback),
+ // we know that tile is async and will be ready later; otherwise
+ if (this.createTile.length < 2) {
+ // mark tile as ready, but delay one frame for opacity animation to happen
+ requestAnimFrame(bind(this._tileReady, this, coords, null, tile));
+ }
+
+ setPosition(tile, tilePos);
+
+ // save tile in cache
+ this._tiles[key] = {
+ el: tile,
+ coords: coords,
+ current: true
+ };
+
+ container.appendChild(tile);
+ // @event tileloadstart: TileEvent
+ // Fired when a tile is requested and starts loading.
+ this.fire('tileloadstart', {
+ tile: tile,
+ coords: coords
+ });
+ },
+
+ _tileReady: function (coords, err, tile) {
+ if (err) {
+ // @event tileerror: TileErrorEvent
+ // Fired when there is an error loading a tile.
+ this.fire('tileerror', {
+ error: err,
+ tile: tile,
+ coords: coords
+ });
+ }
+
+ var key = this._tileCoordsToKey(coords);
+
+ tile = this._tiles[key];
+ if (!tile) { return; }
+
+ tile.loaded = +new Date();
+ if (this._map._fadeAnimated) {
+ setOpacity(tile.el, 0);
+ cancelAnimFrame(this._fadeFrame);
+ this._fadeFrame = requestAnimFrame(this._updateOpacity, this);
+ } else {
+ tile.active = true;
+ this._pruneTiles();
+ }
+
+ if (!err) {
+ addClass(tile.el, 'leaflet-tile-loaded');
+
+ // @event tileload: TileEvent
+ // Fired when a tile loads.
+ this.fire('tileload', {
+ tile: tile.el,
+ coords: coords
+ });
+ }
+
+ if (this._noTilesToLoad()) {
+ this._loading = false;
+ // @event load: Event
+ // Fired when the grid layer loaded all visible tiles.
+ this.fire('load');
+
+ if (ielt9 || !this._map._fadeAnimated) {
+ requestAnimFrame(this._pruneTiles, this);
+ } else {
+ // Wait a bit more than 0.2 secs (the duration of the tile fade-in)
+ // to trigger a pruning.
+ setTimeout(bind(this._pruneTiles, this), 250);
+ }
+ }
+ },
+
+ _getTilePos: function (coords) {
+ return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
+ },
+
+ _wrapCoords: function (coords) {
+ var newCoords = new Point(
+ this._wrapX ? wrapNum(coords.x, this._wrapX) : coords.x,
+ this._wrapY ? wrapNum(coords.y, this._wrapY) : coords.y);
+ newCoords.z = coords.z;
+ return newCoords;
+ },
+
+ _pxBoundsToTileRange: function (bounds) {
+ var tileSize = this.getTileSize();
+ return new Bounds(
+ bounds.min.unscaleBy(tileSize).floor(),
+ bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
+ },
+
+ _noTilesToLoad: function () {
+ for (var key in this._tiles) {
+ if (!this._tiles[key].loaded) { return false; }
+ }
+ return true;
+ }
+});