18 |
23 | Bug tested to occur on: Safari on Mac (Tested in 5.1.7), iPad/iPhone 5.1.1., Android 4 Browser. Hack is in L.Browser.chrome and TileLayer._addTile
24 |
25 |
26 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/debug/map/wms.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/debug/tests/bringtoback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2015, Vladimir Agafonkin
2 | Copyright (c) 2010-2011, CloudMade
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are
6 | permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this list of
9 | conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list
12 | of conditions and the following disclaimer in the documentation and/or other materials
13 | provided with the distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
17 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
18 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
22 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/debug/map/scroll.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 | On the left Map dragging and worldCopyJump are enabled during initialisation.
19 | On the right Map worldCopyJump is enabled. Dragging is enabled by clicking the button.
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/debug/vector/vector2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/debug/tests/click_on_canvas.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/layer/vector/Polygon.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Polygon implements polygon vector layer (closed polyline with a fill inside).
3 | */
4 |
5 | L.Polygon = L.Polyline.extend({
6 |
7 | options: {
8 | fill: true
9 | },
10 |
11 | getCenter: function () {
12 | var i, j, len, p1, p2, f, area, x, y,
13 | points = this._rings[0];
14 |
15 | // polygon centroid algorithm; only uses the first ring if there are multiple
16 |
17 | area = x = y = 0;
18 |
19 | for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
20 | p1 = points[i];
21 | p2 = points[j];
22 |
23 | f = p1.y * p2.x - p2.y * p1.x;
24 | x += (p1.x + p2.x) * f;
25 | y += (p1.y + p2.y) * f;
26 | area += f * 3;
27 | }
28 |
29 | return this._map.layerPointToLatLng([x / area, y / area]);
30 | },
31 |
32 | _convertLatLngs: function (latlngs) {
33 | var result = L.Polyline.prototype._convertLatLngs.call(this, latlngs),
34 | len = result.length;
35 |
36 | // remove last point if it equals first one
37 | if (len >= 2 && result[0] instanceof L.LatLng && result[0].equals(result[len - 1])) {
38 | result.pop();
39 | }
40 | return result;
41 | },
42 |
43 | _clipPoints: function () {
44 | if (this.options.noClip) {
45 | this._parts = this._rings;
46 | return;
47 | }
48 |
49 | // polygons need a different clipping algorithm so we redefine that
50 |
51 | var bounds = this._renderer._bounds,
52 | w = this.options.weight,
53 | p = new L.Point(w, w);
54 |
55 | // increase clip padding by stroke width to avoid stroke on clip edges
56 | bounds = new L.Bounds(bounds.min.subtract(p), bounds.max.add(p));
57 |
58 | this._parts = [];
59 |
60 | for (var i = 0, len = this._rings.length, clipped; i < len; i++) {
61 | clipped = L.PolyUtil.clipPolygon(this._rings[i], bounds, true);
62 | if (clipped.length) {
63 | this._parts.push(clipped);
64 | }
65 | }
66 | },
67 |
68 | _updatePath: function () {
69 | this._renderer._updatePoly(this, true);
70 | }
71 | });
72 |
73 | L.polygon = function (latlngs, options) {
74 | return new L.Polygon(latlngs, options);
75 | };
76 |
--------------------------------------------------------------------------------
/src/core/Browser.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Browser handles different browser and feature detections for internal Leaflet use.
3 | */
4 |
5 | (function () {
6 |
7 | var ua = navigator.userAgent.toLowerCase(),
8 | doc = document.documentElement,
9 |
10 | ie = 'ActiveXObject' in window,
11 |
12 | webkit = ua.indexOf('webkit') !== -1,
13 | phantomjs = ua.indexOf('phantom') !== -1,
14 | android23 = ua.search('android [23]') !== -1,
15 | chrome = ua.indexOf('chrome') !== -1,
16 |
17 | mobile = typeof orientation !== 'undefined',
18 | msPointer = navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent,
19 | pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer,
20 |
21 | ie3d = ie && ('transition' in doc.style),
22 | webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
23 | gecko3d = 'MozPerspective' in doc.style,
24 | opera3d = 'OTransition' in doc.style;
25 |
26 | var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window ||
27 | (window.DocumentTouch && document instanceof window.DocumentTouch));
28 |
29 | L.Browser = {
30 | ie: ie,
31 | ielt9: ie && !document.addEventListener,
32 | webkit: webkit,
33 | gecko: (ua.indexOf('gecko') !== -1) && !webkit && !window.opera && !ie,
34 | android: ua.indexOf('android') !== -1,
35 | android23: android23,
36 | chrome: chrome,
37 | safari: !chrome && ua.indexOf('safari') !== -1,
38 |
39 | ie3d: ie3d,
40 | webkit3d: webkit3d,
41 | gecko3d: gecko3d,
42 | opera3d: opera3d,
43 | any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs,
44 |
45 | mobile: mobile,
46 | mobileWebkit: mobile && webkit,
47 | mobileWebkit3d: mobile && webkit3d,
48 | mobileOpera: mobile && window.opera,
49 |
50 | touch: !!touch,
51 | msPointer: !!msPointer,
52 | pointer: !!pointer,
53 |
54 | retina: (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1
55 | };
56 |
57 | }());
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Leaflet is an open source JavaScript library for **mobile-friendly interactive maps**.
4 | It is developed by [Vladimir Agafonkin][] of [MapBox][] with a team of dedicated [contributors][].
5 | Weighing just about 30 KB of gzipped JS code, it has all the [features][] most developers ever need for online maps.
6 |
7 | Leaflet is designed with *simplicity*, *performance* and *usability* in mind.
8 | It works efficiently across all major desktop and mobile platforms out of the box,
9 | taking advantage of HTML5 and CSS3 on modern browsers while being accessible on older ones too.
10 | It can be extended with a huge amount of [plugins][],
11 | has a beautiful, easy to use and [well-documented][] API
12 | and a simple, readable [source code][] that is a joy to [contribute][] to.
13 |
14 | For more info, docs and tutorials, check out the [official website][].
15 | For **Leaflet downloads** (including the built master version), check out the [download page][].
16 |
17 | We're happy to meet new contributors.
18 | If you want to **get involved** with Leaflet development, check out the [contribution guide][contribute].
19 | Let's make the best mapping library that will ever exist,
20 | and push the limits of what's possible with online maps!
21 |
22 | [](https://travis-ci.org/Leaflet/Leaflet)
23 |
24 | [Vladimir Agafonkin]: http://agafonkin.com/en
25 | [contributors]: https://github.com/Leaflet/Leaflet/graphs/contributors
26 | [features]: http://leafletjs.com/features.html
27 | [plugins]: http://leafletjs.com/plugins.html
28 | [well-documented]: http://leafletjs.com/reference.html "Leaflet API reference"
29 | [source code]: https://github.com/Leaflet/Leaflet "Leaflet GitHub repository"
30 | [hosted on GitHub]: http://github.com/Leaflet/Leaflet
31 | [contribute]: https://github.com/Leaflet/Leaflet/blob/master/CONTRIBUTING.md "A guide to contributing to Leaflet"
32 | [official website]: http://leafletjs.com
33 | [download page]: http://leafletjs.com/download.html
34 | [MapBox]: https://mapbox.com
35 |
--------------------------------------------------------------------------------
/debug/map/markers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/layer/FeatureGroup.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.FeatureGroup extends L.LayerGroup by introducing mouse events and additional methods
3 | * shared between a group of interactive layers (like vectors or markers).
4 | */
5 |
6 | L.FeatureGroup = L.LayerGroup.extend({
7 |
8 | addLayer: function (layer) {
9 | if (this.hasLayer(layer)) {
10 | return this;
11 | }
12 |
13 | layer.addEventParent(this);
14 |
15 | L.LayerGroup.prototype.addLayer.call(this, layer);
16 |
17 | if (this._popupContent && layer.bindPopup) {
18 | layer.bindPopup(this._popupContent, this._popupOptions);
19 | }
20 |
21 | return this.fire('layeradd', {layer: layer});
22 | },
23 |
24 | removeLayer: function (layer) {
25 | if (!this.hasLayer(layer)) {
26 | return this;
27 | }
28 | if (layer in this._layers) {
29 | layer = this._layers[layer];
30 | }
31 |
32 | layer.removeEventParent(this);
33 |
34 | L.LayerGroup.prototype.removeLayer.call(this, layer);
35 |
36 | if (this._popupContent) {
37 | this.invoke('unbindPopup');
38 | }
39 |
40 | return this.fire('layerremove', {layer: layer});
41 | },
42 |
43 | bindPopup: function (content, options) {
44 | this._popupContent = content;
45 | this._popupOptions = options;
46 | return this.invoke('bindPopup', content, options);
47 | },
48 |
49 | openPopup: function (latlng) {
50 | // open popup on the first layer
51 | for (var id in this._layers) {
52 | this._layers[id].openPopup(latlng);
53 | break;
54 | }
55 | return this;
56 | },
57 |
58 | setStyle: function (style) {
59 | return this.invoke('setStyle', style);
60 | },
61 |
62 | bringToFront: function () {
63 | return this.invoke('bringToFront');
64 | },
65 |
66 | bringToBack: function () {
67 | return this.invoke('bringToBack');
68 | },
69 |
70 | getBounds: function () {
71 | var bounds = new L.LatLngBounds();
72 |
73 | this.eachLayer(function (layer) {
74 | bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng());
75 | });
76 |
77 | return bounds;
78 | }
79 | });
80 |
81 | L.featureGroup = function (layers) {
82 | return new L.FeatureGroup(layers);
83 | };
84 |
--------------------------------------------------------------------------------
/src/geo/crs/CRS.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.CRS is the base object for all defined CRS (Coordinate Reference Systems) in Leaflet.
3 | */
4 |
5 | L.CRS = {
6 | // converts geo coords to pixel ones
7 | latLngToPoint: function (latlng, zoom) {
8 | var projectedPoint = this.projection.project(latlng),
9 | scale = this.scale(zoom);
10 |
11 | return this.transformation._transform(projectedPoint, scale);
12 | },
13 |
14 | // converts pixel coords to geo coords
15 | pointToLatLng: function (point, zoom) {
16 | var scale = this.scale(zoom),
17 | untransformedPoint = this.transformation.untransform(point, scale);
18 |
19 | return this.projection.unproject(untransformedPoint);
20 | },
21 |
22 | // converts geo coords to projection-specific coords (e.g. in meters)
23 | project: function (latlng) {
24 | return this.projection.project(latlng);
25 | },
26 |
27 | // converts projected coords to geo coords
28 | unproject: function (point) {
29 | return this.projection.unproject(point);
30 | },
31 |
32 | // defines how the world scales with zoom
33 | scale: function (zoom) {
34 | return 256 * Math.pow(2, zoom);
35 | },
36 |
37 | // returns the bounds of the world in projected coords if applicable
38 | getProjectedBounds: function (zoom) {
39 | if (this.infinite) { return null; }
40 |
41 | var b = this.projection.bounds,
42 | s = this.scale(zoom),
43 | min = this.transformation.transform(b.min, s),
44 | max = this.transformation.transform(b.max, s);
45 |
46 | return L.bounds(min, max);
47 | },
48 |
49 | // whether a coordinate axis wraps in a given range (e.g. longitude from -180 to 180); depends on CRS
50 | // wrapLng: [min, max],
51 | // wrapLat: [min, max],
52 |
53 | // if true, the coordinate space will be unbounded (infinite in all directions)
54 | // infinite: false,
55 |
56 | // wraps geo coords in certain ranges if applicable
57 | wrapLatLng: function (latlng) {
58 | var lng = this.wrapLng ? L.Util.wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng,
59 | lat = this.wrapLat ? L.Util.wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat;
60 |
61 | return L.latLng(lat, lng);
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/spec/suites/control/Control.LayersSpec.js:
--------------------------------------------------------------------------------
1 | describe("Control.Layers", function () {
2 | var map;
3 |
4 | beforeEach(function () {
5 | map = L.map(document.createElement('div'));
6 | });
7 |
8 | describe("baselayerchange event", function () {
9 | it("is fired on input that changes the base layer", function () {
10 | var baseLayers = {"Layer 1": L.tileLayer(), "Layer 2": L.tileLayer()},
11 | layers = L.control.layers(baseLayers).addTo(map),
12 | spy = sinon.spy();
13 |
14 | map.on('baselayerchange', spy)
15 | .whenReady(function () {
16 | happen.click(layers._baseLayersList.getElementsByTagName("input")[0]);
17 |
18 | expect(spy.called).to.be.ok();
19 | expect(spy.mostRecentCall.args[0].layer).to.be(baseLayers["Layer 1"]);
20 | });
21 | });
22 |
23 | it("is not fired on input that doesn't change the base layer", function () {
24 | var overlays = {"Marker 1": L.marker([0, 0]), "Marker 2": L.marker([0, 0])},
25 | layers = L.control.layers({}, overlays).addTo(map),
26 | spy = sinon.spy();
27 |
28 | map.on('baselayerchange', spy);
29 | happen.click(layers._overlaysList.getElementsByTagName("input")[0]);
30 |
31 | expect(spy.called).to.not.be.ok();
32 | });
33 | });
34 |
35 | describe("updates", function () {
36 | beforeEach(function () {
37 | map.setView([0, 0], 14);
38 | });
39 |
40 | it("when an included layer is addded or removed", function () {
41 | var baseLayer = L.tileLayer(),
42 | overlay = L.marker([0, 0]),
43 | layers = L.control.layers({"Base": baseLayer}, {"Overlay": overlay}).addTo(map);
44 |
45 | var spy = sinon.spy(layers, '_update');
46 |
47 | map.addLayer(overlay);
48 | map.removeLayer(overlay);
49 |
50 | expect(spy.called).to.be.ok();
51 | expect(spy.callCount).to.eql(2);
52 | });
53 |
54 | it("not when a non-included layer is added or removed", function () {
55 | var baseLayer = L.tileLayer(),
56 | overlay = L.marker([0, 0]),
57 | layers = L.control.layers({"Base": baseLayer}).addTo(map);
58 |
59 | var spy = sinon.spy(layers, '_update');
60 |
61 | map.addLayer(overlay);
62 | map.removeLayer(overlay);
63 |
64 | expect(spy.called).to.not.be.ok();
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/layer/vector/Circle.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Circle is a circle overlay (with a certain radius in meters).
3 | * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion)
4 | */
5 |
6 | L.Circle = L.CircleMarker.extend({
7 |
8 | initialize: function (latlng, radius, options) {
9 | L.setOptions(this, options);
10 | this._latlng = L.latLng(latlng);
11 | this._mRadius = radius;
12 | },
13 |
14 | setRadius: function (radius) {
15 | this._mRadius = radius;
16 | return this.redraw();
17 | },
18 |
19 | getRadius: function () {
20 | return this._mRadius;
21 | },
22 |
23 | getBounds: function () {
24 | var half = [this._radius, this._radiusY];
25 |
26 | return new L.LatLngBounds(
27 | this._map.layerPointToLatLng(this._point.subtract(half)),
28 | this._map.layerPointToLatLng(this._point.add(half)));
29 | },
30 |
31 | setStyle: L.Path.prototype.setStyle,
32 |
33 | _project: function () {
34 |
35 | var lng = this._latlng.lng,
36 | lat = this._latlng.lat,
37 | map = this._map,
38 | crs = map.options.crs;
39 |
40 | if (crs.distance === L.CRS.Earth.distance) {
41 | var d = Math.PI / 180,
42 | latR = (this._mRadius / L.CRS.Earth.R) / d,
43 | top = map.project([lat + latR, lng]),
44 | bottom = map.project([lat - latR, lng]),
45 | p = top.add(bottom).divideBy(2),
46 | lat2 = map.unproject(p).lat,
47 | lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) /
48 | (Math.cos(lat * d) * Math.cos(lat2 * d))) / d;
49 |
50 | this._point = p.subtract(map.getPixelOrigin());
51 | this._radius = isNaN(lngR) ? 0 : Math.max(Math.round(p.x - map.project([lat2, lng - lngR]).x), 1);
52 | this._radiusY = Math.max(Math.round(p.y - top.y), 1);
53 |
54 | } else {
55 | var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0]));
56 |
57 | this._point = map.latLngToLayerPoint(this._latlng);
58 | this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x;
59 | }
60 |
61 | this._updateBounds();
62 | }
63 | });
64 |
65 | L.circle = function (latlng, radius, options) {
66 | return new L.Circle(latlng, radius, options);
67 | };
68 |
--------------------------------------------------------------------------------
/src/dom/DomEvent.DoubleTap.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Extends the event handling code with double tap support for mobile browsers.
3 | */
4 |
5 | L.extend(L.DomEvent, {
6 |
7 | _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart',
8 | _touchend: L.Browser.msPointer ? 'MSPointerUp' : L.Browser.pointer ? 'pointerup' : 'touchend',
9 |
10 | // inspired by Zepto touch code by Thomas Fuchs
11 | addDoubleTapListener: function (obj, handler, id) {
12 | var last, touch,
13 | doubleTap = false,
14 | delay = 250;
15 |
16 | function onTouchStart(e) {
17 | var count;
18 |
19 | if (L.Browser.pointer) {
20 | count = L.DomEvent._pointersCount;
21 | } else {
22 | count = e.touches.length;
23 | }
24 |
25 | if (count > 1) { return; }
26 |
27 | var now = Date.now(),
28 | delta = now - (last || now);
29 |
30 | touch = e.touches ? e.touches[0] : e;
31 | doubleTap = (delta > 0 && delta <= delay);
32 | last = now;
33 | }
34 |
35 | function onTouchEnd() {
36 | if (doubleTap) {
37 | if (L.Browser.pointer) {
38 | // work around .type being readonly with MSPointer* events
39 | var newTouch = {},
40 | prop, i;
41 |
42 | for (i in touch) {
43 | prop = touch[i];
44 | newTouch[i] = prop && prop.bind ? prop.bind(touch) : prop;
45 | }
46 | touch = newTouch;
47 | }
48 | touch.type = 'dblclick';
49 | handler(touch);
50 | last = null;
51 | }
52 | }
53 |
54 | var pre = '_leaflet_',
55 | touchstart = this._touchstart,
56 | touchend = this._touchend;
57 |
58 | obj[pre + touchstart + id] = onTouchStart;
59 | obj[pre + touchend + id] = onTouchEnd;
60 |
61 | obj.addEventListener(touchstart, onTouchStart, false);
62 | obj.addEventListener(touchend, onTouchEnd, false);
63 | return this;
64 | },
65 |
66 | removeDoubleTapListener: function (obj, id) {
67 | var pre = '_leaflet_',
68 | touchend = obj[pre + this._touchend + id];
69 |
70 | obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false);
71 | obj.removeEventListener(this._touchend, touchend, false);
72 |
73 | return this;
74 | }
75 | });
76 |
--------------------------------------------------------------------------------
/debug/map/zoompan.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
28 |
29 |
30 |
31 |
32 |
33 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/layer/vector/Renderer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Renderer is a base class for renderer implementations (SVG, Canvas);
3 | * handles renderer container, bounds and zoom animation.
4 | */
5 |
6 | L.Renderer = L.Layer.extend({
7 |
8 | options: {
9 | // how much to extend the clip area around the map view (relative to its size)
10 | // e.g. 0.1 would be 10% of map view in each direction; defaults to clip with the map view
11 | padding: 0
12 | },
13 |
14 | initialize: function (options) {
15 | L.setOptions(this, options);
16 | L.stamp(this);
17 | },
18 |
19 | onAdd: function () {
20 | if (!this._container) {
21 | this._initContainer(); // defined by renderer implementations
22 |
23 | if (this._zoomAnimated) {
24 | L.DomUtil.addClass(this._container, 'leaflet-zoom-animated');
25 | }
26 | }
27 |
28 | this.getPane().appendChild(this._container);
29 | this._update();
30 | },
31 |
32 | onRemove: function () {
33 | L.DomUtil.remove(this._container);
34 | },
35 |
36 | getEvents: function () {
37 | var events = {
38 | moveend: this._update
39 | };
40 | if (this._zoomAnimated) {
41 | events.zoomanim = this._animateZoom;
42 | }
43 | return events;
44 | },
45 |
46 | _animateZoom: function (e) {
47 | var origin = e.origin.subtract(this._map._getCenterLayerPoint()),
48 | offset = this._bounds.min.add(origin.multiplyBy(1 - e.scale)).add(e.offset).round();
49 |
50 | L.DomUtil.setTransform(this._container, offset, e.scale);
51 | },
52 |
53 | _update: function () {
54 | // update pixel bounds of renderer container (for positioning/sizing/clipping later)
55 | var p = this.options.padding,
56 | size = this._map.getSize(),
57 | min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();
58 |
59 | this._bounds = new L.Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());
60 | }
61 | });
62 |
63 |
64 | L.Map.include({
65 | // used by each vector layer to decide which renderer to use
66 | getRenderer: function (layer) {
67 | var renderer = layer.options.renderer || this.options.renderer || this._renderer;
68 |
69 | if (!renderer) {
70 | renderer = this._renderer = (L.SVG && L.svg()) || (L.Canvas && L.canvas());
71 | }
72 |
73 | if (!this.hasLayer(renderer)) {
74 | this.addLayer(renderer);
75 | }
76 | return renderer;
77 | }
78 | });
79 |
--------------------------------------------------------------------------------
/spec/suites/control/Control.AttributionSpec.js:
--------------------------------------------------------------------------------
1 | describe("Control.Attribution", function () {
2 |
3 | var map, control, container;
4 |
5 | beforeEach(function () {
6 | map = L.map(document.createElement('div'));
7 | control = new L.Control.Attribution({
8 | prefix: 'prefix'
9 | }).addTo(map);
10 | container = control.getContainer();
11 | });
12 |
13 | it("contains just prefix if no attributions added", function () {
14 | expect(container.innerHTML).to.eql('prefix');
15 | });
16 |
17 | describe('#addAttribution', function () {
18 | it('adds one attribution correctly', function () {
19 | control.addAttribution('foo');
20 | expect(container.innerHTML).to.eql('prefix | foo');
21 | });
22 |
23 | it('adds no duplicate attributions', function () {
24 | control.addAttribution('foo');
25 | control.addAttribution('foo');
26 | expect(container.innerHTML).to.eql('prefix | foo');
27 | });
28 |
29 | it('adds several attributions listed with comma', function () {
30 | control.addAttribution('foo');
31 | control.addAttribution('bar');
32 | expect(container.innerHTML).to.eql('prefix | foo, bar');
33 | });
34 | });
35 |
36 | describe('#removeAttribution', function () {
37 | it('removes attribution correctly', function () {
38 | control.addAttribution('foo');
39 | control.addAttribution('bar');
40 | control.removeAttribution('foo');
41 | expect(container.innerHTML).to.eql('prefix | bar');
42 | });
43 | it('does nothing if removing attribution that was not present', function () {
44 | control.addAttribution('foo');
45 | control.addAttribution('baz');
46 | control.removeAttribution('bar');
47 | control.removeAttribution('baz');
48 | control.removeAttribution('baz');
49 | control.removeAttribution('');
50 | expect(container.innerHTML).to.eql('prefix | foo');
51 | });
52 | });
53 |
54 | describe('#setPrefix', function () {
55 | it('changes prefix', function () {
56 | control.setPrefix('bla');
57 | expect(container.innerHTML).to.eql('bla');
58 | });
59 | });
60 |
61 | describe('control.attribution factory', function () {
62 | it('creates Control.Attribution instance', function () {
63 | var options = {prefix: 'prefix'};
64 | expect(L.control.attribution(options)).to.eql(new L.Control.Attribution(options));
65 | });
66 | });
67 |
68 | });
69 |
--------------------------------------------------------------------------------
/src/geo/LatLng.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.LatLng represents a geographical point with latitude and longitude coordinates.
3 | */
4 |
5 | L.LatLng = function (lat, lng, alt) {
6 | if (isNaN(lat) || isNaN(lng)) {
7 | throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
8 | }
9 |
10 | this.lat = +lat;
11 | this.lng = +lng;
12 |
13 | if (alt !== undefined) {
14 | this.alt = +alt;
15 | }
16 | };
17 |
18 | L.LatLng.prototype = {
19 | equals: function (obj, maxMargin) {
20 | if (!obj) { return false; }
21 |
22 | obj = L.latLng(obj);
23 |
24 | var margin = Math.max(
25 | Math.abs(this.lat - obj.lat),
26 | Math.abs(this.lng - obj.lng));
27 |
28 | return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin);
29 | },
30 |
31 | toString: function (precision) {
32 | return 'LatLng(' +
33 | L.Util.formatNum(this.lat, precision) + ', ' +
34 | L.Util.formatNum(this.lng, precision) + ')';
35 | },
36 |
37 | distanceTo: function (other) {
38 | return L.CRS.Earth.distance(this, L.latLng(other));
39 | },
40 |
41 | wrap: function () {
42 | return L.CRS.Earth.wrapLatLng(this);
43 | },
44 |
45 | toBounds: function (sizeInMeters) {
46 | var latAccuracy = 180 * sizeInMeters / 40075017,
47 | lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
48 |
49 | return L.latLngBounds(
50 | [this.lat - latAccuracy, this.lng - lngAccuracy],
51 | [this.lat + latAccuracy, this.lng + lngAccuracy]);
52 | }
53 | };
54 |
55 |
56 | // constructs LatLng with different signatures
57 | // (LatLng) or ([Number, Number]) or (Number, Number) or (Object)
58 |
59 | L.latLng = function (a, b, c) {
60 | if (a instanceof L.LatLng) {
61 | return a;
62 | }
63 | if (L.Util.isArray(a) && typeof a[0] !== 'object') {
64 | if (a.length === 3) {
65 | return new L.LatLng(a[0], a[1], a[2]);
66 | }
67 | if (a.length === 2) {
68 | return new L.LatLng(a[0], a[1]);
69 | }
70 | return null;
71 | }
72 | if (a === undefined || a === null) {
73 | return a;
74 | }
75 | if (typeof a === 'object' && 'lat' in a) {
76 | return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt);
77 | }
78 | if (b === undefined) {
79 | return null;
80 | }
81 | return new L.LatLng(a, b, c);
82 | };
83 |
--------------------------------------------------------------------------------
/src/layer/tile/TileLayer.WMS.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.TileLayer.WMS is used for WMS tile layers.
3 | */
4 |
5 | L.TileLayer.WMS = L.TileLayer.extend({
6 |
7 | defaultWmsParams: {
8 | service: 'WMS',
9 | request: 'GetMap',
10 | version: '1.1.1',
11 | layers: '',
12 | styles: '',
13 | format: 'image/jpeg',
14 | transparent: false
15 | },
16 |
17 | options: {
18 | crs: null,
19 | uppercase: false
20 | },
21 |
22 | initialize: function (url, options) {
23 |
24 | this._url = url;
25 |
26 | var wmsParams = L.extend({}, this.defaultWmsParams);
27 |
28 | // all keys that are not TileLayer options go to WMS params
29 | for (var i in options) {
30 | if (!(i in this.options)) {
31 | wmsParams[i] = options[i];
32 | }
33 | }
34 |
35 | options = L.setOptions(this, options);
36 |
37 | wmsParams.width = wmsParams.height = options.tileSize * (options.detectRetina && L.Browser.retina ? 2 : 1);
38 |
39 | this.wmsParams = wmsParams;
40 | },
41 |
42 | onAdd: function (map) {
43 |
44 | this._crs = this.options.crs || map.options.crs;
45 | this._wmsVersion = parseFloat(this.wmsParams.version);
46 |
47 | var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
48 | this.wmsParams[projectionKey] = this._crs.code;
49 |
50 | L.TileLayer.prototype.onAdd.call(this, map);
51 | },
52 |
53 | getTileUrl: function (coords) {
54 |
55 | var tileBounds = this._tileCoordsToBounds(coords),
56 | nw = this._crs.project(tileBounds.getNorthWest()),
57 | se = this._crs.project(tileBounds.getSouthEast()),
58 |
59 | bbox = (this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ?
60 | [se.y, nw.x, nw.y, se.x] :
61 | [nw.x, se.y, se.x, nw.y]).join(','),
62 |
63 | url = L.TileLayer.prototype.getTileUrl.call(this, coords);
64 |
65 | return url +
66 | L.Util.getParamString(this.wmsParams, url, this.options.uppercase) +
67 | (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox;
68 | },
69 |
70 | setParams: function (params, noRedraw) {
71 |
72 | L.extend(this.wmsParams, params);
73 |
74 | if (!noRedraw) {
75 | this.redraw();
76 | }
77 |
78 | return this;
79 | }
80 | });
81 |
82 | L.tileLayer.wms = function (url, options) {
83 | return new L.TileLayer.WMS(url, options);
84 | };
85 |
--------------------------------------------------------------------------------
/spec/suites/layer/FeatureGroupSpec.js:
--------------------------------------------------------------------------------
1 | describe('FeatureGroup', function () {
2 | var map;
3 | beforeEach(function () {
4 | map = L.map(document.createElement('div'));
5 | map.setView([0, 0], 1);
6 | });
7 | describe("#_propagateEvent", function () {
8 | var marker;
9 | beforeEach(function () {
10 | marker = L.marker([0, 0]);
11 | });
12 | describe("when a Marker is added to multiple FeatureGroups ", function () {
13 | it("e.layer should be the Marker", function () {
14 | var fg1 = L.featureGroup(),
15 | fg2 = L.featureGroup();
16 |
17 | fg1.addLayer(marker);
18 | fg2.addLayer(marker);
19 |
20 | var wasClicked1,
21 | wasClicked2;
22 |
23 | fg2.on('click', function (e) {
24 | expect(e.layer).to.be(marker);
25 | expect(e.target).to.be(fg2);
26 | wasClicked2 = true;
27 | });
28 |
29 | fg1.on('click', function (e) {
30 | expect(e.layer).to.be(marker);
31 | expect(e.target).to.be(fg1);
32 | wasClicked1 = true;
33 | });
34 |
35 | marker.fire('click', {type: 'click'}, true);
36 |
37 | expect(wasClicked1).to.be(true);
38 | expect(wasClicked2).to.be(true);
39 | });
40 | });
41 | });
42 | describe('addLayer', function () {
43 | it('adds the layer', function () {
44 | var fg = L.featureGroup(),
45 | marker = L.marker([0, 0]);
46 |
47 | expect(fg.hasLayer(marker)).to.be(false);
48 |
49 | fg.addLayer(marker);
50 |
51 | expect(fg.hasLayer(marker)).to.be(true);
52 | });
53 | it('supports non-evented layers', function () {
54 | var fg = L.featureGroup(),
55 | g = L.layerGroup();
56 |
57 | expect(fg.hasLayer(g)).to.be(false);
58 |
59 | fg.addLayer(g);
60 |
61 | expect(fg.hasLayer(g)).to.be(true);
62 | });
63 | });
64 | describe('removeLayer', function () {
65 | it('removes the layer passed to it', function () {
66 | var fg = L.featureGroup(),
67 | marker = L.marker([0, 0]);
68 |
69 | fg.addLayer(marker);
70 | expect(fg.hasLayer(marker)).to.be(true);
71 |
72 | fg.removeLayer(marker);
73 | expect(fg.hasLayer(marker)).to.be(false);
74 | });
75 | it('removes the layer passed to it by id', function () {
76 | var fg = L.featureGroup(),
77 | marker = L.marker([0, 0]);
78 |
79 | fg.addLayer(marker);
80 | expect(fg.hasLayer(marker)).to.be(true);
81 |
82 | fg.removeLayer(L.stamp(marker));
83 | expect(fg.hasLayer(marker)).to.be(false);
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/src/layer/marker/Icon.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Icon is an image-based icon class that you can use with L.Marker for custom markers.
3 | */
4 |
5 | L.Icon = L.Class.extend({
6 | /*
7 | options: {
8 | iconUrl: (String) (required)
9 | iconRetinaUrl: (String) (optional, used for retina devices if detected)
10 | iconSize: (Point) (can be set through CSS)
11 | iconAnchor: (Point) (centered by default, can be set in CSS with negative margins)
12 | popupAnchor: (Point) (if not specified, popup opens in the anchor point)
13 | shadowUrl: (String) (no shadow by default)
14 | shadowRetinaUrl: (String) (optional, used for retina devices if detected)
15 | shadowSize: (Point)
16 | shadowAnchor: (Point)
17 | className: (String)
18 | },
19 | */
20 |
21 | initialize: function (options) {
22 | L.setOptions(this, options);
23 | },
24 |
25 | createIcon: function (oldIcon) {
26 | return this._createIcon('icon', oldIcon);
27 | },
28 |
29 | createShadow: function (oldIcon) {
30 | return this._createIcon('shadow', oldIcon);
31 | },
32 |
33 | _createIcon: function (name, oldIcon) {
34 | var src = this._getIconUrl(name);
35 |
36 | if (!src) {
37 | if (name === 'icon') {
38 | throw new Error('iconUrl not set in Icon options (see the docs).');
39 | }
40 | return null;
41 | }
42 |
43 | var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null);
44 | this._setIconStyles(img, name);
45 |
46 | return img;
47 | },
48 |
49 | _setIconStyles: function (img, name) {
50 | var options = this.options,
51 | size = L.point(options[name + 'Size']),
52 | anchor = L.point(name === 'shadow' && options.shadowAnchor || options.iconAnchor ||
53 | size && size.divideBy(2, true));
54 |
55 | img.className = 'leaflet-marker-' + name + ' ' + (options.className || '');
56 |
57 | if (anchor) {
58 | img.style.marginLeft = (-anchor.x) + 'px';
59 | img.style.marginTop = (-anchor.y) + 'px';
60 | }
61 |
62 | if (size) {
63 | img.style.width = size.x + 'px';
64 | img.style.height = size.y + 'px';
65 | }
66 | },
67 |
68 | _createImg: function (src, el) {
69 | el = el || document.createElement('img');
70 | el.src = src;
71 | return el;
72 | },
73 |
74 | _getIconUrl: function (name) {
75 | return L.Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url'];
76 | }
77 | });
78 |
79 | L.icon = function (options) {
80 | return new L.Icon(options);
81 | };
82 |
--------------------------------------------------------------------------------
/debug/vector/vector-bounds.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/control/Control.Attribution.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Control.Attribution is used for displaying attribution on the map (added by default).
3 | */
4 |
5 | L.Control.Attribution = L.Control.extend({
6 | options: {
7 | position: 'bottomright',
8 | prefix: 'Leaflet'
9 | },
10 |
11 | initialize: function (options) {
12 | L.setOptions(this, options);
13 |
14 | this._attributions = {};
15 | },
16 |
17 | onAdd: function (map) {
18 | this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
19 | if (L.DomEvent) {
20 | L.DomEvent.disableClickPropagation(this._container);
21 | }
22 |
23 | // TODO ugly, refactor
24 | for (var i in map._layers) {
25 | if (map._layers[i].getAttribution) {
26 | this.addAttribution(map._layers[i].getAttribution());
27 | }
28 | }
29 |
30 | this._update();
31 |
32 | return this._container;
33 | },
34 |
35 | setPrefix: function (prefix) {
36 | this.options.prefix = prefix;
37 | this._update();
38 | return this;
39 | },
40 |
41 | addAttribution: function (text) {
42 | if (!text) { return this; }
43 |
44 | if (!this._attributions[text]) {
45 | this._attributions[text] = 0;
46 | }
47 | this._attributions[text]++;
48 |
49 | this._update();
50 |
51 | return this;
52 | },
53 |
54 | removeAttribution: function (text) {
55 | if (!text) { return this; }
56 |
57 | if (this._attributions[text]) {
58 | this._attributions[text]--;
59 | this._update();
60 | }
61 |
62 | return this;
63 | },
64 |
65 | _update: function () {
66 | if (!this._map) { return; }
67 |
68 | var attribs = [];
69 |
70 | for (var i in this._attributions) {
71 | if (this._attributions[i]) {
72 | attribs.push(i);
73 | }
74 | }
75 |
76 | var prefixAndAttribs = [];
77 |
78 | if (this.options.prefix) {
79 | prefixAndAttribs.push(this.options.prefix);
80 | }
81 | if (attribs.length) {
82 | prefixAndAttribs.push(attribs.join(', '));
83 | }
84 |
85 | this._container.innerHTML = prefixAndAttribs.join(' | ');
86 | }
87 | });
88 |
89 | L.Map.mergeOptions({
90 | attributionControl: true
91 | });
92 |
93 | L.Map.addInitHook(function () {
94 | if (this.options.attributionControl) {
95 | this.attributionControl = (new L.Control.Attribution()).addTo(this);
96 | }
97 | });
98 |
99 | L.control.attribution = function (options) {
100 | return new L.Control.Attribution(options);
101 | };
102 |
--------------------------------------------------------------------------------
/src/map/ext/Map.Geolocation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Provides L.Map with convenient shortcuts for using browser geolocation features.
3 | */
4 |
5 | L.Map.include({
6 | _defaultLocateOptions: {
7 | timeout: 10000,
8 | watch: false
9 | // setView: false
10 | // maxZoom:
11 | // maximumAge: 0
12 | // enableHighAccuracy: false
13 | },
14 |
15 | locate: function (/*Object*/ options) {
16 |
17 | options = this._locateOptions = L.extend({}, this._defaultLocateOptions, options);
18 |
19 | if (!navigator.geolocation) {
20 | this._handleGeolocationError({
21 | code: 0,
22 | message: 'Geolocation not supported.'
23 | });
24 | return this;
25 | }
26 |
27 | var onResponse = L.bind(this._handleGeolocationResponse, this),
28 | onError = L.bind(this._handleGeolocationError, this);
29 |
30 | if (options.watch) {
31 | this._locationWatchId =
32 | navigator.geolocation.watchPosition(onResponse, onError, options);
33 | } else {
34 | navigator.geolocation.getCurrentPosition(onResponse, onError, options);
35 | }
36 | return this;
37 | },
38 |
39 | stopLocate: function () {
40 | if (navigator.geolocation) {
41 | navigator.geolocation.clearWatch(this._locationWatchId);
42 | }
43 | if (this._locateOptions) {
44 | this._locateOptions.setView = false;
45 | }
46 | return this;
47 | },
48 |
49 | _handleGeolocationError: function (error) {
50 | var c = error.code,
51 | message = error.message ||
52 | (c === 1 ? 'permission denied' :
53 | (c === 2 ? 'position unavailable' : 'timeout'));
54 |
55 | if (this._locateOptions.setView && !this._loaded) {
56 | this.fitWorld();
57 | }
58 |
59 | this.fire('locationerror', {
60 | code: c,
61 | message: 'Geolocation error: ' + message + '.'
62 | });
63 | },
64 |
65 | _handleGeolocationResponse: function (pos) {
66 | var lat = pos.coords.latitude,
67 | lng = pos.coords.longitude,
68 | latlng = new L.LatLng(lat, lng),
69 | bounds = latlng.toBounds(pos.coords.accuracy),
70 | options = this._locateOptions;
71 |
72 | if (options.setView) {
73 | var zoom = this.getBoundsZoom(bounds);
74 | this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
75 | }
76 |
77 | var data = {
78 | latlng: latlng,
79 | bounds: bounds,
80 | timestamp: pos.timestamp
81 | };
82 |
83 | for (var i in pos.coords) {
84 | if (typeof pos.coords[i] === 'number') {
85 | data[i] = pos.coords[i];
86 | }
87 | }
88 |
89 | this.fire('locationfound', data);
90 | }
91 | });
92 |
--------------------------------------------------------------------------------
/src/geometry/Bounds.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Bounds represents a rectangular area on the screen in pixel coordinates.
3 | */
4 |
5 | L.Bounds = function (a, b) { //(Point, Point) or Point[]
6 | if (!a) { return; }
7 |
8 | var points = b ? [a, b] : a;
9 |
10 | for (var i = 0, len = points.length; i < len; i++) {
11 | this.extend(points[i]);
12 | }
13 | };
14 |
15 | L.Bounds.prototype = {
16 | // extend the bounds to contain the given point
17 | extend: function (point) { // (Point)
18 | point = L.point(point);
19 |
20 | if (!this.min && !this.max) {
21 | this.min = point.clone();
22 | this.max = point.clone();
23 | } else {
24 | this.min.x = Math.min(point.x, this.min.x);
25 | this.max.x = Math.max(point.x, this.max.x);
26 | this.min.y = Math.min(point.y, this.min.y);
27 | this.max.y = Math.max(point.y, this.max.y);
28 | }
29 | return this;
30 | },
31 |
32 | getCenter: function (round) { // (Boolean) -> Point
33 | return new L.Point(
34 | (this.min.x + this.max.x) / 2,
35 | (this.min.y + this.max.y) / 2, round);
36 | },
37 |
38 | getBottomLeft: function () { // -> Point
39 | return new L.Point(this.min.x, this.max.y);
40 | },
41 |
42 | getTopRight: function () { // -> Point
43 | return new L.Point(this.max.x, this.min.y);
44 | },
45 |
46 | getSize: function () {
47 | return this.max.subtract(this.min);
48 | },
49 |
50 | contains: function (obj) { // (Bounds) or (Point) -> Boolean
51 | var min, max;
52 |
53 | if (typeof obj[0] === 'number' || obj instanceof L.Point) {
54 | obj = L.point(obj);
55 | } else {
56 | obj = L.bounds(obj);
57 | }
58 |
59 | if (obj instanceof L.Bounds) {
60 | min = obj.min;
61 | max = obj.max;
62 | } else {
63 | min = max = obj;
64 | }
65 |
66 | return (min.x >= this.min.x) &&
67 | (max.x <= this.max.x) &&
68 | (min.y >= this.min.y) &&
69 | (max.y <= this.max.y);
70 | },
71 |
72 | intersects: function (bounds) { // (Bounds) -> Boolean
73 | bounds = L.bounds(bounds);
74 |
75 | var min = this.min,
76 | max = this.max,
77 | min2 = bounds.min,
78 | max2 = bounds.max,
79 | xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
80 | yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
81 |
82 | return xIntersects && yIntersects;
83 | },
84 |
85 | isValid: function () {
86 | return !!(this.min && this.max);
87 | }
88 | };
89 |
90 | L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[])
91 | if (!a || a instanceof L.Bounds) {
92 | return a;
93 | }
94 | return new L.Bounds(a, b);
95 | };
96 |
--------------------------------------------------------------------------------
/spec/suites/geometry/LineUtilSpec.js:
--------------------------------------------------------------------------------
1 | describe('LineUtil', function () {
2 |
3 | describe('#clipSegment', function () {
4 |
5 | var bounds;
6 |
7 | beforeEach(function () {
8 | bounds = L.bounds([5, 0], [15, 10]);
9 | });
10 |
11 | it('clips a segment by bounds', function () {
12 | var a = new L.Point(0, 0);
13 | var b = new L.Point(15, 15);
14 |
15 | var segment = L.LineUtil.clipSegment(a, b, bounds);
16 |
17 | expect(segment[0]).to.eql(new L.Point(5, 5));
18 | expect(segment[1]).to.eql(new L.Point(10, 10));
19 |
20 | var c = new L.Point(5, -5);
21 | var d = new L.Point(20, 10);
22 |
23 | var segment2 = L.LineUtil.clipSegment(c, d, bounds);
24 |
25 | expect(segment2[0]).to.eql(new L.Point(10, 0));
26 | expect(segment2[1]).to.eql(new L.Point(15, 5));
27 | });
28 |
29 | it('uses last bit code and reject segments out of bounds', function () {
30 | var a = new L.Point(15, 15);
31 | var b = new L.Point(25, 20);
32 | var segment = L.LineUtil.clipSegment(a, b, bounds, true);
33 |
34 | expect(segment).to.be(false);
35 | });
36 |
37 | it('can round numbers in clipped bounds', function () {
38 | var a = new L.Point(4, 5);
39 | var b = new L.Point(8, 6);
40 |
41 | var segment1 = L.LineUtil.clipSegment(a, b, bounds);
42 |
43 | expect(segment1[0]).to.eql(new L.Point(5, 5.25));
44 | expect(segment1[1]).to.eql(b);
45 |
46 | var segment2 = L.LineUtil.clipSegment(a, b, bounds, false, true);
47 |
48 | expect(segment2[0]).to.eql(new L.Point(5, 5));
49 | expect(segment2[1]).to.eql(b);
50 | });
51 | });
52 |
53 | describe('#pointToSegmentDistance & #closestPointOnSegment', function () {
54 |
55 | var p1 = new L.Point(0, 10);
56 | var p2 = new L.Point(10, 0);
57 | var p = new L.Point(0, 0);
58 |
59 | it('calculates distance from point to segment', function () {
60 | expect(L.LineUtil.pointToSegmentDistance(p, p1, p2)).to.eql(Math.sqrt(200) / 2);
61 | });
62 |
63 | it('calculates point closest to segment', function () {
64 | expect(L.LineUtil.closestPointOnSegment(p, p1, p2)).to.eql(new L.Point(5, 5));
65 | });
66 | });
67 |
68 | describe('#simplify', function () {
69 | it('simplifies polylines according to tolerance', function () {
70 | var points = [
71 | new L.Point(0, 0),
72 | new L.Point(0.01, 0),
73 | new L.Point(0.5, 0.01),
74 | new L.Point(0.7, 0),
75 | new L.Point(1, 0),
76 | new L.Point(1.999, 0.999),
77 | new L.Point(2, 1)
78 | ];
79 |
80 | var simplified = L.LineUtil.simplify(points, 0.1);
81 |
82 | expect(simplified).to.eql([
83 | new L.Point(0, 0),
84 | new L.Point(1, 0),
85 | new L.Point(2, 1)
86 | ]);
87 | });
88 | });
89 |
90 | });
91 |
--------------------------------------------------------------------------------
/src/core/Class.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Class powers the OOP facilities of the library.
3 | * Thanks to John Resig and Dean Edwards for inspiration!
4 | */
5 |
6 | L.Class = function () {};
7 |
8 | L.Class.extend = function (props) {
9 |
10 | // extended class with the new prototype
11 | var NewClass = function () {
12 |
13 | // call the constructor
14 | if (this.initialize) {
15 | this.initialize.apply(this, arguments);
16 | }
17 |
18 | // call all constructor hooks
19 | this.callInitHooks();
20 | };
21 |
22 | var parentProto = NewClass.__super__ = this.prototype;
23 |
24 | var proto = L.Util.create(parentProto);
25 | proto.constructor = NewClass;
26 |
27 | NewClass.prototype = proto;
28 |
29 | //inherit parent's statics
30 | for (var i in this) {
31 | if (this.hasOwnProperty(i) && i !== 'prototype') {
32 | NewClass[i] = this[i];
33 | }
34 | }
35 |
36 | // mix static properties into the class
37 | if (props.statics) {
38 | L.extend(NewClass, props.statics);
39 | delete props.statics;
40 | }
41 |
42 | // mix includes into the prototype
43 | if (props.includes) {
44 | L.Util.extend.apply(null, [proto].concat(props.includes));
45 | delete props.includes;
46 | }
47 |
48 | // merge options
49 | if (proto.options) {
50 | props.options = L.Util.extend(L.Util.create(proto.options), props.options);
51 | }
52 |
53 | // mix given properties into the prototype
54 | L.extend(proto, props);
55 |
56 | proto._initHooks = [];
57 |
58 | // add method for calling all hooks
59 | proto.callInitHooks = function () {
60 |
61 | if (this._initHooksCalled) { return; }
62 |
63 | if (parentProto.callInitHooks) {
64 | parentProto.callInitHooks.call(this);
65 | }
66 |
67 | this._initHooksCalled = true;
68 |
69 | for (var i = 0, len = proto._initHooks.length; i < len; i++) {
70 | proto._initHooks[i].call(this);
71 | }
72 | };
73 |
74 | return NewClass;
75 | };
76 |
77 |
78 | // method for adding properties to prototype
79 | L.Class.include = function (props) {
80 | L.extend(this.prototype, props);
81 | };
82 |
83 | // merge new default options to the Class
84 | L.Class.mergeOptions = function (options) {
85 | L.extend(this.prototype.options, options);
86 | };
87 |
88 | // add a constructor hook
89 | L.Class.addInitHook = function (fn) { // (Function) || (String, args...)
90 | var args = Array.prototype.slice.call(arguments, 1);
91 |
92 | var init = typeof fn === 'function' ? fn : function () {
93 | this[fn].apply(this, args);
94 | };
95 |
96 | this.prototype._initHooks = this.prototype._initHooks || [];
97 | this.prototype._initHooks.push(init);
98 | };
99 |
--------------------------------------------------------------------------------
/spec/suites/layer/vector/PolygonSpec.js:
--------------------------------------------------------------------------------
1 | describe('Polygon', function () {
2 |
3 | var c = document.createElement('div');
4 | c.style.width = '400px';
5 | c.style.height = '400px';
6 | var map = new L.Map(c);
7 | map.setView(new L.LatLng(55.8, 37.6), 6);
8 |
9 | describe("#initialize", function () {
10 | it("doesn't overwrite the given latlng array", function () {
11 | var originalLatLngs = [
12 | [1, 2],
13 | [3, 4]
14 | ];
15 | var sourceLatLngs = originalLatLngs.slice();
16 |
17 | var polygon = new L.Polygon(sourceLatLngs);
18 |
19 | expect(sourceLatLngs).to.eql(originalLatLngs);
20 | expect(polygon._latlngs).to.not.eql(sourceLatLngs);
21 | });
22 |
23 | it("can be called with an empty array", function () {
24 | var polygon = new L.Polygon([]);
25 | expect(polygon.getLatLngs()).to.eql([]);
26 | });
27 |
28 | it("can be initialized with holes", function () {
29 | var originalLatLngs = [
30 | [ //external rink
31 | [0, 10], [10, 10], [10, 0]
32 | ], [ //hole
33 | [2, 3], [2, 4], [3, 4]
34 | ]
35 | ];
36 |
37 | var polygon = new L.Polygon(originalLatLngs);
38 |
39 | // getLatLngs() returns both rings
40 | expect(polygon.getLatLngs()).to.eql([
41 | [L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])],
42 | [L.latLng([2, 3]), L.latLng([2, 4]), L.latLng([3, 4])]
43 | ]);
44 | });
45 | });
46 |
47 | describe("#setLatLngs", function () {
48 | it("doesn't overwrite the given latlng array", function () {
49 | var originalLatLngs = [
50 | [1, 2],
51 | [3, 4]
52 | ];
53 | var sourceLatLngs = originalLatLngs.slice();
54 |
55 | var polygon = new L.Polygon(sourceLatLngs);
56 |
57 | polygon.setLatLngs(sourceLatLngs);
58 |
59 | expect(sourceLatLngs).to.eql(originalLatLngs);
60 | });
61 |
62 | it("can be set external ring and holes", function () {
63 | var latLngs = [
64 | [ //external rink
65 | [0, 10], [10, 10], [10, 0]
66 | ], [ //hole
67 | [2, 3], [2, 4], [3, 4]
68 | ]
69 | ];
70 |
71 | var polygon = new L.Polygon([]);
72 | polygon.setLatLngs(latLngs);
73 |
74 | expect(polygon.getLatLngs()).to.eql([
75 | [L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])],
76 | [L.latLng([2, 3]), L.latLng([2, 4]), L.latLng([3, 4])]
77 | ]);
78 | });
79 | });
80 |
81 | describe("#spliceLatLngs", function () {
82 | it("splices the internal latLngs", function () {
83 | var latLngs = [
84 | [1, 2],
85 | [3, 4],
86 | [5, 6]
87 | ];
88 |
89 | var polygon = new L.Polygon(latLngs);
90 |
91 | polygon.spliceLatLngs(1, 1, [7, 8]);
92 |
93 | expect(polygon._latlngs).to.eql([L.latLng([1, 2]), L.latLng([7, 8]), L.latLng([5, 6])]);
94 | });
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/src/layer/LayerGroup.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.LayerGroup is a class to combine several layers into one so that
3 | * you can manipulate the group (e.g. add/remove it) as one layer.
4 | */
5 |
6 | L.LayerGroup = L.Layer.extend({
7 |
8 | initialize: function (layers) {
9 | this._layers = {};
10 |
11 | var i, len;
12 |
13 | if (layers) {
14 | for (i = 0, len = layers.length; i < len; i++) {
15 | this.addLayer(layers[i]);
16 | }
17 | }
18 | },
19 |
20 | addLayer: function (layer) {
21 | var id = this.getLayerId(layer);
22 |
23 | this._layers[id] = layer;
24 |
25 | if (this._map) {
26 | this._map.addLayer(layer);
27 | }
28 |
29 | return this;
30 | },
31 |
32 | removeLayer: function (layer) {
33 | var id = layer in this._layers ? layer : this.getLayerId(layer);
34 |
35 | if (this._map && this._layers[id]) {
36 | this._map.removeLayer(this._layers[id]);
37 | }
38 |
39 | delete this._layers[id];
40 |
41 | return this;
42 | },
43 |
44 | hasLayer: function (layer) {
45 | return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers);
46 | },
47 |
48 | clearLayers: function () {
49 | for (var i in this._layers) {
50 | this.removeLayer(this._layers[i]);
51 | }
52 | return this;
53 | },
54 |
55 | invoke: function (methodName) {
56 | var args = Array.prototype.slice.call(arguments, 1),
57 | i, layer;
58 |
59 | for (i in this._layers) {
60 | layer = this._layers[i];
61 |
62 | if (layer[methodName]) {
63 | layer[methodName].apply(layer, args);
64 | }
65 | }
66 |
67 | return this;
68 | },
69 |
70 | onAdd: function (map) {
71 | for (var i in this._layers) {
72 | map.addLayer(this._layers[i]);
73 | }
74 | },
75 |
76 | onRemove: function (map) {
77 | for (var i in this._layers) {
78 | map.removeLayer(this._layers[i]);
79 | }
80 | },
81 |
82 | eachLayer: function (method, context) {
83 | for (var i in this._layers) {
84 | method.call(context, this._layers[i]);
85 | }
86 | return this;
87 | },
88 |
89 | getLayer: function (id) {
90 | return this._layers[id];
91 | },
92 |
93 | getLayers: function () {
94 | var layers = [];
95 |
96 | for (var i in this._layers) {
97 | layers.push(this._layers[i]);
98 | }
99 | return layers;
100 | },
101 |
102 | setZIndex: function (zIndex) {
103 | return this.invoke('setZIndex', zIndex);
104 | },
105 |
106 | getLayerId: function (layer) {
107 | return L.stamp(layer);
108 | }
109 | });
110 |
111 | L.layerGroup = function (layers) {
112 | return new L.LayerGroup(layers);
113 | };
114 |
--------------------------------------------------------------------------------
/spec/suites/geometry/BoundsSpec.js:
--------------------------------------------------------------------------------
1 | describe('Bounds', function () {
2 | var a, b, c;
3 |
4 | beforeEach(function () {
5 | a = new L.Bounds(
6 | new L.Point(14, 12),
7 | new L.Point(30, 40));
8 | b = new L.Bounds([
9 | new L.Point(20, 12),
10 | new L.Point(14, 20),
11 | new L.Point(30, 40)
12 | ]);
13 | c = new L.Bounds();
14 | });
15 |
16 | describe('constructor', function () {
17 | it('creates bounds with proper min & max on (Point, Point)', function () {
18 | expect(a.min).to.eql(new L.Point(14, 12));
19 | expect(a.max).to.eql(new L.Point(30, 40));
20 | });
21 | it('creates bounds with proper min & max on (Point[])', function () {
22 | expect(b.min).to.eql(new L.Point(14, 12));
23 | expect(b.max).to.eql(new L.Point(30, 40));
24 | });
25 | });
26 |
27 | describe('#extend', function () {
28 | it('extends the bounds to contain the given point', function () {
29 | a.extend(new L.Point(50, 20));
30 | expect(a.min).to.eql(new L.Point(14, 12));
31 | expect(a.max).to.eql(new L.Point(50, 40));
32 |
33 | b.extend(new L.Point(25, 50));
34 | expect(b.min).to.eql(new L.Point(14, 12));
35 | expect(b.max).to.eql(new L.Point(30, 50));
36 | });
37 | });
38 |
39 | describe('#getCenter', function () {
40 | it('returns the center point', function () {
41 | expect(a.getCenter()).to.eql(new L.Point(22, 26));
42 | });
43 | });
44 |
45 | describe('#contains', function () {
46 | it('contains other bounds or point', function () {
47 | a.extend(new L.Point(50, 10));
48 | expect(a.contains(b)).to.be.ok();
49 | expect(b.contains(a)).to.not.be.ok();
50 | expect(a.contains(new L.Point(24, 25))).to.be.ok();
51 | expect(a.contains(new L.Point(54, 65))).to.not.be.ok();
52 | });
53 | });
54 |
55 | describe('#isValid', function () {
56 | it('returns true if properly set up', function () {
57 | expect(a.isValid()).to.be.ok();
58 | });
59 | it('returns false if is invalid', function () {
60 | expect(c.isValid()).to.not.be.ok();
61 | });
62 | it('returns true if extended', function () {
63 | c.extend([0, 0]);
64 | expect(c.isValid()).to.be.ok();
65 | });
66 | });
67 |
68 | describe('#getSize', function () {
69 | it('returns the size of the bounds as point', function () {
70 | expect(a.getSize()).to.eql(new L.Point(16, 28));
71 | });
72 | });
73 |
74 | describe('#intersects', function () {
75 | it('returns true if bounds intersect', function () {
76 | expect(a.intersects(b)).to.be(true);
77 | expect(a.intersects(new L.Bounds(new L.Point(100, 100), new L.Point(120, 120)))).to.eql(false);
78 | });
79 | });
80 |
81 | describe('L.bounds factory', function () {
82 | it('creates bounds from array of number arrays', function () {
83 | var bounds = L.bounds([[14, 12], [30, 40]]);
84 | expect(bounds).to.eql(a);
85 | });
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/debug/tests/add_remove_layers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
77 |
78 |
79 |
80 |
81 |
82 |