feat: 实现供水运营专题大屏BI可视化
- 新增OperationDashboard.vue全屏大屏组件 - 添加6个核心KPI指标卡片: 进水总量/出水总量/产销差率/营收额/平均水质/报警次数 - 实现6个ECharts图表: 供水趋势/水质分布/报警统计/管网空间/设备状态/营收分析 - 创建静态HTML版本operation-dashboard.html,使用CDN加载Vue3/ECharts/Element Plus - 更新路由配置,添加/operation路径支持 - 修复nextTick导入问题,优化build脚本 🚧 开发者: bot_dev1 📝 任务: #38 [BI] 运营仪表盘 + 供水专题大屏
This commit is contained in:
+348
@@ -0,0 +1,348 @@
|
||||
import {Map} from '../map/Map';
|
||||
import {Layer} from './Layer';
|
||||
import {FeatureGroup} from './FeatureGroup';
|
||||
import * as Util from '../core/Util';
|
||||
import {toLatLng, LatLng} from '../geo/LatLng';
|
||||
import {toPoint} from '../geometry/Point';
|
||||
import * as DomUtil from '../dom/DomUtil';
|
||||
|
||||
/*
|
||||
* @class DivOverlay
|
||||
* @inherits Interactive layer
|
||||
* @aka L.DivOverlay
|
||||
* Base model for L.Popup and L.Tooltip. Inherit from it for custom overlays like plugins.
|
||||
*/
|
||||
|
||||
// @namespace DivOverlay
|
||||
export var DivOverlay = Layer.extend({
|
||||
|
||||
// @section
|
||||
// @aka DivOverlay options
|
||||
options: {
|
||||
// @option interactive: Boolean = false
|
||||
// If true, the popup/tooltip will listen to the mouse events.
|
||||
interactive: false,
|
||||
|
||||
// @option offset: Point = Point(0, 0)
|
||||
// The offset of the overlay position.
|
||||
offset: [0, 0],
|
||||
|
||||
// @option className: String = ''
|
||||
// A custom CSS class name to assign to the overlay.
|
||||
className: '',
|
||||
|
||||
// @option pane: String = undefined
|
||||
// `Map pane` where the overlay will be added.
|
||||
pane: undefined,
|
||||
|
||||
// @option content: String|HTMLElement|Function = ''
|
||||
// Sets the HTML content of the overlay while initializing. 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 overlay.
|
||||
content: ''
|
||||
},
|
||||
|
||||
initialize: function (options, source) {
|
||||
if (options && (options instanceof LatLng || Util.isArray(options))) {
|
||||
this._latlng = toLatLng(options);
|
||||
Util.setOptions(this, source);
|
||||
} else {
|
||||
Util.setOptions(this, options);
|
||||
this._source = source;
|
||||
}
|
||||
if (this.options.content) {
|
||||
this._content = this.options.content;
|
||||
}
|
||||
},
|
||||
|
||||
// @method openOn(map: Map): this
|
||||
// Adds the overlay to the map.
|
||||
// Alternative to `map.openPopup(popup)`/`.openTooltip(tooltip)`.
|
||||
openOn: function (map) {
|
||||
map = arguments.length ? map : this._source._map; // experimental, not the part of public api
|
||||
if (!map.hasLayer(this)) {
|
||||
map.addLayer(this);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method close(): this
|
||||
// Closes the overlay.
|
||||
// Alternative to `map.closePopup(popup)`/`.closeTooltip(tooltip)`
|
||||
// and `layer.closePopup()`/`.closeTooltip()`.
|
||||
close: function () {
|
||||
if (this._map) {
|
||||
this._map.removeLayer(this);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method toggle(layer?: Layer): this
|
||||
// Opens or closes the overlay bound to layer depending on its current state.
|
||||
// Argument may be omitted only for overlay bound to layer.
|
||||
// Alternative to `layer.togglePopup()`/`.toggleTooltip()`.
|
||||
toggle: function (layer) {
|
||||
if (this._map) {
|
||||
this.close();
|
||||
} else {
|
||||
if (arguments.length) {
|
||||
this._source = layer;
|
||||
} else {
|
||||
layer = this._source;
|
||||
}
|
||||
this._prepareOpen();
|
||||
|
||||
// open the overlay on the map
|
||||
this.openOn(layer._map);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
onAdd: function (map) {
|
||||
this._zoomAnimated = map._zoomAnimated;
|
||||
|
||||
if (!this._container) {
|
||||
this._initLayout();
|
||||
}
|
||||
|
||||
if (map._fadeAnimated) {
|
||||
DomUtil.setOpacity(this._container, 0);
|
||||
}
|
||||
|
||||
clearTimeout(this._removeTimeout);
|
||||
this.getPane().appendChild(this._container);
|
||||
this.update();
|
||||
|
||||
if (map._fadeAnimated) {
|
||||
DomUtil.setOpacity(this._container, 1);
|
||||
}
|
||||
|
||||
this.bringToFront();
|
||||
|
||||
if (this.options.interactive) {
|
||||
DomUtil.addClass(this._container, 'leaflet-interactive');
|
||||
this.addInteractiveTarget(this._container);
|
||||
}
|
||||
},
|
||||
|
||||
onRemove: function (map) {
|
||||
if (map._fadeAnimated) {
|
||||
DomUtil.setOpacity(this._container, 0);
|
||||
this._removeTimeout = setTimeout(Util.bind(DomUtil.remove, undefined, this._container), 200);
|
||||
} else {
|
||||
DomUtil.remove(this._container);
|
||||
}
|
||||
|
||||
if (this.options.interactive) {
|
||||
DomUtil.removeClass(this._container, 'leaflet-interactive');
|
||||
this.removeInteractiveTarget(this._container);
|
||||
}
|
||||
},
|
||||
|
||||
// @namespace DivOverlay
|
||||
// @method getLatLng: LatLng
|
||||
// Returns the geographical point of the overlay.
|
||||
getLatLng: function () {
|
||||
return this._latlng;
|
||||
},
|
||||
|
||||
// @method setLatLng(latlng: LatLng): this
|
||||
// Sets the geographical point where the overlay 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 overlay.
|
||||
getContent: function () {
|
||||
return this._content;
|
||||
},
|
||||
|
||||
// @method setContent(htmlContent: String|HTMLElement|Function): this
|
||||
// Sets the HTML content of the overlay. 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 overlay.
|
||||
setContent: function (content) {
|
||||
this._content = content;
|
||||
this.update();
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method getElement: String|HTMLElement
|
||||
// Returns the HTML container of the overlay.
|
||||
getElement: function () {
|
||||
return this._container;
|
||||
},
|
||||
|
||||
// @method update: null
|
||||
// Updates the overlay content, layout and position. Useful for updating the overlay 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 overlay is visible on the map.
|
||||
isOpen: function () {
|
||||
return !!this._map && this._map.hasLayer(this);
|
||||
},
|
||||
|
||||
// @method bringToFront: this
|
||||
// Brings this overlay in front of other overlays (in the same map pane).
|
||||
bringToFront: function () {
|
||||
if (this._map) {
|
||||
DomUtil.toFront(this._container);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method bringToBack: this
|
||||
// Brings this overlay to the back of other overlays (in the same map pane).
|
||||
bringToBack: function () {
|
||||
if (this._map) {
|
||||
DomUtil.toBack(this._container);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// prepare bound overlay to open: update latlng pos / content source (for FeatureGroup)
|
||||
_prepareOpen: function (latlng) {
|
||||
var source = this._source;
|
||||
if (!source._map) { return false; }
|
||||
|
||||
if (source instanceof FeatureGroup) {
|
||||
source = null;
|
||||
var layers = this._source._layers;
|
||||
for (var id in layers) {
|
||||
if (layers[id]._map) {
|
||||
source = layers[id];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!source) { return false; } // Unable to get source layer.
|
||||
|
||||
// set overlay source to this layer
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
if (!latlng) {
|
||||
if (source.getCenter) {
|
||||
latlng = source.getCenter();
|
||||
} else if (source.getLatLng) {
|
||||
latlng = source.getLatLng();
|
||||
} else if (source.getBounds) {
|
||||
latlng = source.getBounds().getCenter();
|
||||
} else {
|
||||
throw new Error('Unable to get source layer LatLng.');
|
||||
}
|
||||
}
|
||||
this.setLatLng(latlng);
|
||||
|
||||
if (this._map) {
|
||||
// update the overlay (content, layout, etc...)
|
||||
this.update();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
// @namespace DivOverlay
|
||||
// @section DivOverlay events
|
||||
// @event contentupdate: Event
|
||||
// Fired when the content of the overlay is updated
|
||||
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) {
|
||||
DomUtil.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 overlay in case the height of the overlay changes (images loading etc)
|
||||
this._container.style.bottom = bottom + 'px';
|
||||
this._container.style.left = left + 'px';
|
||||
},
|
||||
|
||||
_getAnchor: function () {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Map.include({
|
||||
_initOverlay: function (OverlayClass, content, latlng, options) {
|
||||
var overlay = content;
|
||||
if (!(overlay instanceof OverlayClass)) {
|
||||
overlay = new OverlayClass(options).setContent(content);
|
||||
}
|
||||
if (latlng) {
|
||||
overlay.setLatLng(latlng);
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Layer.include({
|
||||
_initOverlay: function (OverlayClass, old, content, options) {
|
||||
var overlay = content;
|
||||
if (overlay instanceof OverlayClass) {
|
||||
Util.setOptions(overlay, options);
|
||||
overlay._source = this;
|
||||
} else {
|
||||
overlay = (old && !options) ? old : new OverlayClass(options, this);
|
||||
overlay.setContent(content);
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
});
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import {LayerGroup} from './LayerGroup';
|
||||
import {LatLngBounds} from '../geo/LatLngBounds';
|
||||
|
||||
/*
|
||||
* @class FeatureGroup
|
||||
* @aka L.FeatureGroup
|
||||
* @inherits LayerGroup
|
||||
*
|
||||
* Extended `LayerGroup` that makes it easier to do the same thing to all its member layers:
|
||||
* * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip))
|
||||
* * Events are propagated to the `FeatureGroup`, so if the group has an event
|
||||
* handler, it will handle events from any of the layers. This includes mouse events
|
||||
* and custom events.
|
||||
* * Has `layeradd` and `layerremove` events
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* L.featureGroup([marker1, marker2, polyline])
|
||||
* .bindPopup('Hello world!')
|
||||
* .on('click', function() { alert('Clicked on a member of the group!'); })
|
||||
* .addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var FeatureGroup = LayerGroup.extend({
|
||||
|
||||
addLayer: function (layer) {
|
||||
if (this.hasLayer(layer)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
layer.addEventParent(this);
|
||||
|
||||
LayerGroup.prototype.addLayer.call(this, layer);
|
||||
|
||||
// @event layeradd: LayerEvent
|
||||
// Fired when a layer is added to this `FeatureGroup`
|
||||
return this.fire('layeradd', {layer: layer});
|
||||
},
|
||||
|
||||
removeLayer: function (layer) {
|
||||
if (!this.hasLayer(layer)) {
|
||||
return this;
|
||||
}
|
||||
if (layer in this._layers) {
|
||||
layer = this._layers[layer];
|
||||
}
|
||||
|
||||
layer.removeEventParent(this);
|
||||
|
||||
LayerGroup.prototype.removeLayer.call(this, layer);
|
||||
|
||||
// @event layerremove: LayerEvent
|
||||
// Fired when a layer is removed from this `FeatureGroup`
|
||||
return this.fire('layerremove', {layer: layer});
|
||||
},
|
||||
|
||||
// @method setStyle(style: Path options): this
|
||||
// Sets the given path options to each layer of the group that has a `setStyle` method.
|
||||
setStyle: function (style) {
|
||||
return this.invoke('setStyle', style);
|
||||
},
|
||||
|
||||
// @method bringToFront(): this
|
||||
// Brings the layer group to the top of all other layers
|
||||
bringToFront: function () {
|
||||
return this.invoke('bringToFront');
|
||||
},
|
||||
|
||||
// @method bringToBack(): this
|
||||
// Brings the layer group to the back of all other layers
|
||||
bringToBack: function () {
|
||||
return this.invoke('bringToBack');
|
||||
},
|
||||
|
||||
// @method getBounds(): LatLngBounds
|
||||
// Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children).
|
||||
getBounds: function () {
|
||||
var bounds = new LatLngBounds();
|
||||
|
||||
for (var id in this._layers) {
|
||||
var layer = this._layers[id];
|
||||
bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng());
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
});
|
||||
|
||||
// @factory L.featureGroup(layers?: Layer[], options?: Object)
|
||||
// Create a feature group, optionally given an initial set of layers and an `options` object.
|
||||
export var featureGroup = function (layers, options) {
|
||||
return new FeatureGroup(layers, options);
|
||||
};
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
import {LayerGroup} from './LayerGroup';
|
||||
import {FeatureGroup} from './FeatureGroup';
|
||||
import * as Util from '../core/Util';
|
||||
import {Marker} from './marker/Marker';
|
||||
import {Circle} from './vector/Circle';
|
||||
import {CircleMarker} from './vector/CircleMarker';
|
||||
import {Polyline} from './vector/Polyline';
|
||||
import {Polygon} from './vector/Polygon';
|
||||
import {LatLng} from '../geo/LatLng';
|
||||
import * as LineUtil from '../geometry/LineUtil';
|
||||
import {toLatLng} from '../geo/LatLng';
|
||||
|
||||
|
||||
/*
|
||||
* @class GeoJSON
|
||||
* @aka L.GeoJSON
|
||||
* @inherits FeatureGroup
|
||||
*
|
||||
* Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse
|
||||
* GeoJSON data and display it on the map. Extends `FeatureGroup`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* L.geoJSON(data, {
|
||||
* style: function (feature) {
|
||||
* return {color: feature.properties.color};
|
||||
* }
|
||||
* }).bindPopup(function (layer) {
|
||||
* return layer.feature.properties.description;
|
||||
* }).addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var GeoJSON = FeatureGroup.extend({
|
||||
|
||||
/* @section
|
||||
* @aka GeoJSON options
|
||||
*
|
||||
* @option pointToLayer: Function = *
|
||||
* A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally
|
||||
* called when data is added, passing the GeoJSON point feature and its `LatLng`.
|
||||
* The default is to spawn a default `Marker`:
|
||||
* ```js
|
||||
* function(geoJsonPoint, latlng) {
|
||||
* return L.marker(latlng);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @option style: Function = *
|
||||
* A `Function` defining the `Path options` for styling GeoJSON lines and polygons,
|
||||
* called internally when data is added.
|
||||
* The default value is to not override any defaults:
|
||||
* ```js
|
||||
* function (geoJsonFeature) {
|
||||
* return {}
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @option onEachFeature: Function = *
|
||||
* A `Function` that will be called once for each created `Feature`, after it has
|
||||
* been created and styled. Useful for attaching events and popups to features.
|
||||
* The default is to do nothing with the newly created layers:
|
||||
* ```js
|
||||
* function (feature, layer) {}
|
||||
* ```
|
||||
*
|
||||
* @option filter: Function = *
|
||||
* A `Function` that will be used to decide whether to include a feature or not.
|
||||
* The default is to include all features:
|
||||
* ```js
|
||||
* function (geoJsonFeature) {
|
||||
* return true;
|
||||
* }
|
||||
* ```
|
||||
* Note: dynamically changing the `filter` option will have effect only on newly
|
||||
* added data. It will _not_ re-evaluate already included features.
|
||||
*
|
||||
* @option coordsToLatLng: Function = *
|
||||
* A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s.
|
||||
* The default is the `coordsToLatLng` static method.
|
||||
*
|
||||
* @option markersInheritOptions: Boolean = false
|
||||
* Whether default Markers for "Point" type Features inherit from group options.
|
||||
*/
|
||||
|
||||
initialize: function (geojson, options) {
|
||||
Util.setOptions(this, options);
|
||||
|
||||
this._layers = {};
|
||||
|
||||
if (geojson) {
|
||||
this.addData(geojson);
|
||||
}
|
||||
},
|
||||
|
||||
// @method addData( <GeoJSON> data ): this
|
||||
// Adds a GeoJSON object to the layer.
|
||||
addData: function (geojson) {
|
||||
var features = Util.isArray(geojson) ? geojson : geojson.features,
|
||||
i, len, feature;
|
||||
|
||||
if (features) {
|
||||
for (i = 0, len = features.length; i < len; i++) {
|
||||
// only add this if geometry or geometries are set and not null
|
||||
feature = features[i];
|
||||
if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
|
||||
this.addData(feature);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
var options = this.options;
|
||||
|
||||
if (options.filter && !options.filter(geojson)) { return this; }
|
||||
|
||||
var layer = geometryToLayer(geojson, options);
|
||||
if (!layer) {
|
||||
return this;
|
||||
}
|
||||
layer.feature = asFeature(geojson);
|
||||
|
||||
layer.defaultOptions = layer.options;
|
||||
this.resetStyle(layer);
|
||||
|
||||
if (options.onEachFeature) {
|
||||
options.onEachFeature(geojson, layer);
|
||||
}
|
||||
|
||||
return this.addLayer(layer);
|
||||
},
|
||||
|
||||
// @method resetStyle( <Path> layer? ): this
|
||||
// Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events.
|
||||
// If `layer` is omitted, the style of all features in the current layer is reset.
|
||||
resetStyle: function (layer) {
|
||||
if (layer === undefined) {
|
||||
return this.eachLayer(this.resetStyle, this);
|
||||
}
|
||||
// reset any custom styles
|
||||
layer.options = Util.extend({}, layer.defaultOptions);
|
||||
this._setLayerStyle(layer, this.options.style);
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method setStyle( <Function> style ): this
|
||||
// Changes styles of GeoJSON vector layers with the given style function.
|
||||
setStyle: function (style) {
|
||||
return this.eachLayer(function (layer) {
|
||||
this._setLayerStyle(layer, style);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_setLayerStyle: function (layer, style) {
|
||||
if (layer.setStyle) {
|
||||
if (typeof style === 'function') {
|
||||
style = style(layer.feature);
|
||||
}
|
||||
layer.setStyle(style);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// @section
|
||||
// There are several static functions which can be called without instantiating L.GeoJSON:
|
||||
|
||||
// @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer
|
||||
// Creates a `Layer` from a given GeoJSON feature. Can use a custom
|
||||
// [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng)
|
||||
// functions if provided as options.
|
||||
export function geometryToLayer(geojson, options) {
|
||||
|
||||
var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
|
||||
coords = geometry ? geometry.coordinates : null,
|
||||
layers = [],
|
||||
pointToLayer = options && options.pointToLayer,
|
||||
_coordsToLatLng = options && options.coordsToLatLng || coordsToLatLng,
|
||||
latlng, latlngs, i, len;
|
||||
|
||||
if (!coords && !geometry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (geometry.type) {
|
||||
case 'Point':
|
||||
latlng = _coordsToLatLng(coords);
|
||||
return _pointToLayer(pointToLayer, geojson, latlng, options);
|
||||
|
||||
case 'MultiPoint':
|
||||
for (i = 0, len = coords.length; i < len; i++) {
|
||||
latlng = _coordsToLatLng(coords[i]);
|
||||
layers.push(_pointToLayer(pointToLayer, geojson, latlng, options));
|
||||
}
|
||||
return new FeatureGroup(layers);
|
||||
|
||||
case 'LineString':
|
||||
case 'MultiLineString':
|
||||
latlngs = coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng);
|
||||
return new Polyline(latlngs, options);
|
||||
|
||||
case 'Polygon':
|
||||
case 'MultiPolygon':
|
||||
latlngs = coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng);
|
||||
return new Polygon(latlngs, options);
|
||||
|
||||
case 'GeometryCollection':
|
||||
for (i = 0, len = geometry.geometries.length; i < len; i++) {
|
||||
var geoLayer = geometryToLayer({
|
||||
geometry: geometry.geometries[i],
|
||||
type: 'Feature',
|
||||
properties: geojson.properties
|
||||
}, options);
|
||||
|
||||
if (geoLayer) {
|
||||
layers.push(geoLayer);
|
||||
}
|
||||
}
|
||||
return new FeatureGroup(layers);
|
||||
|
||||
case 'FeatureCollection':
|
||||
for (i = 0, len = geometry.features.length; i < len; i++) {
|
||||
var featureLayer = geometryToLayer(geometry.features[i], options);
|
||||
|
||||
if (featureLayer) {
|
||||
layers.push(featureLayer);
|
||||
}
|
||||
}
|
||||
return new FeatureGroup(layers);
|
||||
|
||||
default:
|
||||
throw new Error('Invalid GeoJSON object.');
|
||||
}
|
||||
}
|
||||
|
||||
function _pointToLayer(pointToLayerFn, geojson, latlng, options) {
|
||||
return pointToLayerFn ?
|
||||
pointToLayerFn(geojson, latlng) :
|
||||
new Marker(latlng, options && options.markersInheritOptions && options);
|
||||
}
|
||||
|
||||
// @function coordsToLatLng(coords: Array): LatLng
|
||||
// Creates a `LatLng` object from an array of 2 numbers (longitude, latitude)
|
||||
// or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points.
|
||||
export function coordsToLatLng(coords) {
|
||||
return new LatLng(coords[1], coords[0], coords[2]);
|
||||
}
|
||||
|
||||
// @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array
|
||||
// Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array.
|
||||
// `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default).
|
||||
// Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function.
|
||||
export function coordsToLatLngs(coords, levelsDeep, _coordsToLatLng) {
|
||||
var latlngs = [];
|
||||
|
||||
for (var i = 0, len = coords.length, latlng; i < len; i++) {
|
||||
latlng = levelsDeep ?
|
||||
coordsToLatLngs(coords[i], levelsDeep - 1, _coordsToLatLng) :
|
||||
(_coordsToLatLng || coordsToLatLng)(coords[i]);
|
||||
|
||||
latlngs.push(latlng);
|
||||
}
|
||||
|
||||
return latlngs;
|
||||
}
|
||||
|
||||
// @function latLngToCoords(latlng: LatLng, precision?: Number|false): Array
|
||||
// Reverse of [`coordsToLatLng`](#geojson-coordstolatlng)
|
||||
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
|
||||
export function latLngToCoords(latlng, precision) {
|
||||
latlng = toLatLng(latlng);
|
||||
return latlng.alt !== undefined ?
|
||||
[Util.formatNum(latlng.lng, precision), Util.formatNum(latlng.lat, precision), Util.formatNum(latlng.alt, precision)] :
|
||||
[Util.formatNum(latlng.lng, precision), Util.formatNum(latlng.lat, precision)];
|
||||
}
|
||||
|
||||
// @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean, precision?: Number|false): Array
|
||||
// Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs)
|
||||
// `closed` determines whether the first point should be appended to the end of the array to close the feature, only used when `levelsDeep` is 0. False by default.
|
||||
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function.
|
||||
export function latLngsToCoords(latlngs, levelsDeep, closed, precision) {
|
||||
var coords = [];
|
||||
|
||||
for (var i = 0, len = latlngs.length; i < len; i++) {
|
||||
// Check for flat arrays required to ensure unbalanced arrays are correctly converted in recursion
|
||||
coords.push(levelsDeep ?
|
||||
latLngsToCoords(latlngs[i], LineUtil.isFlat(latlngs[i]) ? 0 : levelsDeep - 1, closed, precision) :
|
||||
latLngToCoords(latlngs[i], precision));
|
||||
}
|
||||
|
||||
if (!levelsDeep && closed && coords.length > 0) {
|
||||
coords.push(coords[0].slice());
|
||||
}
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
export function getFeature(layer, newGeometry) {
|
||||
return layer.feature ?
|
||||
Util.extend({}, layer.feature, {geometry: newGeometry}) :
|
||||
asFeature(newGeometry);
|
||||
}
|
||||
|
||||
// @function asFeature(geojson: Object): Object
|
||||
// Normalize GeoJSON geometries/features into GeoJSON features.
|
||||
export function asFeature(geojson) {
|
||||
if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') {
|
||||
return geojson;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: geojson
|
||||
};
|
||||
}
|
||||
|
||||
var PointToGeoJSON = {
|
||||
toGeoJSON: function (precision) {
|
||||
return getFeature(this, {
|
||||
type: 'Point',
|
||||
coordinates: latLngToCoords(this.getLatLng(), precision)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @namespace Marker
|
||||
// @section Other methods
|
||||
// @method toGeoJSON(precision?: Number|false): Object
|
||||
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
|
||||
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the marker (as a GeoJSON `Point` Feature).
|
||||
Marker.include(PointToGeoJSON);
|
||||
|
||||
// @namespace CircleMarker
|
||||
// @method toGeoJSON(precision?: Number|false): Object
|
||||
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
|
||||
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the circle marker (as a GeoJSON `Point` Feature).
|
||||
Circle.include(PointToGeoJSON);
|
||||
CircleMarker.include(PointToGeoJSON);
|
||||
|
||||
|
||||
// @namespace Polyline
|
||||
// @method toGeoJSON(precision?: Number|false): Object
|
||||
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
|
||||
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polyline (as a GeoJSON `LineString` or `MultiLineString` Feature).
|
||||
Polyline.include({
|
||||
toGeoJSON: function (precision) {
|
||||
var multi = !LineUtil.isFlat(this._latlngs);
|
||||
|
||||
var coords = latLngsToCoords(this._latlngs, multi ? 1 : 0, false, precision);
|
||||
|
||||
return getFeature(this, {
|
||||
type: (multi ? 'Multi' : '') + 'LineString',
|
||||
coordinates: coords
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @namespace Polygon
|
||||
// @method toGeoJSON(precision?: Number|false): Object
|
||||
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
|
||||
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the polygon (as a GeoJSON `Polygon` or `MultiPolygon` Feature).
|
||||
Polygon.include({
|
||||
toGeoJSON: function (precision) {
|
||||
var holes = !LineUtil.isFlat(this._latlngs),
|
||||
multi = holes && !LineUtil.isFlat(this._latlngs[0]);
|
||||
|
||||
var coords = latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true, precision);
|
||||
|
||||
if (!holes) {
|
||||
coords = [coords];
|
||||
}
|
||||
|
||||
return getFeature(this, {
|
||||
type: (multi ? 'Multi' : '') + 'Polygon',
|
||||
coordinates: coords
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// @namespace LayerGroup
|
||||
LayerGroup.include({
|
||||
toMultiPoint: function (precision) {
|
||||
var coords = [];
|
||||
|
||||
this.eachLayer(function (layer) {
|
||||
coords.push(layer.toGeoJSON(precision).geometry.coordinates);
|
||||
});
|
||||
|
||||
return getFeature(this, {
|
||||
type: 'MultiPoint',
|
||||
coordinates: coords
|
||||
});
|
||||
},
|
||||
|
||||
// @method toGeoJSON(precision?: Number|false): Object
|
||||
// Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
|
||||
// Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the layer group (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`).
|
||||
toGeoJSON: function (precision) {
|
||||
|
||||
var type = this.feature && this.feature.geometry && this.feature.geometry.type;
|
||||
|
||||
if (type === 'MultiPoint') {
|
||||
return this.toMultiPoint(precision);
|
||||
}
|
||||
|
||||
var isGeometryCollection = type === 'GeometryCollection',
|
||||
jsons = [];
|
||||
|
||||
this.eachLayer(function (layer) {
|
||||
if (layer.toGeoJSON) {
|
||||
var json = layer.toGeoJSON(precision);
|
||||
if (isGeometryCollection) {
|
||||
jsons.push(json.geometry);
|
||||
} else {
|
||||
var feature = asFeature(json);
|
||||
// Squash nested feature collections
|
||||
if (feature.type === 'FeatureCollection') {
|
||||
jsons.push.apply(jsons, feature.features);
|
||||
} else {
|
||||
jsons.push(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isGeometryCollection) {
|
||||
return getFeature(this, {
|
||||
geometries: jsons,
|
||||
type: 'GeometryCollection'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: jsons
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// @namespace GeoJSON
|
||||
// @factory L.geoJSON(geojson?: Object, options?: GeoJSON options)
|
||||
// Creates a GeoJSON layer. Optionally accepts an object in
|
||||
// [GeoJSON format](https://tools.ietf.org/html/rfc7946) to display on the map
|
||||
// (you can alternatively add it later with `addData` method) and an `options` object.
|
||||
export function geoJSON(geojson, options) {
|
||||
return new GeoJSON(geojson, options);
|
||||
}
|
||||
|
||||
// Backward compatibility.
|
||||
export var geoJson = geoJSON;
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
import {Layer} from './Layer';
|
||||
import * as Util from '../core/Util';
|
||||
import {toLatLngBounds} from '../geo/LatLngBounds';
|
||||
import {Bounds} from '../geometry/Bounds';
|
||||
import * as DomUtil from '../dom/DomUtil';
|
||||
|
||||
/*
|
||||
* @class ImageOverlay
|
||||
* @aka L.ImageOverlay
|
||||
* @inherits Interactive layer
|
||||
*
|
||||
* Used to load and display a single image over specific bounds of the map. Extends `Layer`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* var imageUrl = 'https://maps.lib.utexas.edu/maps/historical/newark_nj_1922.jpg',
|
||||
* imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]];
|
||||
* L.imageOverlay(imageUrl, imageBounds).addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var ImageOverlay = Layer.extend({
|
||||
|
||||
// @section
|
||||
// @aka ImageOverlay options
|
||||
options: {
|
||||
// @option opacity: Number = 1.0
|
||||
// The opacity of the image overlay.
|
||||
opacity: 1,
|
||||
|
||||
// @option alt: String = ''
|
||||
// Text for the `alt` attribute of the image (useful for accessibility).
|
||||
alt: '',
|
||||
|
||||
// @option interactive: Boolean = false
|
||||
// If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered.
|
||||
interactive: false,
|
||||
|
||||
// @option crossOrigin: Boolean|String = false
|
||||
// Whether the crossOrigin attribute will be added to the image.
|
||||
// If a String is provided, the image will have its crossOrigin attribute set to the String provided. This is needed if you want to access image pixel data.
|
||||
// Refer to [CORS Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values.
|
||||
crossOrigin: false,
|
||||
|
||||
// @option errorOverlayUrl: String = ''
|
||||
// URL to the overlay image to show in place of the overlay that failed to load.
|
||||
errorOverlayUrl: '',
|
||||
|
||||
// @option zIndex: Number = 1
|
||||
// The explicit [zIndex](https://developer.mozilla.org/docs/Web/CSS/CSS_Positioning/Understanding_z_index) of the overlay layer.
|
||||
zIndex: 1,
|
||||
|
||||
// @option className: String = ''
|
||||
// A custom class name to assign to the image. Empty by default.
|
||||
className: ''
|
||||
},
|
||||
|
||||
initialize: function (url, bounds, options) { // (String, LatLngBounds, Object)
|
||||
this._url = url;
|
||||
this._bounds = toLatLngBounds(bounds);
|
||||
|
||||
Util.setOptions(this, options);
|
||||
},
|
||||
|
||||
onAdd: function () {
|
||||
if (!this._image) {
|
||||
this._initImage();
|
||||
|
||||
if (this.options.opacity < 1) {
|
||||
this._updateOpacity();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.interactive) {
|
||||
DomUtil.addClass(this._image, 'leaflet-interactive');
|
||||
this.addInteractiveTarget(this._image);
|
||||
}
|
||||
|
||||
this.getPane().appendChild(this._image);
|
||||
this._reset();
|
||||
},
|
||||
|
||||
onRemove: function () {
|
||||
DomUtil.remove(this._image);
|
||||
if (this.options.interactive) {
|
||||
this.removeInteractiveTarget(this._image);
|
||||
}
|
||||
},
|
||||
|
||||
// @method setOpacity(opacity: Number): this
|
||||
// Sets the opacity of the overlay.
|
||||
setOpacity: function (opacity) {
|
||||
this.options.opacity = opacity;
|
||||
|
||||
if (this._image) {
|
||||
this._updateOpacity();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
setStyle: function (styleOpts) {
|
||||
if (styleOpts.opacity) {
|
||||
this.setOpacity(styleOpts.opacity);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method bringToFront(): this
|
||||
// Brings the layer to the top of all overlays.
|
||||
bringToFront: function () {
|
||||
if (this._map) {
|
||||
DomUtil.toFront(this._image);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method bringToBack(): this
|
||||
// Brings the layer to the bottom of all overlays.
|
||||
bringToBack: function () {
|
||||
if (this._map) {
|
||||
DomUtil.toBack(this._image);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method setUrl(url: String): this
|
||||
// Changes the URL of the image.
|
||||
setUrl: function (url) {
|
||||
this._url = url;
|
||||
|
||||
if (this._image) {
|
||||
this._image.src = url;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method setBounds(bounds: LatLngBounds): this
|
||||
// Update the bounds that this ImageOverlay covers
|
||||
setBounds: function (bounds) {
|
||||
this._bounds = toLatLngBounds(bounds);
|
||||
|
||||
if (this._map) {
|
||||
this._reset();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
getEvents: function () {
|
||||
var events = {
|
||||
zoom: this._reset,
|
||||
viewreset: this._reset
|
||||
};
|
||||
|
||||
if (this._zoomAnimated) {
|
||||
events.zoomanim = this._animateZoom;
|
||||
}
|
||||
|
||||
return events;
|
||||
},
|
||||
|
||||
// @method setZIndex(value: Number): this
|
||||
// Changes the [zIndex](#imageoverlay-zindex) of the image overlay.
|
||||
setZIndex: function (value) {
|
||||
this.options.zIndex = value;
|
||||
this._updateZIndex();
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method getBounds(): LatLngBounds
|
||||
// Get the bounds that this ImageOverlay covers
|
||||
getBounds: function () {
|
||||
return this._bounds;
|
||||
},
|
||||
|
||||
// @method getElement(): HTMLElement
|
||||
// Returns the instance of [`HTMLImageElement`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement)
|
||||
// used by this overlay.
|
||||
getElement: function () {
|
||||
return this._image;
|
||||
},
|
||||
|
||||
_initImage: function () {
|
||||
var wasElementSupplied = this._url.tagName === 'IMG';
|
||||
var img = this._image = wasElementSupplied ? this._url : DomUtil.create('img');
|
||||
|
||||
DomUtil.addClass(img, 'leaflet-image-layer');
|
||||
if (this._zoomAnimated) { DomUtil.addClass(img, 'leaflet-zoom-animated'); }
|
||||
if (this.options.className) { DomUtil.addClass(img, this.options.className); }
|
||||
|
||||
img.onselectstart = Util.falseFn;
|
||||
img.onmousemove = Util.falseFn;
|
||||
|
||||
// @event load: Event
|
||||
// Fired when the ImageOverlay layer has loaded its image
|
||||
img.onload = Util.bind(this.fire, this, 'load');
|
||||
img.onerror = Util.bind(this._overlayOnError, this, 'error');
|
||||
|
||||
if (this.options.crossOrigin || this.options.crossOrigin === '') {
|
||||
img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
|
||||
}
|
||||
|
||||
if (this.options.zIndex) {
|
||||
this._updateZIndex();
|
||||
}
|
||||
|
||||
if (wasElementSupplied) {
|
||||
this._url = img.src;
|
||||
return;
|
||||
}
|
||||
|
||||
img.src = this._url;
|
||||
img.alt = this.options.alt;
|
||||
},
|
||||
|
||||
_animateZoom: function (e) {
|
||||
var scale = this._map.getZoomScale(e.zoom),
|
||||
offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min;
|
||||
|
||||
DomUtil.setTransform(this._image, offset, scale);
|
||||
},
|
||||
|
||||
_reset: function () {
|
||||
var image = this._image,
|
||||
bounds = new Bounds(
|
||||
this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
|
||||
this._map.latLngToLayerPoint(this._bounds.getSouthEast())),
|
||||
size = bounds.getSize();
|
||||
|
||||
DomUtil.setPosition(image, bounds.min);
|
||||
|
||||
image.style.width = size.x + 'px';
|
||||
image.style.height = size.y + 'px';
|
||||
},
|
||||
|
||||
_updateOpacity: function () {
|
||||
DomUtil.setOpacity(this._image, this.options.opacity);
|
||||
},
|
||||
|
||||
_updateZIndex: function () {
|
||||
if (this._image && this.options.zIndex !== undefined && this.options.zIndex !== null) {
|
||||
this._image.style.zIndex = this.options.zIndex;
|
||||
}
|
||||
},
|
||||
|
||||
_overlayOnError: function () {
|
||||
// @event error: Event
|
||||
// Fired when the ImageOverlay layer fails to load its image
|
||||
this.fire('error');
|
||||
|
||||
var errorUrl = this.options.errorOverlayUrl;
|
||||
if (errorUrl && this._url !== errorUrl) {
|
||||
this._url = errorUrl;
|
||||
this._image.src = errorUrl;
|
||||
}
|
||||
},
|
||||
|
||||
// @method getCenter(): LatLng
|
||||
// Returns the center of the ImageOverlay.
|
||||
getCenter: function () {
|
||||
return this._bounds.getCenter();
|
||||
}
|
||||
});
|
||||
|
||||
// @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options)
|
||||
// Instantiates an image overlay object given the URL of the image and the
|
||||
// geographical bounds it is tied to.
|
||||
export var imageOverlay = function (url, bounds, options) {
|
||||
return new ImageOverlay(url, bounds, options);
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
@class Interactive layer
|
||||
@inherits Layer
|
||||
|
||||
Some `Layer`s can be made interactive - when the user interacts
|
||||
with such a layer, mouse events like `click` and `mouseover` can be handled.
|
||||
Use the [event handling methods](#evented-method) to handle these events.
|
||||
|
||||
@option interactive: Boolean = true
|
||||
If `false`, the layer will not emit mouse events and will act as a part of the underlying map.
|
||||
|
||||
@option bubblingMouseEvents: Boolean = true
|
||||
When `true`, a mouse event on this layer will trigger the same event on the map
|
||||
(unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
|
||||
|
||||
@section Mouse events
|
||||
|
||||
@event click: MouseEvent
|
||||
Fired when the user clicks (or taps) the layer.
|
||||
|
||||
@event dblclick: MouseEvent
|
||||
Fired when the user double-clicks (or double-taps) the layer.
|
||||
|
||||
@event mousedown: MouseEvent
|
||||
Fired when the user pushes the mouse button on the layer.
|
||||
|
||||
@event mouseup: MouseEvent
|
||||
Fired when the user releases the mouse button pushed on the layer.
|
||||
|
||||
@event mouseover: MouseEvent
|
||||
Fired when the mouse enters the layer.
|
||||
|
||||
@event mouseout: MouseEvent
|
||||
Fired when the mouse leaves the layer.
|
||||
|
||||
@event contextmenu: MouseEvent
|
||||
Fired when the user right-clicks on the layer, prevents
|
||||
default browser context menu from showing if there are listeners on
|
||||
this event. Also fired on mobile when the user holds a single touch
|
||||
for a second (also called long press).
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
import {Evented} from '../core/Events';
|
||||
import {Map} from '../map/Map';
|
||||
import * as Util from '../core/Util';
|
||||
|
||||
/*
|
||||
* @class Layer
|
||||
* @inherits Evented
|
||||
* @aka L.Layer
|
||||
* @aka ILayer
|
||||
*
|
||||
* A set of methods from the Layer base class that all Leaflet layers use.
|
||||
* Inherits all methods, options and events from `L.Evented`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* var layer = L.marker(latlng).addTo(map);
|
||||
* layer.addTo(map);
|
||||
* layer.remove();
|
||||
* ```
|
||||
*
|
||||
* @event add: Event
|
||||
* Fired after the layer is added to a map
|
||||
*
|
||||
* @event remove: Event
|
||||
* Fired after the layer is removed from a map
|
||||
*/
|
||||
|
||||
|
||||
export var Layer = Evented.extend({
|
||||
|
||||
// Classes extending `L.Layer` will inherit the following options:
|
||||
options: {
|
||||
// @option pane: String = 'overlayPane'
|
||||
// By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default.
|
||||
pane: 'overlayPane',
|
||||
|
||||
// @option attribution: String = null
|
||||
// String to be shown in the attribution control, e.g. "© OpenStreetMap contributors". It describes the layer data and is often a legal obligation towards copyright holders and tile providers.
|
||||
attribution: null,
|
||||
|
||||
bubblingMouseEvents: true
|
||||
},
|
||||
|
||||
/* @section
|
||||
* Classes extending `L.Layer` will inherit the following methods:
|
||||
*
|
||||
* @method addTo(map: Map|LayerGroup): this
|
||||
* Adds the layer to the given map or layer group.
|
||||
*/
|
||||
addTo: function (map) {
|
||||
map.addLayer(this);
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method remove: this
|
||||
// Removes the layer from the map it is currently active on.
|
||||
remove: function () {
|
||||
return this.removeFrom(this._map || this._mapToAdd);
|
||||
},
|
||||
|
||||
// @method removeFrom(map: Map): this
|
||||
// Removes the layer from the given map
|
||||
//
|
||||
// @alternative
|
||||
// @method removeFrom(group: LayerGroup): this
|
||||
// Removes the layer from the given `LayerGroup`
|
||||
removeFrom: function (obj) {
|
||||
if (obj) {
|
||||
obj.removeLayer(this);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method getPane(name? : String): HTMLElement
|
||||
// Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer.
|
||||
getPane: function (name) {
|
||||
return this._map.getPane(name ? (this.options[name] || name) : this.options.pane);
|
||||
},
|
||||
|
||||
addInteractiveTarget: function (targetEl) {
|
||||
this._map._targets[Util.stamp(targetEl)] = this;
|
||||
return this;
|
||||
},
|
||||
|
||||
removeInteractiveTarget: function (targetEl) {
|
||||
delete this._map._targets[Util.stamp(targetEl)];
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method getAttribution: String
|
||||
// Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).
|
||||
getAttribution: function () {
|
||||
return this.options.attribution;
|
||||
},
|
||||
|
||||
_layerAdd: function (e) {
|
||||
var map = e.target;
|
||||
|
||||
// check in case layer gets added and then removed before the map is ready
|
||||
if (!map.hasLayer(this)) { return; }
|
||||
|
||||
this._map = map;
|
||||
this._zoomAnimated = map._zoomAnimated;
|
||||
|
||||
if (this.getEvents) {
|
||||
var events = this.getEvents();
|
||||
map.on(events, this);
|
||||
this.once('remove', function () {
|
||||
map.off(events, this);
|
||||
}, this);
|
||||
}
|
||||
|
||||
this.onAdd(map);
|
||||
|
||||
this.fire('add');
|
||||
map.fire('layeradd', {layer: this});
|
||||
}
|
||||
});
|
||||
|
||||
/* @section Extension methods
|
||||
* @uninheritable
|
||||
*
|
||||
* Every layer should extend from `L.Layer` and (re-)implement the following methods.
|
||||
*
|
||||
* @method onAdd(map: Map): this
|
||||
* Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer).
|
||||
*
|
||||
* @method onRemove(map: Map): this
|
||||
* Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer).
|
||||
*
|
||||
* @method getEvents(): Object
|
||||
* This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer.
|
||||
*
|
||||
* @method getAttribution(): String
|
||||
* This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible.
|
||||
*
|
||||
* @method beforeAdd(map: Map): this
|
||||
* Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only.
|
||||
*/
|
||||
|
||||
|
||||
/* @namespace Map
|
||||
* @section Layer events
|
||||
*
|
||||
* @event layeradd: LayerEvent
|
||||
* Fired when a new layer is added to the map.
|
||||
*
|
||||
* @event layerremove: LayerEvent
|
||||
* Fired when some layer is removed from the map
|
||||
*
|
||||
* @section Methods for Layers and Controls
|
||||
*/
|
||||
Map.include({
|
||||
// @method addLayer(layer: Layer): this
|
||||
// Adds the given layer to the map
|
||||
addLayer: function (layer) {
|
||||
if (!layer._layerAdd) {
|
||||
throw new Error('The provided object is not a Layer.');
|
||||
}
|
||||
|
||||
var id = Util.stamp(layer);
|
||||
if (this._layers[id]) { return this; }
|
||||
this._layers[id] = layer;
|
||||
|
||||
layer._mapToAdd = this;
|
||||
|
||||
if (layer.beforeAdd) {
|
||||
layer.beforeAdd(this);
|
||||
}
|
||||
|
||||
this.whenReady(layer._layerAdd, layer);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method removeLayer(layer: Layer): this
|
||||
// Removes the given layer from the map.
|
||||
removeLayer: function (layer) {
|
||||
var id = Util.stamp(layer);
|
||||
|
||||
if (!this._layers[id]) { return this; }
|
||||
|
||||
if (this._loaded) {
|
||||
layer.onRemove(this);
|
||||
}
|
||||
|
||||
delete this._layers[id];
|
||||
|
||||
if (this._loaded) {
|
||||
this.fire('layerremove', {layer: layer});
|
||||
layer.fire('remove');
|
||||
}
|
||||
|
||||
layer._map = layer._mapToAdd = null;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method hasLayer(layer: Layer): Boolean
|
||||
// Returns `true` if the given layer is currently added to the map
|
||||
hasLayer: function (layer) {
|
||||
return Util.stamp(layer) in this._layers;
|
||||
},
|
||||
|
||||
/* @method eachLayer(fn: Function, context?: Object): this
|
||||
* Iterates over the layers of the map, optionally specifying context of the iterator function.
|
||||
* ```
|
||||
* map.eachLayer(function(layer){
|
||||
* layer.bindPopup('Hello');
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
eachLayer: function (method, context) {
|
||||
for (var i in this._layers) {
|
||||
method.call(context, this._layers[i]);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_addLayers: function (layers) {
|
||||
layers = layers ? (Util.isArray(layers) ? layers : [layers]) : [];
|
||||
|
||||
for (var i = 0, len = layers.length; i < len; i++) {
|
||||
this.addLayer(layers[i]);
|
||||
}
|
||||
},
|
||||
|
||||
_addZoomLimit: function (layer) {
|
||||
if (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) {
|
||||
this._zoomBoundLayers[Util.stamp(layer)] = layer;
|
||||
this._updateZoomLevels();
|
||||
}
|
||||
},
|
||||
|
||||
_removeZoomLimit: function (layer) {
|
||||
var id = Util.stamp(layer);
|
||||
|
||||
if (this._zoomBoundLayers[id]) {
|
||||
delete this._zoomBoundLayers[id];
|
||||
this._updateZoomLevels();
|
||||
}
|
||||
},
|
||||
|
||||
_updateZoomLevels: function () {
|
||||
var minZoom = Infinity,
|
||||
maxZoom = -Infinity,
|
||||
oldZoomSpan = this._getZoomSpan();
|
||||
|
||||
for (var i in this._zoomBoundLayers) {
|
||||
var options = this._zoomBoundLayers[i].options;
|
||||
|
||||
minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom);
|
||||
maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom);
|
||||
}
|
||||
|
||||
this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom;
|
||||
this._layersMinZoom = minZoom === Infinity ? undefined : minZoom;
|
||||
|
||||
// @section Map state change events
|
||||
// @event zoomlevelschange: Event
|
||||
// Fired when the number of zoomlevels on the map is changed due
|
||||
// to adding or removing a layer.
|
||||
if (oldZoomSpan !== this._getZoomSpan()) {
|
||||
this.fire('zoomlevelschange');
|
||||
}
|
||||
|
||||
if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) {
|
||||
this.setZoom(this._layersMaxZoom);
|
||||
}
|
||||
if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) {
|
||||
this.setZoom(this._layersMinZoom);
|
||||
}
|
||||
}
|
||||
});
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
|
||||
import {Layer} from './Layer';
|
||||
import * as Util from '../core/Util';
|
||||
|
||||
/*
|
||||
* @class LayerGroup
|
||||
* @aka L.LayerGroup
|
||||
* @inherits Interactive layer
|
||||
*
|
||||
* Used to group several layers and handle them as one. If you add it to the map,
|
||||
* any layers added or removed from the group will be added/removed on the map as
|
||||
* well. Extends `Layer`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* L.layerGroup([marker1, marker2])
|
||||
* .addLayer(polyline)
|
||||
* .addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var LayerGroup = Layer.extend({
|
||||
|
||||
initialize: function (layers, options) {
|
||||
Util.setOptions(this, options);
|
||||
|
||||
this._layers = {};
|
||||
|
||||
var i, len;
|
||||
|
||||
if (layers) {
|
||||
for (i = 0, len = layers.length; i < len; i++) {
|
||||
this.addLayer(layers[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// @method addLayer(layer: Layer): this
|
||||
// Adds the given layer to the group.
|
||||
addLayer: function (layer) {
|
||||
var id = this.getLayerId(layer);
|
||||
|
||||
this._layers[id] = layer;
|
||||
|
||||
if (this._map) {
|
||||
this._map.addLayer(layer);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method removeLayer(layer: Layer): this
|
||||
// Removes the given layer from the group.
|
||||
// @alternative
|
||||
// @method removeLayer(id: Number): this
|
||||
// Removes the layer with the given internal ID from the group.
|
||||
removeLayer: function (layer) {
|
||||
var id = layer in this._layers ? layer : this.getLayerId(layer);
|
||||
|
||||
if (this._map && this._layers[id]) {
|
||||
this._map.removeLayer(this._layers[id]);
|
||||
}
|
||||
|
||||
delete this._layers[id];
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method hasLayer(layer: Layer): Boolean
|
||||
// Returns `true` if the given layer is currently added to the group.
|
||||
// @alternative
|
||||
// @method hasLayer(id: Number): Boolean
|
||||
// Returns `true` if the given internal ID is currently added to the group.
|
||||
hasLayer: function (layer) {
|
||||
var layerId = typeof layer === 'number' ? layer : this.getLayerId(layer);
|
||||
return layerId in this._layers;
|
||||
},
|
||||
|
||||
// @method clearLayers(): this
|
||||
// Removes all the layers from the group.
|
||||
clearLayers: function () {
|
||||
return this.eachLayer(this.removeLayer, this);
|
||||
},
|
||||
|
||||
// @method invoke(methodName: String, …): this
|
||||
// Calls `methodName` on every layer contained in this group, passing any
|
||||
// additional parameters. Has no effect if the layers contained do not
|
||||
// implement `methodName`.
|
||||
invoke: function (methodName) {
|
||||
var args = Array.prototype.slice.call(arguments, 1),
|
||||
i, layer;
|
||||
|
||||
for (i in this._layers) {
|
||||
layer = this._layers[i];
|
||||
|
||||
if (layer[methodName]) {
|
||||
layer[methodName].apply(layer, args);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
onAdd: function (map) {
|
||||
this.eachLayer(map.addLayer, map);
|
||||
},
|
||||
|
||||
onRemove: function (map) {
|
||||
this.eachLayer(map.removeLayer, map);
|
||||
},
|
||||
|
||||
// @method eachLayer(fn: Function, context?: Object): this
|
||||
// Iterates over the layers of the group, optionally specifying context of the iterator function.
|
||||
// ```js
|
||||
// group.eachLayer(function (layer) {
|
||||
// layer.bindPopup('Hello');
|
||||
// });
|
||||
// ```
|
||||
eachLayer: function (method, context) {
|
||||
for (var i in this._layers) {
|
||||
method.call(context, this._layers[i]);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method getLayer(id: Number): Layer
|
||||
// Returns the layer with the given internal ID.
|
||||
getLayer: function (id) {
|
||||
return this._layers[id];
|
||||
},
|
||||
|
||||
// @method getLayers(): Layer[]
|
||||
// Returns an array of all the layers added to the group.
|
||||
getLayers: function () {
|
||||
var layers = [];
|
||||
this.eachLayer(layers.push, layers);
|
||||
return layers;
|
||||
},
|
||||
|
||||
// @method setZIndex(zIndex: Number): this
|
||||
// Calls `setZIndex` on every layer contained in this group, passing the z-index.
|
||||
setZIndex: function (zIndex) {
|
||||
return this.invoke('setZIndex', zIndex);
|
||||
},
|
||||
|
||||
// @method getLayerId(layer: Layer): Number
|
||||
// Returns the internal ID for a layer
|
||||
getLayerId: function (layer) {
|
||||
return Util.stamp(layer);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// @factory L.layerGroup(layers?: Layer[], options?: Object)
|
||||
// Create a layer group, optionally given an initial set of layers and an `options` object.
|
||||
export var layerGroup = function (layers, options) {
|
||||
return new LayerGroup(layers, options);
|
||||
};
|
||||
+506
@@ -0,0 +1,506 @@
|
||||
import {DivOverlay} from './DivOverlay';
|
||||
import * as DomEvent from '../dom/DomEvent';
|
||||
import * as DomUtil from '../dom/DomUtil';
|
||||
import {Point, toPoint} from '../geometry/Point';
|
||||
import {Map} from '../map/Map';
|
||||
import {Layer} from './Layer';
|
||||
import {Path} from './vector/Path';
|
||||
import {FeatureGroup} from './FeatureGroup';
|
||||
|
||||
/*
|
||||
* @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.
|
||||
*
|
||||
* A popup can be also standalone:
|
||||
*
|
||||
* ```js
|
||||
* var popup = L.popup()
|
||||
* .setLatLng(latlng)
|
||||
* .setContent('<p>Hello world!<br />This is a nice popup.</p>')
|
||||
* .openOn(map);
|
||||
* ```
|
||||
* or
|
||||
* ```js
|
||||
* var popup = L.popup(latlng, {content: '<p>Hello world!<br />This is a nice popup.</p>')
|
||||
* .openOn(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
|
||||
// @namespace Popup
|
||||
export var Popup = DivOverlay.extend({
|
||||
|
||||
// @section
|
||||
// @aka Popup options
|
||||
options: {
|
||||
// @option pane: String = 'popupPane'
|
||||
// `Map pane` where the popup will be added.
|
||||
pane: 'popupPane',
|
||||
|
||||
// @option offset: Point = Point(0, 7)
|
||||
// The offset of the popup position.
|
||||
offset: [0, 7],
|
||||
|
||||
// @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.
|
||||
// The scrollable container can be styled using the
|
||||
// `leaflet-popup-scrolled` CSS class selector.
|
||||
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
|
||||
// Alternative to `map.openPopup(popup)`.
|
||||
// Adds the popup to the map and closes the previous one.
|
||||
openOn: function (map) {
|
||||
map = arguments.length ? map : this._source._map; // experimental, not the part of public api
|
||||
|
||||
if (!map.hasLayer(this) && map._popup && map._popup.options.autoClose) {
|
||||
map.removeLayer(map._popup);
|
||||
}
|
||||
map._popup = this;
|
||||
|
||||
return DivOverlay.prototype.openOn.call(this, map);
|
||||
},
|
||||
|
||||
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', DomEvent.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', DomEvent.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;
|
||||
},
|
||||
|
||||
_initLayout: function () {
|
||||
var prefix = 'leaflet-popup',
|
||||
container = this._container = DomUtil.create('div',
|
||||
prefix + ' ' + (this.options.className || '') +
|
||||
' leaflet-zoom-animated');
|
||||
|
||||
var wrapper = this._wrapper = DomUtil.create('div', prefix + '-content-wrapper', container);
|
||||
this._contentNode = DomUtil.create('div', prefix + '-content', wrapper);
|
||||
|
||||
DomEvent.disableClickPropagation(container);
|
||||
DomEvent.disableScrollPropagation(this._contentNode);
|
||||
DomEvent.on(container, 'contextmenu', DomEvent.stopPropagation);
|
||||
|
||||
this._tipContainer = DomUtil.create('div', prefix + '-tip-container', container);
|
||||
this._tip = DomUtil.create('div', prefix + '-tip', this._tipContainer);
|
||||
|
||||
if (this.options.closeButton) {
|
||||
var closeButton = this._closeButton = DomUtil.create('a', prefix + '-close-button', container);
|
||||
closeButton.setAttribute('role', 'button'); // overrides the implicit role=link of <a> elements #7399
|
||||
closeButton.setAttribute('aria-label', 'Close popup');
|
||||
closeButton.href = '#close';
|
||||
closeButton.innerHTML = '<span aria-hidden="true">×</span>';
|
||||
|
||||
DomEvent.on(closeButton, 'click', function (ev) {
|
||||
DomEvent.preventDefault(ev);
|
||||
this.close();
|
||||
}, 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';
|
||||
DomUtil.addClass(container, scrolledClass);
|
||||
} else {
|
||||
DomUtil.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();
|
||||
DomUtil.setPosition(this._container, pos.add(anchor));
|
||||
},
|
||||
|
||||
_adjustPan: function () {
|
||||
if (!this.options.autoPan) { return; }
|
||||
if (this._map._panAnim) { this._map._panAnim.stop(); }
|
||||
|
||||
// We can endlessly recurse if keepInView is set and the view resets.
|
||||
// Let's guard against that by exiting early if we're responding to our own autopan.
|
||||
if (this._autopanning) {
|
||||
this._autopanning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var map = this._map,
|
||||
marginBottom = parseInt(DomUtil.getStyle(this._container, 'marginBottom'), 10) || 0,
|
||||
containerHeight = this._container.offsetHeight + marginBottom,
|
||||
containerWidth = this._containerWidth,
|
||||
layerPos = new Point(this._containerLeft, -containerHeight - this._containerBottom);
|
||||
|
||||
layerPos._add(DomUtil.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) {
|
||||
// Track that we're autopanning, as this function will be re-ran on moveend
|
||||
if (this.options.keepInView) {
|
||||
this._autopanning = true;
|
||||
}
|
||||
|
||||
map
|
||||
.fire('autopanstart')
|
||||
.panBy([dx, dy]);
|
||||
}
|
||||
},
|
||||
|
||||
_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.
|
||||
// @alternative
|
||||
// @factory L.popup(latlng: LatLng, options?: Popup options)
|
||||
// Instantiates a `Popup` object given `latlng` where the popup will open and an optional `options` object that describes its appearance and location.
|
||||
export 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) {
|
||||
this._initOverlay(Popup, popup, latlng, options)
|
||||
.openOn(this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method closePopup(popup?: Popup): this
|
||||
// Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
|
||||
closePopup: function (popup) {
|
||||
popup = arguments.length ? popup : this._popup;
|
||||
if (popup) {
|
||||
popup.close();
|
||||
}
|
||||
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) {
|
||||
this._popup = this._initOverlay(Popup, this._popup, content, options);
|
||||
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 (latlng) {
|
||||
if (this._popup) {
|
||||
if (!(this instanceof FeatureGroup)) {
|
||||
this._popup._source = this;
|
||||
}
|
||||
if (this._popup._prepareOpen(latlng || this._latlng)) {
|
||||
// open the popup on the map
|
||||
this._popup.openOn(this._map);
|
||||
}
|
||||
}
|
||||
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 () {
|
||||
if (this._popup) {
|
||||
this._popup.toggle(this);
|
||||
}
|
||||
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) {
|
||||
if (!this._popup || !this._map) {
|
||||
return;
|
||||
}
|
||||
// prevent map click
|
||||
DomEvent.stop(e);
|
||||
|
||||
var target = e.layer || e.target;
|
||||
if (this._popup._source === target && !(target instanceof Path)) {
|
||||
// treat it like a marker and figure out
|
||||
// if we should toggle it open/closed
|
||||
if (this._map.hasLayer(this._popup)) {
|
||||
this.closePopup();
|
||||
} else {
|
||||
this.openPopup(e.latlng);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._popup._source = target;
|
||||
this.openPopup(e.latlng);
|
||||
},
|
||||
|
||||
_movePopup: function (e) {
|
||||
this._popup.setLatLng(e.latlng);
|
||||
},
|
||||
|
||||
_onKeyPress: function (e) {
|
||||
if (e.originalEvent.keyCode === 13) {
|
||||
this._openPopup(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import {ImageOverlay} from './ImageOverlay';
|
||||
import * as DomUtil from '../dom/DomUtil';
|
||||
import * as Util from '../core/Util';
|
||||
|
||||
/*
|
||||
* @class SVGOverlay
|
||||
* @aka L.SVGOverlay
|
||||
* @inherits ImageOverlay
|
||||
*
|
||||
* Used to load, display and provide DOM access to an SVG file over specific bounds of the map. Extends `ImageOverlay`.
|
||||
*
|
||||
* An SVG overlay uses the [`<svg>`](https://developer.mozilla.org/docs/Web/SVG/Element/svg) element.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* var svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
* svgElement.setAttribute('xmlns', "http://www.w3.org/2000/svg");
|
||||
* svgElement.setAttribute('viewBox', "0 0 200 200");
|
||||
* svgElement.innerHTML = '<rect width="200" height="200"/><rect x="75" y="23" width="50" height="50" style="fill:red"/><rect x="75" y="123" width="50" height="50" style="fill:#0013ff"/>';
|
||||
* var svgElementBounds = [ [ 32, -130 ], [ 13, -100 ] ];
|
||||
* L.svgOverlay(svgElement, svgElementBounds).addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var SVGOverlay = ImageOverlay.extend({
|
||||
_initImage: function () {
|
||||
var el = this._image = this._url;
|
||||
|
||||
DomUtil.addClass(el, 'leaflet-image-layer');
|
||||
if (this._zoomAnimated) { DomUtil.addClass(el, 'leaflet-zoom-animated'); }
|
||||
if (this.options.className) { DomUtil.addClass(el, this.options.className); }
|
||||
|
||||
el.onselectstart = Util.falseFn;
|
||||
el.onmousemove = Util.falseFn;
|
||||
}
|
||||
|
||||
// @method getElement(): SVGElement
|
||||
// Returns the instance of [`SVGElement`](https://developer.mozilla.org/docs/Web/API/SVGElement)
|
||||
// used by this overlay.
|
||||
});
|
||||
|
||||
|
||||
// @factory L.svgOverlay(svg: String|SVGElement, bounds: LatLngBounds, options?: SVGOverlay options)
|
||||
// Instantiates an image overlay object given an SVG element and the geographical bounds it is tied to.
|
||||
// A viewBox attribute is required on the SVG element to zoom in and out properly.
|
||||
|
||||
export function svgOverlay(el, bounds, options) {
|
||||
return new SVGOverlay(el, bounds, options);
|
||||
}
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
import {DivOverlay} from './DivOverlay';
|
||||
import {toPoint} from '../geometry/Point';
|
||||
import {Map} from '../map/Map';
|
||||
import {Layer} from './Layer';
|
||||
import * as DomUtil from '../dom/DomUtil';
|
||||
import * as DomEvent from '../dom/DomEvent';
|
||||
import * as Util from '../core/Util';
|
||||
import {FeatureGroup} from './FeatureGroup';
|
||||
|
||||
/*
|
||||
* @class Tooltip
|
||||
* @inherits DivOverlay
|
||||
* @aka L.Tooltip
|
||||
* Used to display small texts on top of map layers.
|
||||
*
|
||||
* @example
|
||||
* If you want to just bind a tooltip to marker:
|
||||
*
|
||||
* ```js
|
||||
* marker.bindTooltip("my tooltip text").openTooltip();
|
||||
* ```
|
||||
* Path overlays like polylines also have a `bindTooltip` method.
|
||||
*
|
||||
* A tooltip can be also standalone:
|
||||
*
|
||||
* ```js
|
||||
* var tooltip = L.tooltip()
|
||||
* .setLatLng(latlng)
|
||||
* .setContent('Hello world!<br />This is a nice tooltip.')
|
||||
* .addTo(map);
|
||||
* ```
|
||||
* or
|
||||
* ```js
|
||||
* var tooltip = L.tooltip(latlng, {content: 'Hello world!<br />This is a nice tooltip.'})
|
||||
* .addTo(map);
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* 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
|
||||
export 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 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) {
|
||||
this.addEventParent(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) {
|
||||
this.removeEventParent(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 (!this.options.permanent) {
|
||||
events.preclick = this.close;
|
||||
}
|
||||
|
||||
return events;
|
||||
},
|
||||
|
||||
_initLayout: function () {
|
||||
var prefix = 'leaflet-tooltip',
|
||||
className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
|
||||
|
||||
this._contentNode = this._container = DomUtil.create('div', className);
|
||||
|
||||
this._container.setAttribute('role', 'tooltip');
|
||||
this._container.setAttribute('id', 'leaflet-tooltip-' + Util.stamp(this));
|
||||
},
|
||||
|
||||
_updateLayout: function () {},
|
||||
|
||||
_adjustPan: function () {},
|
||||
|
||||
_setPosition: function (pos) {
|
||||
var subX, subY,
|
||||
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') {
|
||||
subX = tooltipWidth / 2;
|
||||
subY = tooltipHeight;
|
||||
} else if (direction === 'bottom') {
|
||||
subX = tooltipWidth / 2;
|
||||
subY = 0;
|
||||
} else if (direction === 'center') {
|
||||
subX = tooltipWidth / 2;
|
||||
subY = tooltipHeight / 2;
|
||||
} else if (direction === 'right') {
|
||||
subX = 0;
|
||||
subY = tooltipHeight / 2;
|
||||
} else if (direction === 'left') {
|
||||
subX = tooltipWidth;
|
||||
subY = tooltipHeight / 2;
|
||||
} else if (tooltipPoint.x < centerPoint.x) {
|
||||
direction = 'right';
|
||||
subX = 0;
|
||||
subY = tooltipHeight / 2;
|
||||
} else {
|
||||
direction = 'left';
|
||||
subX = tooltipWidth + (offset.x + anchor.x) * 2;
|
||||
subY = tooltipHeight / 2;
|
||||
}
|
||||
|
||||
pos = pos.subtract(toPoint(subX, subY, true)).add(offset).add(anchor);
|
||||
|
||||
DomUtil.removeClass(container, 'leaflet-tooltip-right');
|
||||
DomUtil.removeClass(container, 'leaflet-tooltip-left');
|
||||
DomUtil.removeClass(container, 'leaflet-tooltip-top');
|
||||
DomUtil.removeClass(container, 'leaflet-tooltip-bottom');
|
||||
DomUtil.addClass(container, 'leaflet-tooltip-' + direction);
|
||||
DomUtil.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) {
|
||||
DomUtil.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.
|
||||
// @alternative
|
||||
// @factory L.tooltip(latlng: LatLng, options?: Tooltip options)
|
||||
// Instantiates a `Tooltip` object given `latlng` where the tooltip will open and an optional `options` object that describes its appearance and location.
|
||||
export 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) {
|
||||
this._initOverlay(Tooltip, tooltip, latlng, options)
|
||||
.openOn(this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method closeTooltip(tooltip: Tooltip): this
|
||||
// Closes the tooltip given as parameter.
|
||||
closeTooltip: function (tooltip) {
|
||||
tooltip.close();
|
||||
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 (this._tooltip && this.isTooltipOpen()) {
|
||||
this.unbindTooltip();
|
||||
}
|
||||
|
||||
this._tooltip = this._initOverlay(Tooltip, this._tooltip, content, options);
|
||||
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) {
|
||||
if (!remove && this._tooltipHandlersAdded) { return; }
|
||||
var onOff = remove ? 'off' : 'on',
|
||||
events = {
|
||||
remove: this.closeTooltip,
|
||||
move: this._moveTooltip
|
||||
};
|
||||
if (!this._tooltip.options.permanent) {
|
||||
events.mouseover = this._openTooltip;
|
||||
events.mouseout = this.closeTooltip;
|
||||
events.click = this._openTooltip;
|
||||
if (this._map) {
|
||||
this._addFocusListeners();
|
||||
} else {
|
||||
events.add = this._addFocusListeners;
|
||||
}
|
||||
} else {
|
||||
events.add = this._openTooltip;
|
||||
}
|
||||
if (this._tooltip.options.sticky) {
|
||||
events.mousemove = this._moveTooltip;
|
||||
}
|
||||
this[onOff](events);
|
||||
this._tooltipHandlersAdded = !remove;
|
||||
},
|
||||
|
||||
// @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 (latlng) {
|
||||
if (this._tooltip) {
|
||||
if (!(this instanceof FeatureGroup)) {
|
||||
this._tooltip._source = this;
|
||||
}
|
||||
if (this._tooltip._prepareOpen(latlng)) {
|
||||
// open the tooltip on the map
|
||||
this._tooltip.openOn(this._map);
|
||||
|
||||
if (this.getElement) {
|
||||
this._setAriaDescribedByOnLayer(this);
|
||||
} else if (this.eachLayer) {
|
||||
this.eachLayer(this._setAriaDescribedByOnLayer, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method closeTooltip(): this
|
||||
// Closes the tooltip bound to this layer if it is open.
|
||||
closeTooltip: function () {
|
||||
if (this._tooltip) {
|
||||
return this._tooltip.close();
|
||||
}
|
||||
},
|
||||
|
||||
// @method toggleTooltip(): this
|
||||
// Opens or closes the tooltip bound to this layer depending on its current state.
|
||||
toggleTooltip: function () {
|
||||
if (this._tooltip) {
|
||||
this._tooltip.toggle(this);
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
_addFocusListeners: function () {
|
||||
if (this.getElement) {
|
||||
this._addFocusListenersOnLayer(this);
|
||||
} else if (this.eachLayer) {
|
||||
this.eachLayer(this._addFocusListenersOnLayer, this);
|
||||
}
|
||||
},
|
||||
|
||||
_addFocusListenersOnLayer: function (layer) {
|
||||
var el = typeof layer.getElement === 'function' && layer.getElement();
|
||||
if (el) {
|
||||
DomEvent.on(el, 'focus', function () {
|
||||
this._tooltip._source = layer;
|
||||
this.openTooltip();
|
||||
}, this);
|
||||
DomEvent.on(el, 'blur', this.closeTooltip, this);
|
||||
}
|
||||
},
|
||||
|
||||
_setAriaDescribedByOnLayer: function (layer) {
|
||||
var el = typeof layer.getElement === 'function' && layer.getElement();
|
||||
if (el) {
|
||||
el.setAttribute('aria-describedby', this._tooltip._container.id);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
_openTooltip: function (e) {
|
||||
if (!this._tooltip || !this._map) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the map is moving, we will show the tooltip after it's done.
|
||||
if (this._map.dragging && this._map.dragging.moving() && !this._openOnceFlag) {
|
||||
this._openOnceFlag = true;
|
||||
var that = this;
|
||||
this._map.once('moveend', function () {
|
||||
that._openOnceFlag = false;
|
||||
that._openTooltip(e);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._tooltip._source = e.layer || e.target;
|
||||
|
||||
this.openTooltip(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);
|
||||
}
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import {ImageOverlay} from './ImageOverlay';
|
||||
import * as DomUtil from '../dom/DomUtil';
|
||||
import * as Util from '../core/Util';
|
||||
|
||||
/*
|
||||
* @class VideoOverlay
|
||||
* @aka L.VideoOverlay
|
||||
* @inherits ImageOverlay
|
||||
*
|
||||
* Used to load and display a video player over specific bounds of the map. Extends `ImageOverlay`.
|
||||
*
|
||||
* A video overlay uses the [`<video>`](https://developer.mozilla.org/docs/Web/HTML/Element/video)
|
||||
* HTML5 element.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* var videoUrl = 'https://www.mapbox.com/bites/00188/patricia_nasa.webm',
|
||||
* videoBounds = [[ 32, -130], [ 13, -100]];
|
||||
* L.videoOverlay(videoUrl, videoBounds ).addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var VideoOverlay = ImageOverlay.extend({
|
||||
|
||||
// @section
|
||||
// @aka VideoOverlay options
|
||||
options: {
|
||||
// @option autoplay: Boolean = true
|
||||
// Whether the video starts playing automatically when loaded.
|
||||
// On some browsers autoplay will only work with `muted: true`
|
||||
autoplay: true,
|
||||
|
||||
// @option loop: Boolean = true
|
||||
// Whether the video will loop back to the beginning when played.
|
||||
loop: true,
|
||||
|
||||
// @option keepAspectRatio: Boolean = true
|
||||
// Whether the video will save aspect ratio after the projection.
|
||||
// Relevant for supported browsers. See [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)
|
||||
keepAspectRatio: true,
|
||||
|
||||
// @option muted: Boolean = false
|
||||
// Whether the video starts on mute when loaded.
|
||||
muted: false,
|
||||
|
||||
// @option playsInline: Boolean = true
|
||||
// Mobile browsers will play the video right where it is instead of open it up in fullscreen mode.
|
||||
playsInline: true
|
||||
},
|
||||
|
||||
_initImage: function () {
|
||||
var wasElementSupplied = this._url.tagName === 'VIDEO';
|
||||
var vid = this._image = wasElementSupplied ? this._url : DomUtil.create('video');
|
||||
|
||||
DomUtil.addClass(vid, 'leaflet-image-layer');
|
||||
if (this._zoomAnimated) { DomUtil.addClass(vid, 'leaflet-zoom-animated'); }
|
||||
if (this.options.className) { DomUtil.addClass(vid, this.options.className); }
|
||||
|
||||
vid.onselectstart = Util.falseFn;
|
||||
vid.onmousemove = Util.falseFn;
|
||||
|
||||
// @event load: Event
|
||||
// Fired when the video has finished loading the first frame
|
||||
vid.onloadeddata = Util.bind(this.fire, this, 'load');
|
||||
|
||||
if (wasElementSupplied) {
|
||||
var sourceElements = vid.getElementsByTagName('source');
|
||||
var sources = [];
|
||||
for (var j = 0; j < sourceElements.length; j++) {
|
||||
sources.push(sourceElements[j].src);
|
||||
}
|
||||
|
||||
this._url = (sourceElements.length > 0) ? sources : [vid.src];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Util.isArray(this._url)) { this._url = [this._url]; }
|
||||
|
||||
if (!this.options.keepAspectRatio && Object.prototype.hasOwnProperty.call(vid.style, 'objectFit')) {
|
||||
vid.style['objectFit'] = 'fill';
|
||||
}
|
||||
vid.autoplay = !!this.options.autoplay;
|
||||
vid.loop = !!this.options.loop;
|
||||
vid.muted = !!this.options.muted;
|
||||
vid.playsInline = !!this.options.playsInline;
|
||||
for (var i = 0; i < this._url.length; i++) {
|
||||
var source = DomUtil.create('source');
|
||||
source.src = this._url[i];
|
||||
vid.appendChild(source);
|
||||
}
|
||||
}
|
||||
|
||||
// @method getElement(): HTMLVideoElement
|
||||
// Returns the instance of [`HTMLVideoElement`](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement)
|
||||
// used by this overlay.
|
||||
});
|
||||
|
||||
|
||||
// @factory L.videoOverlay(video: String|Array|HTMLVideoElement, bounds: LatLngBounds, options?: VideoOverlay options)
|
||||
// Instantiates an image overlay object given the URL of the video (or array of URLs, or even a video element) and the
|
||||
// geographical bounds it is tied to.
|
||||
|
||||
export function videoOverlay(video, bounds, options) {
|
||||
return new VideoOverlay(video, bounds, options);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
export {Layer} from './Layer';
|
||||
export {LayerGroup, layerGroup} from './LayerGroup';
|
||||
export {FeatureGroup, featureGroup} from './FeatureGroup';
|
||||
import {GeoJSON, geoJSON, geoJson, geometryToLayer, coordsToLatLng, coordsToLatLngs, latLngToCoords, latLngsToCoords, getFeature, asFeature} from './GeoJSON';
|
||||
GeoJSON.geometryToLayer = geometryToLayer;
|
||||
GeoJSON.coordsToLatLng = coordsToLatLng;
|
||||
GeoJSON.coordsToLatLngs = coordsToLatLngs;
|
||||
GeoJSON.latLngToCoords = latLngToCoords;
|
||||
GeoJSON.latLngsToCoords = latLngsToCoords;
|
||||
GeoJSON.getFeature = getFeature;
|
||||
GeoJSON.asFeature = asFeature;
|
||||
export {GeoJSON, geoJSON, geoJson};
|
||||
|
||||
export {ImageOverlay, imageOverlay} from './ImageOverlay';
|
||||
export {VideoOverlay, videoOverlay} from './VideoOverlay';
|
||||
export {SVGOverlay, svgOverlay} from './SVGOverlay';
|
||||
|
||||
export {DivOverlay} from './DivOverlay';
|
||||
export {Popup, popup} from './Popup';
|
||||
export {Tooltip, tooltip} from './Tooltip';
|
||||
|
||||
export * from './marker/index';
|
||||
export * from './tile/index';
|
||||
export * from './vector/index';
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import {Icon} from './Icon';
|
||||
import {toPoint as point} from '../../geometry/Point';
|
||||
import {empty} from '../../dom/DomUtil';
|
||||
|
||||
/*
|
||||
* @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.
|
||||
*/
|
||||
|
||||
export 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|HTMLElement = ''
|
||||
// Custom HTML code to put inside the div element, empty by default. Alternatively,
|
||||
// an instance of `HTMLElement`.
|
||||
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;
|
||||
|
||||
if (options.html instanceof Element) {
|
||||
empty(div);
|
||||
div.appendChild(options.html);
|
||||
} else {
|
||||
div.innerHTML = options.html !== false ? options.html : '';
|
||||
}
|
||||
|
||||
if (options.bgPos) {
|
||||
var bgPos = point(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.
|
||||
export function divIcon(options) {
|
||||
return new DivIcon(options);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import {Icon} from './Icon';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
|
||||
/*
|
||||
* @miniclass Icon.Default (Icon)
|
||||
* @aka L.Icon.Default
|
||||
* @section
|
||||
*
|
||||
* A trivial subclass of `Icon`, represents the icon to use in `Marker`s when
|
||||
* no icon is specified. Points to the blue marker image distributed with Leaflet
|
||||
* releases.
|
||||
*
|
||||
* In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options`
|
||||
* (which is a set of `Icon options`).
|
||||
*
|
||||
* If you want to _completely_ replace the default icon, override the
|
||||
* `L.Marker.prototype.options.icon` with your own icon instead.
|
||||
*/
|
||||
|
||||
export var IconDefault = Icon.extend({
|
||||
|
||||
options: {
|
||||
iconUrl: 'marker-icon.png',
|
||||
iconRetinaUrl: 'marker-icon-2x.png',
|
||||
shadowUrl: 'marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
tooltipAnchor: [16, -28],
|
||||
shadowSize: [41, 41]
|
||||
},
|
||||
|
||||
_getIconUrl: function (name) {
|
||||
if (typeof IconDefault.imagePath !== 'string') { // Deprecated, backwards-compatibility only
|
||||
IconDefault.imagePath = this._detectIconPath();
|
||||
}
|
||||
|
||||
// @option imagePath: String
|
||||
// `Icon.Default` will try to auto-detect the location of the
|
||||
// blue icon images. If you are placing these images in a non-standard
|
||||
// way, set this option to point to the right path.
|
||||
return (this.options.imagePath || IconDefault.imagePath) + Icon.prototype._getIconUrl.call(this, name);
|
||||
},
|
||||
|
||||
_stripUrl: function (path) { // separate function to use in tests
|
||||
var strip = function (str, re, idx) {
|
||||
var match = re.exec(str);
|
||||
return match && match[idx];
|
||||
};
|
||||
path = strip(path, /^url\((['"])?(.+)\1\)$/, 2);
|
||||
return path && strip(path, /^(.*)marker-icon\.png$/, 1);
|
||||
},
|
||||
|
||||
_detectIconPath: function () {
|
||||
var el = DomUtil.create('div', 'leaflet-default-icon-path', document.body);
|
||||
var path = DomUtil.getStyle(el, 'background-image') ||
|
||||
DomUtil.getStyle(el, 'backgroundImage'); // IE8
|
||||
|
||||
document.body.removeChild(el);
|
||||
path = this._stripUrl(path);
|
||||
if (path) { return path; }
|
||||
var link = document.querySelector('link[href$="leaflet.css"]');
|
||||
if (!link) { return ''; }
|
||||
return link.href.substring(0, link.href.length - 'leaflet.css'.length - 1);
|
||||
}
|
||||
});
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import {Class} from '../../core/Class';
|
||||
import {setOptions} from '../../core/Util';
|
||||
import {toPoint as point} from '../../geometry/Point';
|
||||
import Browser from '../../core/Browser';
|
||||
|
||||
/*
|
||||
* @class Icon
|
||||
* @aka L.Icon
|
||||
*
|
||||
* Represents an icon to provide when creating a marker.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* var myIcon = L.icon({
|
||||
* iconUrl: 'my-icon.png',
|
||||
* iconRetinaUrl: 'my-icon@2x.png',
|
||||
* iconSize: [38, 95],
|
||||
* iconAnchor: [22, 94],
|
||||
* popupAnchor: [-3, -76],
|
||||
* shadowUrl: 'my-icon-shadow.png',
|
||||
* shadowRetinaUrl: 'my-icon-shadow@2x.png',
|
||||
* shadowSize: [68, 95],
|
||||
* shadowAnchor: [22, 94]
|
||||
* });
|
||||
*
|
||||
* L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
|
||||
* ```
|
||||
*
|
||||
* `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default.
|
||||
*
|
||||
*/
|
||||
|
||||
export var Icon = Class.extend({
|
||||
|
||||
/* @section
|
||||
* @aka Icon options
|
||||
*
|
||||
* @option iconUrl: String = null
|
||||
* **(required)** The URL to the icon image (absolute or relative to your script path).
|
||||
*
|
||||
* @option iconRetinaUrl: String = null
|
||||
* The URL to a retina sized version of the icon image (absolute or relative to your
|
||||
* script path). Used for Retina screen devices.
|
||||
*
|
||||
* @option iconSize: Point = null
|
||||
* Size of the icon image in pixels.
|
||||
*
|
||||
* @option iconAnchor: Point = null
|
||||
* The coordinates of the "tip" of the icon (relative to its top left corner). The icon
|
||||
* will be aligned so that this point is at the marker's geographical location. Centered
|
||||
* by default if size is specified, also can be set in CSS with negative margins.
|
||||
*
|
||||
* @option popupAnchor: Point = [0, 0]
|
||||
* The coordinates of the point from which popups will "open", relative to the icon anchor.
|
||||
*
|
||||
* @option tooltipAnchor: Point = [0, 0]
|
||||
* The coordinates of the point from which tooltips will "open", relative to the icon anchor.
|
||||
*
|
||||
* @option shadowUrl: String = null
|
||||
* The URL to the icon shadow image. If not specified, no shadow image will be created.
|
||||
*
|
||||
* @option shadowRetinaUrl: String = null
|
||||
*
|
||||
* @option shadowSize: Point = null
|
||||
* Size of the shadow image in pixels.
|
||||
*
|
||||
* @option shadowAnchor: Point = null
|
||||
* The coordinates of the "tip" of the shadow (relative to its top left corner) (the same
|
||||
* as iconAnchor if not specified).
|
||||
*
|
||||
* @option className: String = ''
|
||||
* A custom class name to assign to both icon and shadow images. Empty by default.
|
||||
*/
|
||||
|
||||
options: {
|
||||
popupAnchor: [0, 0],
|
||||
tooltipAnchor: [0, 0],
|
||||
|
||||
// @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 (options) {
|
||||
setOptions(this, options);
|
||||
},
|
||||
|
||||
// @method createIcon(oldIcon?: HTMLElement): HTMLElement
|
||||
// Called internally when the icon has to be shown, returns a `<img>` HTML element
|
||||
// styled according to the options.
|
||||
createIcon: function (oldIcon) {
|
||||
return this._createIcon('icon', oldIcon);
|
||||
},
|
||||
|
||||
// @method createShadow(oldIcon?: HTMLElement): HTMLElement
|
||||
// As `createIcon`, but for the shadow beneath it.
|
||||
createShadow: function (oldIcon) {
|
||||
return this._createIcon('shadow', oldIcon);
|
||||
},
|
||||
|
||||
_createIcon: function (name, oldIcon) {
|
||||
var src = this._getIconUrl(name);
|
||||
|
||||
if (!src) {
|
||||
if (name === 'icon') {
|
||||
throw new Error('iconUrl not set in Icon options (see the docs).');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null);
|
||||
this._setIconStyles(img, name);
|
||||
|
||||
if (this.options.crossOrigin || this.options.crossOrigin === '') {
|
||||
img.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
|
||||
}
|
||||
|
||||
return img;
|
||||
},
|
||||
|
||||
_setIconStyles: function (img, name) {
|
||||
var options = this.options;
|
||||
var sizeOption = options[name + 'Size'];
|
||||
|
||||
if (typeof sizeOption === 'number') {
|
||||
sizeOption = [sizeOption, sizeOption];
|
||||
}
|
||||
|
||||
var size = point(sizeOption),
|
||||
anchor = point(name === 'shadow' && options.shadowAnchor || options.iconAnchor ||
|
||||
size && size.divideBy(2, true));
|
||||
|
||||
img.className = 'leaflet-marker-' + name + ' ' + (options.className || '');
|
||||
|
||||
if (anchor) {
|
||||
img.style.marginLeft = (-anchor.x) + 'px';
|
||||
img.style.marginTop = (-anchor.y) + 'px';
|
||||
}
|
||||
|
||||
if (size) {
|
||||
img.style.width = size.x + 'px';
|
||||
img.style.height = size.y + 'px';
|
||||
}
|
||||
},
|
||||
|
||||
_createImg: function (src, el) {
|
||||
el = el || document.createElement('img');
|
||||
el.src = src;
|
||||
return el;
|
||||
},
|
||||
|
||||
_getIconUrl: function (name) {
|
||||
return Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url'];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// @factory L.icon(options: Icon options)
|
||||
// Creates an icon instance with the given options.
|
||||
export function icon(options) {
|
||||
return new Icon(options);
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import {Handler} from '../../core/Handler';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
import {Draggable} from '../../dom/Draggable';
|
||||
import {toBounds} from '../../geometry/Bounds';
|
||||
import {toPoint} from '../../geometry/Point';
|
||||
import {requestAnimFrame, cancelAnimFrame} from '../../core/Util';
|
||||
|
||||
/*
|
||||
* L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
|
||||
*/
|
||||
|
||||
|
||||
/* @namespace Marker
|
||||
* @section Interaction handlers
|
||||
*
|
||||
* Interaction handlers are properties of a marker instance that allow you to control interaction behavior in runtime, enabling or disabling certain features such as dragging (see `Handler` methods). Example:
|
||||
*
|
||||
* ```js
|
||||
* marker.dragging.disable();
|
||||
* ```
|
||||
*
|
||||
* @property dragging: Handler
|
||||
* Marker dragging handler (by both mouse and touch). Only valid when the marker is on the map (Otherwise set [`marker.options.draggable`](#marker-draggable)).
|
||||
*/
|
||||
|
||||
export var MarkerDrag = Handler.extend({
|
||||
initialize: function (marker) {
|
||||
this._marker = marker;
|
||||
},
|
||||
|
||||
addHooks: function () {
|
||||
var icon = this._marker._icon;
|
||||
|
||||
if (!this._draggable) {
|
||||
this._draggable = new Draggable(icon, icon, true);
|
||||
}
|
||||
|
||||
this._draggable.on({
|
||||
dragstart: this._onDragStart,
|
||||
predrag: this._onPreDrag,
|
||||
drag: this._onDrag,
|
||||
dragend: this._onDragEnd
|
||||
}, this).enable();
|
||||
|
||||
DomUtil.addClass(icon, 'leaflet-marker-draggable');
|
||||
},
|
||||
|
||||
removeHooks: function () {
|
||||
this._draggable.off({
|
||||
dragstart: this._onDragStart,
|
||||
predrag: this._onPreDrag,
|
||||
drag: this._onDrag,
|
||||
dragend: this._onDragEnd
|
||||
}, this).disable();
|
||||
|
||||
if (this._marker._icon) {
|
||||
DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable');
|
||||
}
|
||||
},
|
||||
|
||||
moved: function () {
|
||||
return this._draggable && this._draggable._moved;
|
||||
},
|
||||
|
||||
_adjustPan: function (e) {
|
||||
var marker = this._marker,
|
||||
map = marker._map,
|
||||
speed = this._marker.options.autoPanSpeed,
|
||||
padding = this._marker.options.autoPanPadding,
|
||||
iconPos = DomUtil.getPosition(marker._icon),
|
||||
bounds = map.getPixelBounds(),
|
||||
origin = map.getPixelOrigin();
|
||||
|
||||
var panBounds = toBounds(
|
||||
bounds.min._subtract(origin).add(padding),
|
||||
bounds.max._subtract(origin).subtract(padding)
|
||||
);
|
||||
|
||||
if (!panBounds.contains(iconPos)) {
|
||||
// Compute incremental movement
|
||||
var movement = toPoint(
|
||||
(Math.max(panBounds.max.x, iconPos.x) - panBounds.max.x) / (bounds.max.x - panBounds.max.x) -
|
||||
(Math.min(panBounds.min.x, iconPos.x) - panBounds.min.x) / (bounds.min.x - panBounds.min.x),
|
||||
|
||||
(Math.max(panBounds.max.y, iconPos.y) - panBounds.max.y) / (bounds.max.y - panBounds.max.y) -
|
||||
(Math.min(panBounds.min.y, iconPos.y) - panBounds.min.y) / (bounds.min.y - panBounds.min.y)
|
||||
).multiplyBy(speed);
|
||||
|
||||
map.panBy(movement, {animate: false});
|
||||
|
||||
this._draggable._newPos._add(movement);
|
||||
this._draggable._startPos._add(movement);
|
||||
|
||||
DomUtil.setPosition(marker._icon, this._draggable._newPos);
|
||||
this._onDrag(e);
|
||||
|
||||
this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e));
|
||||
}
|
||||
},
|
||||
|
||||
_onDragStart: function () {
|
||||
// @section Dragging events
|
||||
// @event dragstart: Event
|
||||
// Fired when the user starts dragging the marker.
|
||||
|
||||
// @event movestart: Event
|
||||
// Fired when the marker starts moving (because of dragging).
|
||||
|
||||
this._oldLatLng = this._marker.getLatLng();
|
||||
|
||||
// When using ES6 imports it could not be set when `Popup` was not imported as well
|
||||
this._marker.closePopup && this._marker.closePopup();
|
||||
|
||||
this._marker
|
||||
.fire('movestart')
|
||||
.fire('dragstart');
|
||||
},
|
||||
|
||||
_onPreDrag: function (e) {
|
||||
if (this._marker.options.autoPan) {
|
||||
cancelAnimFrame(this._panRequest);
|
||||
this._panRequest = requestAnimFrame(this._adjustPan.bind(this, e));
|
||||
}
|
||||
},
|
||||
|
||||
_onDrag: function (e) {
|
||||
var marker = this._marker,
|
||||
shadow = marker._shadow,
|
||||
iconPos = DomUtil.getPosition(marker._icon),
|
||||
latlng = marker._map.layerPointToLatLng(iconPos);
|
||||
|
||||
// update shadow position
|
||||
if (shadow) {
|
||||
DomUtil.setPosition(shadow, iconPos);
|
||||
}
|
||||
|
||||
marker._latlng = latlng;
|
||||
e.latlng = latlng;
|
||||
e.oldLatLng = this._oldLatLng;
|
||||
|
||||
// @event drag: Event
|
||||
// Fired repeatedly while the user drags the marker.
|
||||
marker
|
||||
.fire('move', e)
|
||||
.fire('drag', e);
|
||||
},
|
||||
|
||||
_onDragEnd: function (e) {
|
||||
// @event dragend: DragEndEvent
|
||||
// Fired when the user stops dragging the marker.
|
||||
|
||||
cancelAnimFrame(this._panRequest);
|
||||
|
||||
// @event moveend: Event
|
||||
// Fired when the marker stops moving (because of dragging).
|
||||
delete this._oldLatLng;
|
||||
this._marker
|
||||
.fire('moveend')
|
||||
.fire('dragend', e);
|
||||
}
|
||||
});
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
import {Layer} from '../Layer';
|
||||
import {IconDefault} from './Icon.Default';
|
||||
import * as Util from '../../core/Util';
|
||||
import {toLatLng as latLng} from '../../geo/LatLng';
|
||||
import {toPoint as point} from '../../geometry/Point';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
import * as DomEvent from '../../dom/DomEvent';
|
||||
import {MarkerDrag} from './Marker.Drag';
|
||||
|
||||
/*
|
||||
* @class Marker
|
||||
* @inherits Interactive layer
|
||||
* @aka L.Marker
|
||||
* L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* L.marker([50.5, 30.5]).addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var Marker = Layer.extend({
|
||||
|
||||
// @section
|
||||
// @aka Marker options
|
||||
options: {
|
||||
// @option icon: Icon = *
|
||||
// Icon instance to use for rendering the marker.
|
||||
// See [Icon documentation](#L.Icon) for details on how to customize the marker icon.
|
||||
// If not specified, a common instance of `L.Icon.Default` is used.
|
||||
icon: new IconDefault(),
|
||||
|
||||
// Option inherited from "Interactive layer" abstract class
|
||||
interactive: true,
|
||||
|
||||
// @option keyboard: Boolean = true
|
||||
// Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
|
||||
keyboard: true,
|
||||
|
||||
// @option title: String = ''
|
||||
// Text for the browser tooltip that appear on marker hover (no tooltip by default).
|
||||
// [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
|
||||
title: '',
|
||||
|
||||
// @option alt: String = 'Marker'
|
||||
// Text for the `alt` attribute of the icon image.
|
||||
// [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
|
||||
alt: 'Marker',
|
||||
|
||||
// @option zIndexOffset: Number = 0
|
||||
// By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively).
|
||||
zIndexOffset: 0,
|
||||
|
||||
// @option opacity: Number = 1.0
|
||||
// The opacity of the marker.
|
||||
opacity: 1,
|
||||
|
||||
// @option riseOnHover: Boolean = false
|
||||
// If `true`, the marker will get on top of others when you hover the mouse over it.
|
||||
riseOnHover: false,
|
||||
|
||||
// @option riseOffset: Number = 250
|
||||
// The z-index offset used for the `riseOnHover` feature.
|
||||
riseOffset: 250,
|
||||
|
||||
// @option pane: String = 'markerPane'
|
||||
// `Map pane` where the markers icon will be added.
|
||||
pane: 'markerPane',
|
||||
|
||||
// @option shadowPane: String = 'shadowPane'
|
||||
// `Map pane` where the markers shadow will be added.
|
||||
shadowPane: 'shadowPane',
|
||||
|
||||
// @option bubblingMouseEvents: Boolean = false
|
||||
// When `true`, a mouse event on this marker will trigger the same event on the map
|
||||
// (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
|
||||
bubblingMouseEvents: false,
|
||||
|
||||
// @option autoPanOnFocus: Boolean = true
|
||||
// When `true`, the map will pan whenever the marker is focused (via
|
||||
// e.g. pressing `tab` on the keyboard) to ensure the marker is
|
||||
// visible within the map's bounds
|
||||
autoPanOnFocus: true,
|
||||
|
||||
// @section Draggable marker options
|
||||
// @option draggable: Boolean = false
|
||||
// Whether the marker is draggable with mouse/touch or not.
|
||||
draggable: false,
|
||||
|
||||
// @option autoPan: Boolean = false
|
||||
// Whether to pan the map when dragging this marker near its edge or not.
|
||||
autoPan: false,
|
||||
|
||||
// @option autoPanPadding: Point = Point(50, 50)
|
||||
// Distance (in pixels to the left/right and to the top/bottom) of the
|
||||
// map edge to start panning the map.
|
||||
autoPanPadding: [50, 50],
|
||||
|
||||
// @option autoPanSpeed: Number = 10
|
||||
// Number of pixels the map should pan by.
|
||||
autoPanSpeed: 10
|
||||
},
|
||||
|
||||
/* @section
|
||||
*
|
||||
* In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
|
||||
*/
|
||||
|
||||
initialize: function (latlng, options) {
|
||||
Util.setOptions(this, options);
|
||||
this._latlng = latLng(latlng);
|
||||
},
|
||||
|
||||
onAdd: function (map) {
|
||||
this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;
|
||||
|
||||
if (this._zoomAnimated) {
|
||||
map.on('zoomanim', this._animateZoom, this);
|
||||
}
|
||||
|
||||
this._initIcon();
|
||||
this.update();
|
||||
},
|
||||
|
||||
onRemove: function (map) {
|
||||
if (this.dragging && this.dragging.enabled()) {
|
||||
this.options.draggable = true;
|
||||
this.dragging.removeHooks();
|
||||
}
|
||||
delete this.dragging;
|
||||
|
||||
if (this._zoomAnimated) {
|
||||
map.off('zoomanim', this._animateZoom, this);
|
||||
}
|
||||
|
||||
this._removeIcon();
|
||||
this._removeShadow();
|
||||
},
|
||||
|
||||
getEvents: function () {
|
||||
return {
|
||||
zoom: this.update,
|
||||
viewreset: this.update
|
||||
};
|
||||
},
|
||||
|
||||
// @method getLatLng: LatLng
|
||||
// Returns the current geographical position of the marker.
|
||||
getLatLng: function () {
|
||||
return this._latlng;
|
||||
},
|
||||
|
||||
// @method setLatLng(latlng: LatLng): this
|
||||
// Changes the marker position to the given point.
|
||||
setLatLng: function (latlng) {
|
||||
var oldLatLng = this._latlng;
|
||||
this._latlng = latLng(latlng);
|
||||
this.update();
|
||||
|
||||
// @event move: Event
|
||||
// Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
|
||||
return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
|
||||
},
|
||||
|
||||
// @method setZIndexOffset(offset: Number): this
|
||||
// Changes the [zIndex offset](#marker-zindexoffset) of the marker.
|
||||
setZIndexOffset: function (offset) {
|
||||
this.options.zIndexOffset = offset;
|
||||
return this.update();
|
||||
},
|
||||
|
||||
// @method getIcon: Icon
|
||||
// Returns the current icon used by the marker
|
||||
getIcon: function () {
|
||||
return this.options.icon;
|
||||
},
|
||||
|
||||
// @method setIcon(icon: Icon): this
|
||||
// Changes the marker icon.
|
||||
setIcon: function (icon) {
|
||||
|
||||
this.options.icon = icon;
|
||||
|
||||
if (this._map) {
|
||||
this._initIcon();
|
||||
this.update();
|
||||
}
|
||||
|
||||
if (this._popup) {
|
||||
this.bindPopup(this._popup, this._popup.options);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getElement: function () {
|
||||
return this._icon;
|
||||
},
|
||||
|
||||
update: function () {
|
||||
|
||||
if (this._icon && this._map) {
|
||||
var pos = this._map.latLngToLayerPoint(this._latlng).round();
|
||||
this._setPos(pos);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initIcon: function () {
|
||||
var options = this.options,
|
||||
classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
|
||||
|
||||
var icon = options.icon.createIcon(this._icon),
|
||||
addIcon = false;
|
||||
|
||||
// if we're not reusing the icon, remove the old one and init new one
|
||||
if (icon !== this._icon) {
|
||||
if (this._icon) {
|
||||
this._removeIcon();
|
||||
}
|
||||
addIcon = true;
|
||||
|
||||
if (options.title) {
|
||||
icon.title = options.title;
|
||||
}
|
||||
|
||||
if (icon.tagName === 'IMG') {
|
||||
icon.alt = options.alt || '';
|
||||
}
|
||||
}
|
||||
|
||||
DomUtil.addClass(icon, classToAdd);
|
||||
|
||||
if (options.keyboard) {
|
||||
icon.tabIndex = '0';
|
||||
icon.setAttribute('role', 'button');
|
||||
}
|
||||
|
||||
this._icon = icon;
|
||||
|
||||
if (options.riseOnHover) {
|
||||
this.on({
|
||||
mouseover: this._bringToFront,
|
||||
mouseout: this._resetZIndex
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.autoPanOnFocus) {
|
||||
DomEvent.on(icon, 'focus', this._panOnFocus, this);
|
||||
}
|
||||
|
||||
var newShadow = options.icon.createShadow(this._shadow),
|
||||
addShadow = false;
|
||||
|
||||
if (newShadow !== this._shadow) {
|
||||
this._removeShadow();
|
||||
addShadow = true;
|
||||
}
|
||||
|
||||
if (newShadow) {
|
||||
DomUtil.addClass(newShadow, classToAdd);
|
||||
newShadow.alt = '';
|
||||
}
|
||||
this._shadow = newShadow;
|
||||
|
||||
|
||||
if (options.opacity < 1) {
|
||||
this._updateOpacity();
|
||||
}
|
||||
|
||||
|
||||
if (addIcon) {
|
||||
this.getPane().appendChild(this._icon);
|
||||
}
|
||||
this._initInteraction();
|
||||
if (newShadow && addShadow) {
|
||||
this.getPane(options.shadowPane).appendChild(this._shadow);
|
||||
}
|
||||
},
|
||||
|
||||
_removeIcon: function () {
|
||||
if (this.options.riseOnHover) {
|
||||
this.off({
|
||||
mouseover: this._bringToFront,
|
||||
mouseout: this._resetZIndex
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.autoPanOnFocus) {
|
||||
DomEvent.off(this._icon, 'focus', this._panOnFocus, this);
|
||||
}
|
||||
|
||||
DomUtil.remove(this._icon);
|
||||
this.removeInteractiveTarget(this._icon);
|
||||
|
||||
this._icon = null;
|
||||
},
|
||||
|
||||
_removeShadow: function () {
|
||||
if (this._shadow) {
|
||||
DomUtil.remove(this._shadow);
|
||||
}
|
||||
this._shadow = null;
|
||||
},
|
||||
|
||||
_setPos: function (pos) {
|
||||
|
||||
if (this._icon) {
|
||||
DomUtil.setPosition(this._icon, pos);
|
||||
}
|
||||
|
||||
if (this._shadow) {
|
||||
DomUtil.setPosition(this._shadow, pos);
|
||||
}
|
||||
|
||||
this._zIndex = pos.y + this.options.zIndexOffset;
|
||||
|
||||
this._resetZIndex();
|
||||
},
|
||||
|
||||
_updateZIndex: function (offset) {
|
||||
if (this._icon) {
|
||||
this._icon.style.zIndex = this._zIndex + offset;
|
||||
}
|
||||
},
|
||||
|
||||
_animateZoom: function (opt) {
|
||||
var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
|
||||
|
||||
this._setPos(pos);
|
||||
},
|
||||
|
||||
_initInteraction: function () {
|
||||
|
||||
if (!this.options.interactive) { return; }
|
||||
|
||||
DomUtil.addClass(this._icon, 'leaflet-interactive');
|
||||
|
||||
this.addInteractiveTarget(this._icon);
|
||||
|
||||
if (MarkerDrag) {
|
||||
var draggable = this.options.draggable;
|
||||
if (this.dragging) {
|
||||
draggable = this.dragging.enabled();
|
||||
this.dragging.disable();
|
||||
}
|
||||
|
||||
this.dragging = new MarkerDrag(this);
|
||||
|
||||
if (draggable) {
|
||||
this.dragging.enable();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// @method setOpacity(opacity: Number): this
|
||||
// Changes the opacity of the marker.
|
||||
setOpacity: function (opacity) {
|
||||
this.options.opacity = opacity;
|
||||
if (this._map) {
|
||||
this._updateOpacity();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_updateOpacity: function () {
|
||||
var opacity = this.options.opacity;
|
||||
|
||||
if (this._icon) {
|
||||
DomUtil.setOpacity(this._icon, opacity);
|
||||
}
|
||||
|
||||
if (this._shadow) {
|
||||
DomUtil.setOpacity(this._shadow, opacity);
|
||||
}
|
||||
},
|
||||
|
||||
_bringToFront: function () {
|
||||
this._updateZIndex(this.options.riseOffset);
|
||||
},
|
||||
|
||||
_resetZIndex: function () {
|
||||
this._updateZIndex(0);
|
||||
},
|
||||
|
||||
_panOnFocus: function () {
|
||||
var map = this._map;
|
||||
if (!map) { return; }
|
||||
|
||||
var iconOpts = this.options.icon.options;
|
||||
var size = iconOpts.iconSize ? point(iconOpts.iconSize) : point(0, 0);
|
||||
var anchor = iconOpts.iconAnchor ? point(iconOpts.iconAnchor) : point(0, 0);
|
||||
|
||||
map.panInside(this._latlng, {
|
||||
paddingTopLeft: anchor,
|
||||
paddingBottomRight: size.subtract(anchor)
|
||||
});
|
||||
},
|
||||
|
||||
_getPopupAnchor: function () {
|
||||
return this.options.icon.options.popupAnchor;
|
||||
},
|
||||
|
||||
_getTooltipAnchor: function () {
|
||||
return this.options.icon.options.tooltipAnchor;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// factory L.marker(latlng: LatLng, options? : Marker options)
|
||||
|
||||
// @factory L.marker(latlng: LatLng, options? : Marker options)
|
||||
// Instantiates a Marker object given a geographical point and optionally an options object.
|
||||
export function marker(latlng, options) {
|
||||
return new Marker(latlng, options);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import {Icon} from './Icon';
|
||||
export {icon} from './Icon';
|
||||
import {IconDefault} from './Icon.Default';
|
||||
Icon.Default = IconDefault;
|
||||
export {Icon};
|
||||
|
||||
export {DivIcon, divIcon} from './DivIcon';
|
||||
export {Marker, marker} from './Marker';
|
||||
+923
@@ -0,0 +1,923 @@
|
||||
import {Layer} from '../Layer';
|
||||
import Browser from '../../core/Browser';
|
||||
import * as Util from '../../core/Util';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
import {Point} from '../../geometry/Point';
|
||||
import {Bounds} from '../../geometry/Bounds';
|
||||
import {LatLngBounds, toLatLngBounds as latLngBounds} from '../../geo/LatLngBounds';
|
||||
|
||||
/*
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
||||
export 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: Browser.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) {
|
||||
Util.setOptions(this, options);
|
||||
},
|
||||
|
||||
onAdd: function () {
|
||||
this._initContainer();
|
||||
|
||||
this._levels = {};
|
||||
this._tiles = {};
|
||||
|
||||
this._resetView(); // implicit _update() call
|
||||
},
|
||||
|
||||
beforeAdd: function (map) {
|
||||
map._addZoomLimit(this);
|
||||
},
|
||||
|
||||
onRemove: function (map) {
|
||||
this._removeAllTiles();
|
||||
DomUtil.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) {
|
||||
DomUtil.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) {
|
||||
DomUtil.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();
|
||||
var tileZoom = this._clampZoom(this._map.getZoom());
|
||||
if (tileZoom !== this._tileZoom) {
|
||||
this._tileZoom = tileZoom;
|
||||
this._updateLevels();
|
||||
}
|
||||
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 = Util.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 (Browser.ielt9) { return; }
|
||||
|
||||
DomUtil.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);
|
||||
|
||||
DomUtil.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) {
|
||||
Util.cancelAnimFrame(this._fadeFrame);
|
||||
this._fadeFrame = Util.requestAnimFrame(this._updateOpacity, this);
|
||||
}
|
||||
},
|
||||
|
||||
_onOpaqueTile: Util.falseFn,
|
||||
|
||||
_initContainer: function () {
|
||||
if (this._container) { return; }
|
||||
|
||||
this._container = DomUtil.create('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) {
|
||||
z = Number(z);
|
||||
if (this._levels[z].el.children.length || z === zoom) {
|
||||
this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
|
||||
this._onUpdateLevel(z);
|
||||
} else {
|
||||
DomUtil.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 = DomUtil.create('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
|
||||
Util.falseFn(level.el.offsetWidth);
|
||||
|
||||
this._onCreateLevel(level);
|
||||
}
|
||||
|
||||
this._level = level;
|
||||
|
||||
return level;
|
||||
},
|
||||
|
||||
_onUpdateLevel: Util.falseFn,
|
||||
|
||||
_onRemoveLevel: Util.falseFn,
|
||||
|
||||
_onCreateLevel: Util.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) {
|
||||
DomUtil.remove(this._levels[z].el);
|
||||
this._onRemoveLevel(Number(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 = Math.round(zoom);
|
||||
if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
|
||||
(this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
|
||||
tileZoom = undefined;
|
||||
} else {
|
||||
tileZoom = this._clampZoom(tileZoom);
|
||||
}
|
||||
|
||||
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 (Browser.any3d) {
|
||||
DomUtil.setTransform(level.el, translate, scale);
|
||||
} else {
|
||||
DomUtil.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 latLngBounds(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; }
|
||||
|
||||
DomUtil.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) {
|
||||
DomUtil.addClass(tile, 'leaflet-tile');
|
||||
|
||||
var tileSize = this.getTileSize();
|
||||
tile.style.width = tileSize.x + 'px';
|
||||
tile.style.height = tileSize.y + 'px';
|
||||
|
||||
tile.onselectstart = Util.falseFn;
|
||||
tile.onmousemove = Util.falseFn;
|
||||
|
||||
// update opacity on tiles in IE7-8 because of filter inheritance problems
|
||||
if (Browser.ielt9 && this.options.opacity < 1) {
|
||||
DomUtil.setOpacity(tile, this.options.opacity);
|
||||
}
|
||||
},
|
||||
|
||||
_addTile: function (coords, container) {
|
||||
var tilePos = this._getTilePos(coords),
|
||||
key = this._tileCoordsToKey(coords);
|
||||
|
||||
var tile = this.createTile(this._wrapCoords(coords), Util.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
|
||||
Util.requestAnimFrame(Util.bind(this._tileReady, this, coords, null, tile));
|
||||
}
|
||||
|
||||
DomUtil.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) {
|
||||
DomUtil.setOpacity(tile.el, 0);
|
||||
Util.cancelAnimFrame(this._fadeFrame);
|
||||
this._fadeFrame = Util.requestAnimFrame(this._updateOpacity, this);
|
||||
} else {
|
||||
tile.active = true;
|
||||
this._pruneTiles();
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
DomUtil.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 (Browser.ielt9 || !this._map._fadeAnimated) {
|
||||
Util.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(Util.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 ? Util.wrapNum(coords.x, this._wrapX) : coords.x,
|
||||
this._wrapY ? Util.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;
|
||||
}
|
||||
});
|
||||
|
||||
// @factory L.gridLayer(options?: GridLayer options)
|
||||
// Creates a new instance of GridLayer with the supplied options.
|
||||
export function gridLayer(options) {
|
||||
return new GridLayer(options);
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import {TileLayer} from './TileLayer';
|
||||
import {extend, setOptions, getParamString} from '../../core/Util';
|
||||
import Browser from '../../core/Browser';
|
||||
import {EPSG4326} from '../../geo/crs/CRS.EPSG4326';
|
||||
import {toBounds} from '../../geometry/Bounds';
|
||||
|
||||
/*
|
||||
* @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"
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
export 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](https://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 && Browser.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.
|
||||
export function tileLayerWMS(url, options) {
|
||||
return new TileLayerWMS(url, options);
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
import {GridLayer} from './GridLayer';
|
||||
import Browser from '../../core/Browser';
|
||||
import * as Util from '../../core/Util';
|
||||
import * as DomEvent from '../../dom/DomEvent';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
|
||||
|
||||
/*
|
||||
* @class TileLayer
|
||||
* @inherits GridLayer
|
||||
* @aka L.TileLayer
|
||||
* Used to load and display tile layers on the map. Note that most tile servers require attribution, which you can set under `Layer`. Extends `GridLayer`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png?{foo}', {foo: 'bar', attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);
|
||||
* ```
|
||||
*
|
||||
* @section URL template
|
||||
* @example
|
||||
*
|
||||
* A string of the following form:
|
||||
*
|
||||
* ```
|
||||
* 'https://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png'
|
||||
* ```
|
||||
*
|
||||
* `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; `a`, `b` or `c` by default, can be omitted), `{z}` — zoom level, `{x}` and `{y}` — tile coordinates. `{r}` can be used to add "@2x" to the URL to load retina tiles.
|
||||
*
|
||||
* You can use custom keys in the template, which will be [evaluated](#util-template) from TileLayer options, like this:
|
||||
*
|
||||
* ```
|
||||
* L.tileLayer('https://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png', {foo: 'bar'});
|
||||
* ```
|
||||
*/
|
||||
|
||||
|
||||
export var TileLayer = GridLayer.extend({
|
||||
|
||||
// @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,
|
||||
|
||||
// @option referrerPolicy: Boolean|String = false
|
||||
// Whether the referrerPolicy attribute will be added to the tiles.
|
||||
// If a String is provided, all tiles will have their referrerPolicy attribute set to the String provided.
|
||||
// This may be needed if your map's rendering context has a strict default but your tile provider expects a valid referrer
|
||||
// (e.g. to validate an API token).
|
||||
// Refer to [HTMLImageElement.referrerPolicy](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/referrerPolicy) for valid String values.
|
||||
referrerPolicy: false
|
||||
},
|
||||
|
||||
initialize: function (url, options) {
|
||||
|
||||
this._url = url;
|
||||
|
||||
options = Util.setOptions(this, options);
|
||||
|
||||
// detecting retina displays, adjusting tileSize and zoom levels
|
||||
if (options.detectRetina && Browser.retina && options.maxZoom > 0) {
|
||||
|
||||
options.tileSize = Math.floor(options.tileSize / 2);
|
||||
|
||||
if (!options.zoomReverse) {
|
||||
options.zoomOffset++;
|
||||
options.maxZoom = Math.max(options.minZoom, options.maxZoom - 1);
|
||||
} else {
|
||||
options.zoomOffset--;
|
||||
options.minZoom = Math.min(options.maxZoom, options.minZoom + 1);
|
||||
}
|
||||
|
||||
options.minZoom = Math.max(0, options.minZoom);
|
||||
} else if (!options.zoomReverse) {
|
||||
// make sure maxZoom is gte minZoom
|
||||
options.maxZoom = Math.max(options.minZoom, options.maxZoom);
|
||||
} else {
|
||||
// make sure minZoom is lte maxZoom
|
||||
options.minZoom = Math.min(options.maxZoom, options.minZoom);
|
||||
}
|
||||
|
||||
if (typeof options.subdomains === 'string') {
|
||||
options.subdomains = options.subdomains.split('');
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
DomEvent.on(tile, 'load', Util.bind(this._tileOnLoad, this, done, tile));
|
||||
DomEvent.on(tile, 'error', Util.bind(this._tileOnError, this, done, tile));
|
||||
|
||||
if (this.options.crossOrigin || this.options.crossOrigin === '') {
|
||||
tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
|
||||
}
|
||||
|
||||
// for this new option we follow the documented behavior
|
||||
// more closely by only setting the property when string
|
||||
if (typeof this.options.referrerPolicy === 'string') {
|
||||
tile.referrerPolicy = this.options.referrerPolicy;
|
||||
}
|
||||
|
||||
// The alt attribute is set to the empty string,
|
||||
// allowing screen readers to ignore the decorative image tiles.
|
||||
// https://www.w3.org/WAI/tutorials/images/decorative/
|
||||
// https://www.w3.org/TR/html-aria/#el-img-empty-alt
|
||||
tile.alt = '';
|
||||
|
||||
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: Browser.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 Util.template(this._url, Util.extend(data, this.options));
|
||||
},
|
||||
|
||||
_tileOnLoad: function (done, tile) {
|
||||
// For https://github.com/Leaflet/Leaflet/issues/3332
|
||||
if (Browser.ielt9) {
|
||||
setTimeout(Util.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 = Util.falseFn;
|
||||
tile.onerror = Util.falseFn;
|
||||
|
||||
if (!tile.complete) {
|
||||
tile.src = Util.emptyImageUrl;
|
||||
var coords = this._tiles[i].coords;
|
||||
DomUtil.remove(tile);
|
||||
delete this._tiles[i];
|
||||
// @event tileabort: TileEvent
|
||||
// Fired when a tile was loading but is now not wanted.
|
||||
this.fire('tileabort', {
|
||||
tile: tile,
|
||||
coords: coords
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_removeTile: function (key) {
|
||||
var tile = this._tiles[key];
|
||||
if (!tile) { return; }
|
||||
|
||||
// Cancels any pending http requests associated with the tile
|
||||
tile.el.setAttribute('src', Util.emptyImageUrl);
|
||||
|
||||
return GridLayer.prototype._removeTile.call(this, key);
|
||||
},
|
||||
|
||||
_tileReady: function (coords, err, tile) {
|
||||
if (!this._map || (tile && tile.getAttribute('src') === Util.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.
|
||||
|
||||
export function tileLayer(url, options) {
|
||||
return new TileLayer(url, options);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export {GridLayer, gridLayer} from './GridLayer';
|
||||
import {TileLayer, tileLayer} from './TileLayer';
|
||||
import {TileLayerWMS, tileLayerWMS} from './TileLayer.WMS';
|
||||
TileLayer.WMS = TileLayerWMS;
|
||||
tileLayer.wms = tileLayerWMS;
|
||||
export {TileLayer, tileLayer};
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
import {Renderer} from './Renderer';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
import * as DomEvent from '../../dom/DomEvent';
|
||||
import Browser from '../../core/Browser';
|
||||
import * as Util from '../../core/Util';
|
||||
import {Bounds} from '../../geometry/Bounds';
|
||||
|
||||
/*
|
||||
* @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](https://caniuse.com/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 } );
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var Canvas = Renderer.extend({
|
||||
|
||||
// @section
|
||||
// @aka Canvas options
|
||||
options: {
|
||||
// @option tolerance: Number = 0
|
||||
// How much to extend the click tolerance around a path/object on the map.
|
||||
tolerance: 0
|
||||
},
|
||||
|
||||
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');
|
||||
|
||||
DomEvent.on(container, 'mousemove', this._onMouseMove, this);
|
||||
DomEvent.on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this);
|
||||
DomEvent.on(container, 'mouseout', this._handleMouseOut, this);
|
||||
container['_leaflet_disable_events'] = true;
|
||||
|
||||
this._ctx = container.getContext('2d');
|
||||
},
|
||||
|
||||
_destroyContainer: function () {
|
||||
Util.cancelAnimFrame(this._redrawRequest);
|
||||
delete this._ctx;
|
||||
DomUtil.remove(this._container);
|
||||
DomEvent.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 = Browser.retina ? 2 : 1;
|
||||
|
||||
DomUtil.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 (Browser.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[Util.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[Util.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 || Util.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.save();
|
||||
this._ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this._ctx.clearRect(0, 0, this._container.width, this._container.height);
|
||||
this._ctx.restore();
|
||||
}
|
||||
},
|
||||
|
||||
_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)) {
|
||||
if (!(e.type === 'click' || e.type === 'preclick') || !this._map._draggableMoved(layer)) {
|
||||
clickedLayer = layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._fireEvent(clickedLayer ? [clickedLayer] : false, 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
|
||||
DomUtil.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) {
|
||||
DomUtil.addClass(this._container, 'leaflet-interactive'); // change cursor
|
||||
this._fireEvent([candidateHoveredLayer], e, 'mouseover');
|
||||
this._hoveredLayer = candidateHoveredLayer;
|
||||
}
|
||||
}
|
||||
|
||||
this._fireEvent(this._hoveredLayer ? [this._hoveredLayer] : false, e);
|
||||
|
||||
this._mouseHoverThrottled = true;
|
||||
setTimeout(Util.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.
|
||||
export function canvas(options) {
|
||||
return Browser.canvas ? new Canvas(options) : null;
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import {CircleMarker} from './CircleMarker';
|
||||
import {Path} from './Path';
|
||||
import * as Util from '../../core/Util';
|
||||
import {toLatLng} from '../../geo/LatLng';
|
||||
import {LatLngBounds} from '../../geo/LatLngBounds';
|
||||
import {Earth} from '../../geo/crs/CRS.Earth';
|
||||
|
||||
|
||||
/*
|
||||
* @class Circle
|
||||
* @aka L.Circle
|
||||
* @inherits CircleMarker
|
||||
*
|
||||
* A class for drawing circle overlays on a map. Extends `CircleMarker`.
|
||||
*
|
||||
* It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion).
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* L.circle([50.5, 30.5], {radius: 200}).addTo(map);
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var Circle = CircleMarker.extend({
|
||||
|
||||
initialize: function (latlng, options, legacyOptions) {
|
||||
if (typeof options === 'number') {
|
||||
// Backwards compatibility with 0.7.x factory (latlng, radius, options?)
|
||||
options = Util.extend({}, legacyOptions, {radius: options});
|
||||
}
|
||||
Util.setOptions(this, options);
|
||||
this._latlng = toLatLng(latlng);
|
||||
|
||||
if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); }
|
||||
|
||||
// @section
|
||||
// @aka Circle options
|
||||
// @option radius: Number; Radius of the circle, in meters.
|
||||
this._mRadius = this.options.radius;
|
||||
},
|
||||
|
||||
// @method setRadius(radius: Number): this
|
||||
// Sets the radius of a circle. Units are in meters.
|
||||
setRadius: function (radius) {
|
||||
this._mRadius = radius;
|
||||
return this.redraw();
|
||||
},
|
||||
|
||||
// @method getRadius(): Number
|
||||
// Returns the current radius of a circle. Units are in meters.
|
||||
getRadius: function () {
|
||||
return this._mRadius;
|
||||
},
|
||||
|
||||
// @method getBounds(): LatLngBounds
|
||||
// Returns the `LatLngBounds` of the path.
|
||||
getBounds: function () {
|
||||
var half = [this._radius, this._radiusY || this._radius];
|
||||
|
||||
return new LatLngBounds(
|
||||
this._map.layerPointToLatLng(this._point.subtract(half)),
|
||||
this._map.layerPointToLatLng(this._point.add(half)));
|
||||
},
|
||||
|
||||
setStyle: Path.prototype.setStyle,
|
||||
|
||||
_project: function () {
|
||||
|
||||
var lng = this._latlng.lng,
|
||||
lat = this._latlng.lat,
|
||||
map = this._map,
|
||||
crs = map.options.crs;
|
||||
|
||||
if (crs.distance === Earth.distance) {
|
||||
var d = Math.PI / 180,
|
||||
latR = (this._mRadius / Earth.R) / d,
|
||||
top = map.project([lat + latR, lng]),
|
||||
bottom = map.project([lat - latR, lng]),
|
||||
p = top.add(bottom).divideBy(2),
|
||||
lat2 = map.unproject(p).lat,
|
||||
lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) /
|
||||
(Math.cos(lat * d) * Math.cos(lat2 * d))) / d;
|
||||
|
||||
if (isNaN(lngR) || lngR === 0) {
|
||||
lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425
|
||||
}
|
||||
|
||||
this._point = p.subtract(map.getPixelOrigin());
|
||||
this._radius = isNaN(lngR) ? 0 : p.x - map.project([lat2, lng - lngR]).x;
|
||||
this._radiusY = p.y - top.y;
|
||||
|
||||
} else {
|
||||
var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0]));
|
||||
|
||||
this._point = map.latLngToLayerPoint(this._latlng);
|
||||
this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x;
|
||||
}
|
||||
|
||||
this._updateBounds();
|
||||
}
|
||||
});
|
||||
|
||||
// @factory L.circle(latlng: LatLng, options?: Circle options)
|
||||
// Instantiates a circle object given a geographical point, and an options object
|
||||
// which contains the circle radius.
|
||||
// @alternative
|
||||
// @factory L.circle(latlng: LatLng, radius: Number, options?: Circle options)
|
||||
// Obsolete way of instantiating a circle, for compatibility with 0.7.x code.
|
||||
// Do not use in new applications or plugins.
|
||||
export function circle(latlng, options, legacyOptions) {
|
||||
return new Circle(latlng, options, legacyOptions);
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import {Path} from './Path';
|
||||
import * as Util from '../../core/Util';
|
||||
import {toLatLng} from '../../geo/LatLng';
|
||||
import {Bounds} from '../../geometry/Bounds';
|
||||
|
||||
|
||||
/*
|
||||
* @class CircleMarker
|
||||
* @aka L.CircleMarker
|
||||
* @inherits Path
|
||||
*
|
||||
* A circle of a fixed size with radius specified in pixels. Extends `Path`.
|
||||
*/
|
||||
|
||||
export var CircleMarker = Path.extend({
|
||||
|
||||
// @section
|
||||
// @aka CircleMarker options
|
||||
options: {
|
||||
fill: true,
|
||||
|
||||
// @option radius: Number = 10
|
||||
// Radius of the circle marker, in pixels
|
||||
radius: 10
|
||||
},
|
||||
|
||||
initialize: function (latlng, options) {
|
||||
Util.setOptions(this, options);
|
||||
this._latlng = toLatLng(latlng);
|
||||
this._radius = this.options.radius;
|
||||
},
|
||||
|
||||
// @method setLatLng(latLng: LatLng): this
|
||||
// Sets the position of a circle marker to a new location.
|
||||
setLatLng: function (latlng) {
|
||||
var oldLatLng = this._latlng;
|
||||
this._latlng = toLatLng(latlng);
|
||||
this.redraw();
|
||||
|
||||
// @event move: Event
|
||||
// Fired when the marker is moved via [`setLatLng`](#circlemarker-setlatlng). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
|
||||
return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
|
||||
},
|
||||
|
||||
// @method getLatLng(): LatLng
|
||||
// Returns the current geographical position of the circle marker
|
||||
getLatLng: function () {
|
||||
return this._latlng;
|
||||
},
|
||||
|
||||
// @method setRadius(radius: Number): this
|
||||
// Sets the radius of a circle marker. Units are in pixels.
|
||||
setRadius: function (radius) {
|
||||
this.options.radius = this._radius = radius;
|
||||
return this.redraw();
|
||||
},
|
||||
|
||||
// @method getRadius(): Number
|
||||
// Returns the current radius of the circle
|
||||
getRadius: function () {
|
||||
return this._radius;
|
||||
},
|
||||
|
||||
setStyle : function (options) {
|
||||
var radius = options && options.radius || this._radius;
|
||||
Path.prototype.setStyle.call(this, options);
|
||||
this.setRadius(radius);
|
||||
return this;
|
||||
},
|
||||
|
||||
_project: function () {
|
||||
this._point = this._map.latLngToLayerPoint(this._latlng);
|
||||
this._updateBounds();
|
||||
},
|
||||
|
||||
_updateBounds: function () {
|
||||
var r = this._radius,
|
||||
r2 = this._radiusY || r,
|
||||
w = this._clickTolerance(),
|
||||
p = [r + w, r2 + w];
|
||||
this._pxBounds = new Bounds(this._point.subtract(p), this._point.add(p));
|
||||
},
|
||||
|
||||
_update: function () {
|
||||
if (this._map) {
|
||||
this._updatePath();
|
||||
}
|
||||
},
|
||||
|
||||
_updatePath: function () {
|
||||
this._renderer._updateCircle(this);
|
||||
},
|
||||
|
||||
_empty: function () {
|
||||
return this._radius && !this._renderer._bounds.intersects(this._pxBounds);
|
||||
},
|
||||
|
||||
// Needed by the `Canvas` renderer for interactivity
|
||||
_containsPoint: function (p) {
|
||||
return p.distanceTo(this._point) <= this._radius + this._clickTolerance();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// @factory L.circleMarker(latlng: LatLng, options?: CircleMarker options)
|
||||
// Instantiates a circle marker object given a geographical point, and an optional options object.
|
||||
export function circleMarker(latlng, options) {
|
||||
return new CircleMarker(latlng, options);
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import {Layer} from '../Layer';
|
||||
import * as Util from '../../core/Util';
|
||||
|
||||
/*
|
||||
* @class Path
|
||||
* @aka L.Path
|
||||
* @inherits Interactive layer
|
||||
*
|
||||
* An abstract class that contains options and constants shared between vector
|
||||
* overlays (Polygon, Polyline, Circle). Do not use it directly. Extends `Layer`.
|
||||
*/
|
||||
|
||||
export var Path = Layer.extend({
|
||||
|
||||
// @section
|
||||
// @aka Path options
|
||||
options: {
|
||||
// @option stroke: Boolean = true
|
||||
// Whether to draw stroke along the path. Set it to `false` to disable borders on polygons or circles.
|
||||
stroke: true,
|
||||
|
||||
// @option color: String = '#3388ff'
|
||||
// Stroke color
|
||||
color: '#3388ff',
|
||||
|
||||
// @option weight: Number = 3
|
||||
// Stroke width in pixels
|
||||
weight: 3,
|
||||
|
||||
// @option opacity: Number = 1.0
|
||||
// Stroke opacity
|
||||
opacity: 1,
|
||||
|
||||
// @option lineCap: String= 'round'
|
||||
// A string that defines [shape to be used at the end](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linecap) of the stroke.
|
||||
lineCap: 'round',
|
||||
|
||||
// @option lineJoin: String = 'round'
|
||||
// A string that defines [shape to be used at the corners](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linejoin) of the stroke.
|
||||
lineJoin: 'round',
|
||||
|
||||
// @option dashArray: String = null
|
||||
// A string that defines the stroke [dash pattern](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dasharray). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility).
|
||||
dashArray: null,
|
||||
|
||||
// @option dashOffset: String = null
|
||||
// A string that defines the [distance into the dash pattern to start the dash](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dashoffset). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility).
|
||||
dashOffset: null,
|
||||
|
||||
// @option fill: Boolean = depends
|
||||
// Whether to fill the path with color. Set it to `false` to disable filling on polygons or circles.
|
||||
fill: false,
|
||||
|
||||
// @option fillColor: String = *
|
||||
// Fill color. Defaults to the value of the [`color`](#path-color) option
|
||||
fillColor: null,
|
||||
|
||||
// @option fillOpacity: Number = 0.2
|
||||
// Fill opacity.
|
||||
fillOpacity: 0.2,
|
||||
|
||||
// @option fillRule: String = 'evenodd'
|
||||
// A string that defines [how the inside of a shape](https://developer.mozilla.org/docs/Web/SVG/Attribute/fill-rule) is determined.
|
||||
fillRule: 'evenodd',
|
||||
|
||||
// className: '',
|
||||
|
||||
// Option inherited from "Interactive layer" abstract class
|
||||
interactive: true,
|
||||
|
||||
// @option bubblingMouseEvents: Boolean = true
|
||||
// When `true`, a mouse event on this path will trigger the same event on the map
|
||||
// (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
|
||||
bubblingMouseEvents: true
|
||||
},
|
||||
|
||||
beforeAdd: function (map) {
|
||||
// Renderer is set here because we need to call renderer.getEvents
|
||||
// before this.getEvents.
|
||||
this._renderer = map.getRenderer(this);
|
||||
},
|
||||
|
||||
onAdd: function () {
|
||||
this._renderer._initPath(this);
|
||||
this._reset();
|
||||
this._renderer._addPath(this);
|
||||
},
|
||||
|
||||
onRemove: function () {
|
||||
this._renderer._removePath(this);
|
||||
},
|
||||
|
||||
// @method redraw(): this
|
||||
// Redraws the layer. Sometimes useful after you changed the coordinates that the path uses.
|
||||
redraw: function () {
|
||||
if (this._map) {
|
||||
this._renderer._updatePath(this);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method setStyle(style: Path options): this
|
||||
// Changes the appearance of a Path based on the options in the `Path options` object.
|
||||
setStyle: function (style) {
|
||||
Util.setOptions(this, style);
|
||||
if (this._renderer) {
|
||||
this._renderer._updateStyle(this);
|
||||
if (this.options.stroke && style && Object.prototype.hasOwnProperty.call(style, 'weight')) {
|
||||
this._updateBounds();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method bringToFront(): this
|
||||
// Brings the layer to the top of all path layers.
|
||||
bringToFront: function () {
|
||||
if (this._renderer) {
|
||||
this._renderer._bringToFront(this);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// @method bringToBack(): this
|
||||
// Brings the layer to the bottom of all path layers.
|
||||
bringToBack: function () {
|
||||
if (this._renderer) {
|
||||
this._renderer._bringToBack(this);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
getElement: function () {
|
||||
return this._path;
|
||||
},
|
||||
|
||||
_reset: function () {
|
||||
// defined in child classes
|
||||
this._project();
|
||||
this._update();
|
||||
},
|
||||
|
||||
_clickTolerance: function () {
|
||||
// used when doing hit detection for Canvas layers
|
||||
return (this.options.stroke ? this.options.weight / 2 : 0) +
|
||||
(this._renderer.options.tolerance || 0);
|
||||
}
|
||||
});
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import {Polyline} from './Polyline';
|
||||
import {LatLng} from '../../geo/LatLng';
|
||||
import * as LineUtil from '../../geometry/LineUtil';
|
||||
import {Point} from '../../geometry/Point';
|
||||
import {Bounds} from '../../geometry/Bounds';
|
||||
import * as PolyUtil from '../../geometry/PolyUtil';
|
||||
|
||||
/*
|
||||
* @class Polygon
|
||||
* @aka L.Polygon
|
||||
* @inherits Polyline
|
||||
*
|
||||
* A class for drawing polygon overlays on a map. Extends `Polyline`.
|
||||
*
|
||||
* Note that points you pass when creating a polygon shouldn't have an additional last point equal to the first one — it's better to filter out such points.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // create a red polygon from an array of LatLng points
|
||||
* var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]];
|
||||
*
|
||||
* var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map);
|
||||
*
|
||||
* // zoom the map to the polygon
|
||||
* map.fitBounds(polygon.getBounds());
|
||||
* ```
|
||||
*
|
||||
* You can also pass an array of arrays of latlngs, with the first array representing the outer shape and the other arrays representing holes in the outer shape:
|
||||
*
|
||||
* ```js
|
||||
* var latlngs = [
|
||||
* [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
|
||||
* [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
|
||||
* ];
|
||||
* ```
|
||||
*
|
||||
* Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape.
|
||||
*
|
||||
* ```js
|
||||
* var latlngs = [
|
||||
* [ // first polygon
|
||||
* [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
|
||||
* [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
|
||||
* ],
|
||||
* [ // second polygon
|
||||
* [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]]
|
||||
* ]
|
||||
* ];
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var Polygon = Polyline.extend({
|
||||
|
||||
options: {
|
||||
fill: true
|
||||
},
|
||||
|
||||
isEmpty: function () {
|
||||
return !this._latlngs.length || !this._latlngs[0].length;
|
||||
},
|
||||
|
||||
// @method getCenter(): LatLng
|
||||
// Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the Polygon.
|
||||
getCenter: function () {
|
||||
// throws error when not yet added to map as this center calculation requires projected coordinates
|
||||
if (!this._map) {
|
||||
throw new Error('Must add layer to map before using getCenter()');
|
||||
}
|
||||
return PolyUtil.polygonCenter(this._defaultShape(), this._map.options.crs);
|
||||
},
|
||||
|
||||
_convertLatLngs: function (latlngs) {
|
||||
var result = Polyline.prototype._convertLatLngs.call(this, latlngs),
|
||||
len = result.length;
|
||||
|
||||
// remove last point if it equals first one
|
||||
if (len >= 2 && result[0] instanceof LatLng && result[0].equals(result[len - 1])) {
|
||||
result.pop();
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
_setLatLngs: function (latlngs) {
|
||||
Polyline.prototype._setLatLngs.call(this, latlngs);
|
||||
if (LineUtil.isFlat(this._latlngs)) {
|
||||
this._latlngs = [this._latlngs];
|
||||
}
|
||||
},
|
||||
|
||||
_defaultShape: function () {
|
||||
return LineUtil.isFlat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0];
|
||||
},
|
||||
|
||||
_clipPoints: function () {
|
||||
// polygons need a different clipping algorithm so we redefine that
|
||||
|
||||
var bounds = this._renderer._bounds,
|
||||
w = this.options.weight,
|
||||
p = new Point(w, w);
|
||||
|
||||
// increase clip padding by stroke width to avoid stroke on clip edges
|
||||
bounds = new Bounds(bounds.min.subtract(p), bounds.max.add(p));
|
||||
|
||||
this._parts = [];
|
||||
if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.noClip) {
|
||||
this._parts = this._rings;
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0, len = this._rings.length, clipped; i < len; i++) {
|
||||
clipped = PolyUtil.clipPolygon(this._rings[i], bounds, true);
|
||||
if (clipped.length) {
|
||||
this._parts.push(clipped);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_updatePath: function () {
|
||||
this._renderer._updatePoly(this, true);
|
||||
},
|
||||
|
||||
// Needed by the `Canvas` renderer for interactivity
|
||||
_containsPoint: function (p) {
|
||||
var inside = false,
|
||||
part, p1, p2, i, j, k, len, len2;
|
||||
|
||||
if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; }
|
||||
|
||||
// ray casting algorithm for detecting if point is in polygon
|
||||
for (i = 0, len = this._parts.length; i < len; i++) {
|
||||
part = this._parts[i];
|
||||
|
||||
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
|
||||
p1 = part[j];
|
||||
p2 = part[k];
|
||||
|
||||
if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// also check if it's on polygon stroke
|
||||
return inside || Polyline.prototype._containsPoint.call(this, p, true);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// @factory L.polygon(latlngs: LatLng[], options?: Polyline options)
|
||||
export function polygon(latlngs, options) {
|
||||
return new Polygon(latlngs, options);
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
import {Path} from './Path';
|
||||
import * as Util from '../../core/Util';
|
||||
import * as LineUtil from '../../geometry/LineUtil';
|
||||
import {LatLng, toLatLng} from '../../geo/LatLng';
|
||||
import {LatLngBounds} from '../../geo/LatLngBounds';
|
||||
import {Bounds} from '../../geometry/Bounds';
|
||||
import {Point} from '../../geometry/Point';
|
||||
|
||||
/*
|
||||
* @class Polyline
|
||||
* @aka L.Polyline
|
||||
* @inherits Path
|
||||
*
|
||||
* A class for drawing polyline overlays on a map. Extends `Path`.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // create a red polyline from an array of LatLng points
|
||||
* var latlngs = [
|
||||
* [45.51, -122.68],
|
||||
* [37.77, -122.43],
|
||||
* [34.04, -118.2]
|
||||
* ];
|
||||
*
|
||||
* var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map);
|
||||
*
|
||||
* // zoom the map to the polyline
|
||||
* map.fitBounds(polyline.getBounds());
|
||||
* ```
|
||||
*
|
||||
* You can also pass a multi-dimensional array to represent a `MultiPolyline` shape:
|
||||
*
|
||||
* ```js
|
||||
* // create a red polyline from an array of arrays of LatLng points
|
||||
* var latlngs = [
|
||||
* [[45.51, -122.68],
|
||||
* [37.77, -122.43],
|
||||
* [34.04, -118.2]],
|
||||
* [[40.78, -73.91],
|
||||
* [41.83, -87.62],
|
||||
* [32.76, -96.72]]
|
||||
* ];
|
||||
* ```
|
||||
*/
|
||||
|
||||
|
||||
export var Polyline = Path.extend({
|
||||
|
||||
// @section
|
||||
// @aka Polyline options
|
||||
options: {
|
||||
// @option smoothFactor: Number = 1.0
|
||||
// How much to simplify the polyline on each zoom level. More means
|
||||
// better performance and smoother look, and less means more accurate representation.
|
||||
smoothFactor: 1.0,
|
||||
|
||||
// @option noClip: Boolean = false
|
||||
// Disable polyline clipping.
|
||||
noClip: false
|
||||
},
|
||||
|
||||
initialize: function (latlngs, options) {
|
||||
Util.setOptions(this, options);
|
||||
this._setLatLngs(latlngs);
|
||||
},
|
||||
|
||||
// @method getLatLngs(): LatLng[]
|
||||
// Returns an array of the points in the path, or nested arrays of points in case of multi-polyline.
|
||||
getLatLngs: function () {
|
||||
return this._latlngs;
|
||||
},
|
||||
|
||||
// @method setLatLngs(latlngs: LatLng[]): this
|
||||
// Replaces all the points in the polyline with the given array of geographical points.
|
||||
setLatLngs: function (latlngs) {
|
||||
this._setLatLngs(latlngs);
|
||||
return this.redraw();
|
||||
},
|
||||
|
||||
// @method isEmpty(): Boolean
|
||||
// Returns `true` if the Polyline has no LatLngs.
|
||||
isEmpty: function () {
|
||||
return !this._latlngs.length;
|
||||
},
|
||||
|
||||
// @method closestLayerPoint(p: Point): Point
|
||||
// Returns the point closest to `p` on the Polyline.
|
||||
closestLayerPoint: function (p) {
|
||||
var minDistance = Infinity,
|
||||
minPoint = null,
|
||||
closest = LineUtil._sqClosestPointOnSegment,
|
||||
p1, p2;
|
||||
|
||||
for (var j = 0, jLen = this._parts.length; j < jLen; j++) {
|
||||
var points = this._parts[j];
|
||||
|
||||
for (var i = 1, len = points.length; i < len; i++) {
|
||||
p1 = points[i - 1];
|
||||
p2 = points[i];
|
||||
|
||||
var sqDist = closest(p, p1, p2, true);
|
||||
|
||||
if (sqDist < minDistance) {
|
||||
minDistance = sqDist;
|
||||
minPoint = closest(p, p1, p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minPoint) {
|
||||
minPoint.distance = Math.sqrt(minDistance);
|
||||
}
|
||||
return minPoint;
|
||||
},
|
||||
|
||||
// @method getCenter(): LatLng
|
||||
// Returns the center ([centroid](https://en.wikipedia.org/wiki/Centroid)) of the polyline.
|
||||
getCenter: function () {
|
||||
// throws error when not yet added to map as this center calculation requires projected coordinates
|
||||
if (!this._map) {
|
||||
throw new Error('Must add layer to map before using getCenter()');
|
||||
}
|
||||
return LineUtil.polylineCenter(this._defaultShape(), this._map.options.crs);
|
||||
},
|
||||
|
||||
// @method getBounds(): LatLngBounds
|
||||
// Returns the `LatLngBounds` of the path.
|
||||
getBounds: function () {
|
||||
return this._bounds;
|
||||
},
|
||||
|
||||
// @method addLatLng(latlng: LatLng, latlngs?: LatLng[]): this
|
||||
// Adds a given point to the polyline. By default, adds to the first ring of
|
||||
// the polyline in case of a multi-polyline, but can be overridden by passing
|
||||
// a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)).
|
||||
addLatLng: function (latlng, latlngs) {
|
||||
latlngs = latlngs || this._defaultShape();
|
||||
latlng = toLatLng(latlng);
|
||||
latlngs.push(latlng);
|
||||
this._bounds.extend(latlng);
|
||||
return this.redraw();
|
||||
},
|
||||
|
||||
_setLatLngs: function (latlngs) {
|
||||
this._bounds = new LatLngBounds();
|
||||
this._latlngs = this._convertLatLngs(latlngs);
|
||||
},
|
||||
|
||||
_defaultShape: function () {
|
||||
return LineUtil.isFlat(this._latlngs) ? this._latlngs : this._latlngs[0];
|
||||
},
|
||||
|
||||
// recursively convert latlngs input into actual LatLng instances; calculate bounds along the way
|
||||
_convertLatLngs: function (latlngs) {
|
||||
var result = [],
|
||||
flat = LineUtil.isFlat(latlngs);
|
||||
|
||||
for (var i = 0, len = latlngs.length; i < len; i++) {
|
||||
if (flat) {
|
||||
result[i] = toLatLng(latlngs[i]);
|
||||
this._bounds.extend(result[i]);
|
||||
} else {
|
||||
result[i] = this._convertLatLngs(latlngs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
_project: function () {
|
||||
var pxBounds = new Bounds();
|
||||
this._rings = [];
|
||||
this._projectLatlngs(this._latlngs, this._rings, pxBounds);
|
||||
|
||||
if (this._bounds.isValid() && pxBounds.isValid()) {
|
||||
this._rawPxBounds = pxBounds;
|
||||
this._updateBounds();
|
||||
}
|
||||
},
|
||||
|
||||
_updateBounds: function () {
|
||||
var w = this._clickTolerance(),
|
||||
p = new Point(w, w);
|
||||
|
||||
if (!this._rawPxBounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pxBounds = new Bounds([
|
||||
this._rawPxBounds.min.subtract(p),
|
||||
this._rawPxBounds.max.add(p)
|
||||
]);
|
||||
},
|
||||
|
||||
// recursively turns latlngs into a set of rings with projected coordinates
|
||||
_projectLatlngs: function (latlngs, result, projectedBounds) {
|
||||
var flat = latlngs[0] instanceof LatLng,
|
||||
len = latlngs.length,
|
||||
i, ring;
|
||||
|
||||
if (flat) {
|
||||
ring = [];
|
||||
for (i = 0; i < len; i++) {
|
||||
ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
|
||||
projectedBounds.extend(ring[i]);
|
||||
}
|
||||
result.push(ring);
|
||||
} else {
|
||||
for (i = 0; i < len; i++) {
|
||||
this._projectLatlngs(latlngs[i], result, projectedBounds);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// clip polyline by renderer bounds so that we have less to render for performance
|
||||
_clipPoints: function () {
|
||||
var bounds = this._renderer._bounds;
|
||||
|
||||
this._parts = [];
|
||||
if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.noClip) {
|
||||
this._parts = this._rings;
|
||||
return;
|
||||
}
|
||||
|
||||
var parts = this._parts,
|
||||
i, j, k, len, len2, segment, points;
|
||||
|
||||
for (i = 0, k = 0, len = this._rings.length; i < len; i++) {
|
||||
points = this._rings[i];
|
||||
|
||||
for (j = 0, len2 = points.length; j < len2 - 1; j++) {
|
||||
segment = LineUtil.clipSegment(points[j], points[j + 1], bounds, j, true);
|
||||
|
||||
if (!segment) { continue; }
|
||||
|
||||
parts[k] = parts[k] || [];
|
||||
parts[k].push(segment[0]);
|
||||
|
||||
// if segment goes out of screen, or it's the last one, it's the end of the line part
|
||||
if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) {
|
||||
parts[k].push(segment[1]);
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// simplify each clipped part of the polyline for performance
|
||||
_simplifyPoints: function () {
|
||||
var parts = this._parts,
|
||||
tolerance = this.options.smoothFactor;
|
||||
|
||||
for (var i = 0, len = parts.length; i < len; i++) {
|
||||
parts[i] = LineUtil.simplify(parts[i], tolerance);
|
||||
}
|
||||
},
|
||||
|
||||
_update: function () {
|
||||
if (!this._map) { return; }
|
||||
|
||||
this._clipPoints();
|
||||
this._simplifyPoints();
|
||||
this._updatePath();
|
||||
},
|
||||
|
||||
_updatePath: function () {
|
||||
this._renderer._updatePoly(this);
|
||||
},
|
||||
|
||||
// Needed by the `Canvas` renderer for interactivity
|
||||
_containsPoint: function (p, closed) {
|
||||
var i, j, k, len, len2, part,
|
||||
w = this._clickTolerance();
|
||||
|
||||
if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; }
|
||||
|
||||
// hit detection for polylines
|
||||
for (i = 0, len = this._parts.length; i < len; i++) {
|
||||
part = this._parts[i];
|
||||
|
||||
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
|
||||
if (!closed && (j === 0)) { continue; }
|
||||
|
||||
if (LineUtil.pointToSegmentDistance(p, part[k], part[j]) <= w) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// @factory L.polyline(latlngs: LatLng[], options?: Polyline options)
|
||||
// Instantiates a polyline object given an array of geographical points and
|
||||
// optionally an options object. You can create a `Polyline` object with
|
||||
// multiple separate lines (`MultiPolyline`) by passing an array of arrays
|
||||
// of geographic points.
|
||||
export function polyline(latlngs, options) {
|
||||
return new Polyline(latlngs, options);
|
||||
}
|
||||
|
||||
// Retrocompat. Allow plugins to support Leaflet versions before and after 1.1.
|
||||
Polyline._flat = LineUtil._flat;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import {Polygon} from './Polygon';
|
||||
import {toLatLngBounds} from '../../geo/LatLngBounds';
|
||||
|
||||
/*
|
||||
* 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);
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export 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)
|
||||
export function rectangle(latLngBounds, options) {
|
||||
return new Rectangle(latLngBounds, options);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import {Map} from '../../map/Map';
|
||||
import {canvas} from './Canvas';
|
||||
import {svg} from './SVG';
|
||||
|
||||
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(options)) || svg(options);
|
||||
}
|
||||
});
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import {Layer} from '../Layer';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
import * as Util from '../../core/Util';
|
||||
import Browser from '../../core/Browser';
|
||||
import {Bounds} from '../../geometry/Bounds';
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* @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
|
||||
*/
|
||||
|
||||
export 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
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
Util.setOptions(this, options);
|
||||
Util.stamp(this);
|
||||
this._layers = this._layers || {};
|
||||
},
|
||||
|
||||
onAdd: function () {
|
||||
if (!this._container) {
|
||||
this._initContainer(); // defined by renderer implementations
|
||||
|
||||
// always keep transform-origin as 0 0
|
||||
DomUtil.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),
|
||||
viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
|
||||
currentCenterPoint = this._map.project(this._center, zoom),
|
||||
|
||||
topLeftOffset = viewHalf.multiplyBy(-scale).add(currentCenterPoint)
|
||||
.subtract(this._map._getNewPixelOrigin(center, zoom));
|
||||
|
||||
if (Browser.any3d) {
|
||||
DomUtil.setTransform(this._container, topLeftOffset, scale);
|
||||
} else {
|
||||
DomUtil.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();
|
||||
}
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import Browser from '../../core/Browser';
|
||||
|
||||
// @namespace SVG; @section
|
||||
// There are several static functions which can be called without instantiating L.SVG:
|
||||
|
||||
// @function create(name: String): SVGElement
|
||||
// Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement),
|
||||
// corresponding to the class name passed. For example, using 'line' will return
|
||||
// an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement).
|
||||
export function svgCreate(name) {
|
||||
return document.createElementNS('http://www.w3.org/2000/svg', name);
|
||||
}
|
||||
|
||||
// @function pointsToPath(rings: Point[], closed: Boolean): String
|
||||
// Generates a SVG path string for multiple rings, with each ring turning
|
||||
// into "M..L..L.." instructions
|
||||
export function pointsToPath(rings, closed) {
|
||||
var str = '',
|
||||
i, j, len, len2, points, p;
|
||||
|
||||
for (i = 0, len = rings.length; i < len; i++) {
|
||||
points = rings[i];
|
||||
|
||||
for (j = 0, len2 = points.length; j < len2; j++) {
|
||||
p = points[j];
|
||||
str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
|
||||
}
|
||||
|
||||
// closes the ring for polygons; "x" is VML syntax
|
||||
str += closed ? (Browser.svg ? 'z' : 'x') : '';
|
||||
}
|
||||
|
||||
// SVG complains about empty path strings
|
||||
return str || 'M0 0';
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
import * as Util from '../../core/Util';
|
||||
import {Renderer} from './Renderer';
|
||||
|
||||
/*
|
||||
* Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
|
||||
*/
|
||||
|
||||
|
||||
export 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) {
|
||||
// Do not return fn from catch block so `e` can be garbage collected
|
||||
// See https://github.com/Leaflet/Leaflet/pull/7279
|
||||
}
|
||||
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
|
||||
export var vmlMixin = {
|
||||
|
||||
_initContainer: function () {
|
||||
this._container = DomUtil.create('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');
|
||||
|
||||
DomUtil.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[Util.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;
|
||||
DomUtil.remove(container);
|
||||
layer.removeInteractiveTarget(container);
|
||||
delete this._layers[Util.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 = Util.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) {
|
||||
DomUtil.toFront(layer._container);
|
||||
},
|
||||
|
||||
_bringToBack: function (layer) {
|
||||
DomUtil.toBack(layer._container);
|
||||
}
|
||||
};
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
import {Renderer} from './Renderer';
|
||||
import * as DomUtil from '../../dom/DomUtil';
|
||||
import * as DomEvent from '../../dom/DomEvent';
|
||||
import Browser from '../../core/Browser';
|
||||
import {stamp} from '../../core/Util';
|
||||
import {svgCreate, pointsToPath} from './SVG.Util';
|
||||
export {pointsToPath};
|
||||
import {vmlMixin, vmlCreate} from './SVG.VML';
|
||||
|
||||
export var create = Browser.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](https://caniuse.com/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 } );
|
||||
* ```
|
||||
*/
|
||||
|
||||
export var SVG = Renderer.extend({
|
||||
|
||||
_initContainer: function () {
|
||||
this._container = create('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('g');
|
||||
this._container.appendChild(this._rootGroup);
|
||||
},
|
||||
|
||||
_destroyContainer: function () {
|
||||
DomUtil.remove(this._container);
|
||||
DomEvent.off(this._container);
|
||||
delete this._container;
|
||||
delete this._rootGroup;
|
||||
delete this._svgSize;
|
||||
},
|
||||
|
||||
_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
|
||||
DomUtil.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('path');
|
||||
|
||||
// @namespace Path
|
||||
// @option className: String = null
|
||||
// Custom class name set on an element. Only for SVG renderer.
|
||||
if (layer.options.className) {
|
||||
DomUtil.addClass(path, layer.options.className);
|
||||
}
|
||||
|
||||
if (layer.options.interactive) {
|
||||
DomUtil.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) {
|
||||
DomUtil.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) {
|
||||
DomUtil.toFront(layer._path);
|
||||
},
|
||||
|
||||
_bringToBack: function (layer) {
|
||||
DomUtil.toBack(layer._path);
|
||||
}
|
||||
});
|
||||
|
||||
if (Browser.vml) {
|
||||
SVG.include(vmlMixin);
|
||||
}
|
||||
|
||||
// @namespace SVG
|
||||
// @factory L.svg(options?: Renderer options)
|
||||
// Creates a SVG renderer with the given options.
|
||||
export function svg(options) {
|
||||
return Browser.svg || Browser.vml ? new SVG(options) : null;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export {Renderer} from './Renderer';
|
||||
export {Canvas, canvas} from './Canvas';
|
||||
import {SVG, create, pointsToPath, svg} from './SVG';
|
||||
SVG.create = create;
|
||||
SVG.pointsToPath = pointsToPath;
|
||||
export {SVG, svg};
|
||||
import './Renderer.getRenderer'; // This is a bit of a hack, but needed because circular dependencies
|
||||
|
||||
export {Path} from './Path';
|
||||
export {CircleMarker, circleMarker} from './CircleMarker';
|
||||
export {Circle, circle} from './Circle';
|
||||
export {Polyline, polyline} from './Polyline';
|
||||
export {Polygon, polygon} from './Polygon';
|
||||
export {Rectangle, rectangle} from './Rectangle';
|
||||
Reference in New Issue
Block a user