Click the map to place a popup at the mouse location
23 |
24 |
25 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/layer/vector/CircleMarker.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.CircleMarker is a circle overlay with a permanent pixel radius.
3 | */
4 |
5 | L.CircleMarker = L.Circle.extend({
6 | options: {
7 | radius: 10,
8 | weight: 2
9 | },
10 |
11 | initialize: function (latlng, options) {
12 | L.Circle.prototype.initialize.call(this, latlng, null, options);
13 | this._radius = this.options.radius;
14 | },
15 |
16 | projectLatlngs: function () {
17 | this._point = this._map.latLngToLayerPoint(this._latlng);
18 | },
19 |
20 | _updateStyle : function () {
21 | L.Circle.prototype._updateStyle.call(this);
22 | this.setRadius(this.options.radius);
23 | },
24 |
25 | setLatLng: function (latlng) {
26 | L.Circle.prototype.setLatLng.call(this, latlng);
27 | if (this._popup && this._popup._isOpen) {
28 | this._popup.setLatLng(latlng);
29 | }
30 | },
31 |
32 | setRadius: function (radius) {
33 | this.options.radius = this._radius = radius;
34 | return this.redraw();
35 | },
36 |
37 | getRadius: function () {
38 | return this._radius;
39 | }
40 | });
41 |
42 | L.circleMarker = function (latlng, options) {
43 | return new L.CircleMarker(latlng, options);
44 | };
45 |
--------------------------------------------------------------------------------
/spec/suites/SpecHelper.js:
--------------------------------------------------------------------------------
1 | if (!Array.prototype.map) {
2 | Array.prototype.map = function (fun /*, thisp */) {
3 | "use strict";
4 |
5 | if (this === void 0 || this === null) {
6 | throw new TypeError();
7 | }
8 |
9 | var t = Object(this);
10 | // jshint bitwise: false
11 | var len = t.length >>> 0;
12 | if (typeof fun !== "function") {
13 | throw new TypeError();
14 | }
15 |
16 | var res = new Array(len);
17 | var thisp = arguments[1];
18 | for (var i = 0; i < len; i++) {
19 | if (i in t) {
20 | res[i] = fun.call(thisp, t[i], i, t);
21 | }
22 | }
23 |
24 | return res;
25 | };
26 | }
27 |
28 | expect.Assertion.prototype.near = function (expected, delta) {
29 | delta = delta || 1;
30 | expect(this.obj.x).to
31 | .be.within(expected.x - delta, expected.x + delta);
32 | expect(this.obj.y).to
33 | .be.within(expected.y - delta, expected.y + delta);
34 | };
35 |
36 | expect.Assertion.prototype.nearLatLng = function (expected, delta) {
37 | delta = delta || 1e-4;
38 | expect(this.obj.lat).to
39 | .be.within(expected.lat - delta, expected.lat + delta);
40 | expect(this.obj.lng).to
41 | .be.within(expected.lng - delta, expected.lng + delta);
42 | };
43 |
--------------------------------------------------------------------------------
/debug/tests/reuse_popups.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/debug/vector/vector.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/debug/map/max-bounds.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/layer/marker/Icon.Default.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Icon.Default is the blue marker icon used by default in Leaflet.
3 | */
4 |
5 | L.Icon.Default = L.Icon.extend({
6 |
7 | options: {
8 | iconSize: [25, 41],
9 | iconAnchor: [12, 41],
10 | popupAnchor: [1, -34],
11 |
12 | shadowSize: [41, 41]
13 | },
14 |
15 | _getIconUrl: function (name) {
16 | var key = name + 'Url';
17 |
18 | if (this.options[key]) {
19 | return this.options[key];
20 | }
21 |
22 | if (L.Browser.retina && name === 'icon') {
23 | name += '-2x';
24 | }
25 |
26 | var path = L.Icon.Default.imagePath;
27 |
28 | if (!path) {
29 | throw new Error('Couldn\'t autodetect L.Icon.Default.imagePath, set it manually.');
30 | }
31 |
32 | return path + '/marker-' + name + '.png';
33 | }
34 | });
35 |
36 | L.Icon.Default.imagePath = (function () {
37 | var scripts = document.getElementsByTagName('script'),
38 | leafletRe = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/;
39 |
40 | var i, len, src, matches, path;
41 |
42 | for (i = 0, len = scripts.length; i < len; i++) {
43 | src = scripts[i].src;
44 | matches = src.match(leafletRe);
45 |
46 | if (matches) {
47 | path = src.split(leafletRe)[0];
48 | return (path ? path + '/' : '') + 'images';
49 | }
50 | }
51 | }());
52 |
--------------------------------------------------------------------------------
/debug/vector/vector-mobile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/debug/map/canvas.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/debug/vector/geojson-sample.js:
--------------------------------------------------------------------------------
1 | var geojsonSample = {
2 | "type": "FeatureCollection",
3 | "features": [
4 | {
5 | "type": "Feature",
6 | "geometry": {
7 | "type": "Point",
8 | "coordinates": [102.0, 0.5]
9 | },
10 | "properties": {
11 | "prop0": "value0",
12 | "color": "blue"
13 | }
14 | },
15 |
16 | {
17 | "type": "Feature",
18 | "geometry": {
19 | "type": "LineString",
20 | "coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]
21 | },
22 | "properties": {
23 | "color": "red",
24 | "prop1": 0.0
25 | }
26 | },
27 |
28 | {
29 | "type": "Feature",
30 | "geometry": {
31 | "type": "Polygon",
32 | "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]
33 | },
34 | "properties": {
35 | "color": "green",
36 | "prop1": {
37 | "this": "that"
38 | }
39 | }
40 | },
41 |
42 | {
43 | "type": "Feature",
44 | "geometry": {
45 | "type": "MultiPolygon",
46 | "coordinates": [[[[100.0, 1.5], [100.5, 1.5], [100.5, 2.0], [100.0, 2.0], [100.0, 1.5]]], [[[100.5, 2.0], [100.5, 2.5], [101.0, 2.5], [101.0, 2.0], [100.5, 2.0]]]]
47 | },
48 | "properties": {
49 | "color": "purple"
50 | }
51 | }
52 | ]
53 | };
54 |
--------------------------------------------------------------------------------
/spec/suites/layer/vector/PolylineGeometrySpec.js:
--------------------------------------------------------------------------------
1 | describe('PolylineGeometry', 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("#distanceTo", function () {
10 | it("calculates distances to points", function () {
11 | var p1 = map.latLngToLayerPoint(new L.LatLng(55.8, 37.6));
12 | var p2 = map.latLngToLayerPoint(new L.LatLng(57.123076977278, 44.861962891635));
13 | var latlngs = [[56.485503424111, 35.545556640339], [55.972522915346, 36.116845702918], [55.502459116923, 34.930322265253], [55.31534617509, 38.973291015816]]
14 | .map(function (ll) {
15 | return new L.LatLng(ll[0], ll[1]);
16 | });
17 | var polyline = new L.Polyline([], {
18 | 'noClip': true
19 | });
20 | map.addLayer(polyline);
21 |
22 | expect(polyline.closestLayerPoint(p1)).to.be(null);
23 |
24 | polyline.setLatLngs(latlngs);
25 | var point = polyline.closestLayerPoint(p1);
26 | expect(point).not.to.be(null);
27 | expect(point.distance).to.not.be(Infinity);
28 | expect(point.distance).to.not.be(NaN);
29 |
30 | var point2 = polyline.closestLayerPoint(p2);
31 |
32 | expect(point.distance).to.be.lessThan(point2.distance);
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/spec/suites/map/handler/Map.DragSpec.js:
--------------------------------------------------------------------------------
1 | describe("Map.Drag", function () {
2 | describe("#addHook", function () {
3 | it("calls the map with dragging enabled", function () {
4 | var container = document.createElement('div'),
5 | map = new L.Map(container, {
6 | dragging: true
7 | });
8 |
9 | expect(map.dragging.enabled()).to.be(true);
10 | map.setView([0, 0], 0);
11 | expect(map.dragging.enabled()).to.be(true);
12 | });
13 | it("calls the map with dragging and worldCopyJump enabled", function () {
14 | var container = document.createElement('div'),
15 | map = new L.Map(container, {
16 | dragging: true,
17 | worldCopyJump: true
18 | });
19 |
20 | expect(map.dragging.enabled()).to.be(true);
21 | map.setView([0, 0], 0);
22 | expect(map.dragging.enabled()).to.be(true);
23 | });
24 | it("calls the map with dragging disabled and worldCopyJump enabled; " +
25 | "enables dragging after setting center and zoom", function () {
26 | var container = document.createElement('div'),
27 | map = new L.Map(container, {
28 | dragging: false,
29 | worldCopyJump: true
30 | });
31 |
32 | expect(map.dragging.enabled()).to.be(false);
33 | map.setView([0, 0], 0);
34 | map.dragging.enable();
35 | expect(map.dragging.enabled()).to.be(true);
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/layer/vector/MultiPoly.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Contains L.MultiPolyline and L.MultiPolygon layers.
3 | */
4 |
5 | (function () {
6 | function createMulti(Klass) {
7 |
8 | return L.FeatureGroup.extend({
9 |
10 | initialize: function (latlngs, options) {
11 | this._layers = {};
12 | this._options = options;
13 | this.setLatLngs(latlngs);
14 | },
15 |
16 | setLatLngs: function (latlngs) {
17 | var i = 0,
18 | len = latlngs.length;
19 |
20 | this.eachLayer(function (layer) {
21 | if (i < len) {
22 | layer.setLatLngs(latlngs[i++]);
23 | } else {
24 | this.removeLayer(layer);
25 | }
26 | }, this);
27 |
28 | while (i < len) {
29 | this.addLayer(new Klass(latlngs[i++], this._options));
30 | }
31 |
32 | return this;
33 | },
34 |
35 | getLatLngs: function () {
36 | var latlngs = [];
37 |
38 | this.eachLayer(function (layer) {
39 | latlngs.push(layer.getLatLngs());
40 | });
41 |
42 | return latlngs;
43 | }
44 | });
45 | }
46 |
47 | L.MultiPolyline = createMulti(L.Polyline);
48 | L.MultiPolygon = createMulti(L.Polygon);
49 |
50 | L.multiPolyline = function (latlngs, options) {
51 | return new L.MultiPolyline(latlngs, options);
52 | };
53 |
54 | L.multiPolygon = function (latlngs, options) {
55 | return new L.MultiPolygon(latlngs, options);
56 | };
57 | }());
58 |
--------------------------------------------------------------------------------
/debug/map/image-overlay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/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-2013, 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/hacks/jitter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
38 | describe('#pointToSegmentDistance & #closestPointOnSegment', function () {
39 |
40 | var p1 = new L.Point(0, 10);
41 | var p2 = new L.Point(10, 0);
42 | var p = new L.Point(0, 0);
43 |
44 | it('calculates distance from point to segment', function () {
45 | expect(L.LineUtil.pointToSegmentDistance(p, p1, p2)).to.eql(Math.sqrt(200) / 2);
46 | });
47 |
48 | it('calculates point closest to segment', function () {
49 | expect(L.LineUtil.closestPointOnSegment(p, p1, p2)).to.eql(new L.Point(5, 5));
50 | });
51 | });
52 |
53 | describe('#simplify', function () {
54 | it('simplifies polylines according to tolerance', function () {
55 | var points = [
56 | new L.Point(0, 0),
57 | new L.Point(0.01, 0),
58 | new L.Point(0.5, 0.01),
59 | new L.Point(0.7, 0),
60 | new L.Point(1, 0),
61 | new L.Point(1.999, 0.999),
62 | new L.Point(2, 1)
63 | ];
64 |
65 | var simplified = L.LineUtil.simplify(points, 0.1);
66 |
67 | expect(simplified).to.eql([
68 | new L.Point(0, 0),
69 | new L.Point(1, 0),
70 | new L.Point(2, 1)
71 | ]);
72 | });
73 | });
74 |
75 | });
76 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/layer/marker/Marker.Popup.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Popup extension to L.Marker, adding popup-related methods.
3 | */
4 |
5 | L.Marker.include({
6 | openPopup: function () {
7 | if (this._popup && this._map && !this._map.hasLayer(this._popup)) {
8 | this._popup.setLatLng(this._latlng);
9 | this._map.openPopup(this._popup);
10 | if (this._popup.options.closeButton) {
11 | this._popup._closeButton.focus();
12 | }
13 | }
14 |
15 | return this;
16 | },
17 |
18 | closePopup: function () {
19 | if (this._popup) {
20 | this._popup._close();
21 | }
22 | return this;
23 | },
24 |
25 | togglePopup: function () {
26 | if (this._popup) {
27 | if (this._popup._isOpen) {
28 | this.closePopup();
29 | } else {
30 | this.openPopup();
31 | }
32 | }
33 | return this;
34 | },
35 |
36 | bindPopup: function (content, options) {
37 | var anchor = L.point(this.options.icon.options.popupAnchor || [0, 0]);
38 |
39 | anchor = anchor.add(L.Popup.prototype.options.offset);
40 |
41 | if (options && options.offset) {
42 | anchor = anchor.add(options.offset);
43 | }
44 |
45 | options = L.extend({offset: anchor}, options);
46 |
47 | if (!this._popupHandlersAdded) {
48 | this
49 | .on('click', this.togglePopup, this)
50 | .on('remove', this.closePopup, this)
51 | .on('move', this._movePopup, this);
52 | this._popupHandlersAdded = true;
53 | }
54 |
55 | if (content instanceof L.Popup) {
56 | L.setOptions(content, options);
57 | this._popup = content;
58 | } else {
59 | this._popup = new L.Popup(options, this)
60 | .setContent(content);
61 | }
62 |
63 | return this;
64 | },
65 |
66 | setPopupContent: function (content) {
67 | if (this._popup) {
68 | this._popup.setContent(content);
69 | }
70 | return this;
71 | },
72 |
73 | unbindPopup: function () {
74 | if (this._popup) {
75 | this._popup = null;
76 | this
77 | .off('click', this.togglePopup, this)
78 | .off('remove', this.closePopup, this)
79 | .off('move', this._movePopup, this);
80 | this._popupHandlersAdded = false;
81 | }
82 | return this;
83 | },
84 |
85 | getPopup: function () {
86 | return this._popup;
87 | },
88 |
89 | _movePopup: function (e) {
90 | this._popup.setLatLng(e.latlng);
91 | }
92 | });
93 |
--------------------------------------------------------------------------------
/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' });
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 |
--------------------------------------------------------------------------------
/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 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/layer/tile/TileLayer.WMS.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.TileLayer.WMS is used for putting WMS tile layers on the map.
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 | initialize: function (url, options) { // (String, Object)
18 |
19 | this._url = url;
20 |
21 | var wmsParams = L.extend({}, this.defaultWmsParams),
22 | tileSize = options.tileSize || this.options.tileSize;
23 |
24 | if (options.detectRetina && L.Browser.retina) {
25 | wmsParams.width = wmsParams.height = tileSize * 2;
26 | } else {
27 | wmsParams.width = wmsParams.height = tileSize;
28 | }
29 |
30 | for (var i in options) {
31 | // all keys that are not TileLayer options go to WMS params
32 | if (!this.options.hasOwnProperty(i) && i !== 'crs') {
33 | wmsParams[i] = options[i];
34 | }
35 | }
36 |
37 | this.wmsParams = wmsParams;
38 |
39 | L.setOptions(this, options);
40 | },
41 |
42 | onAdd: function (map) {
43 |
44 | this._crs = this.options.crs || map.options.crs;
45 |
46 | this._wmsVersion = parseFloat(this.wmsParams.version);
47 |
48 | var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
49 | this.wmsParams[projectionKey] = this._crs.code;
50 |
51 | L.TileLayer.prototype.onAdd.call(this, map);
52 | },
53 |
54 | getTileUrl: function (tilePoint) { // (Point, Number) -> String
55 |
56 | var map = this._map,
57 | tileSize = this.options.tileSize,
58 |
59 | nwPoint = tilePoint.multiplyBy(tileSize),
60 | sePoint = nwPoint.add([tileSize, tileSize]),
61 |
62 | nw = this._crs.project(map.unproject(nwPoint, tilePoint.z)),
63 | se = this._crs.project(map.unproject(sePoint, tilePoint.z)),
64 | bbox = this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ?
65 | [se.y, nw.x, nw.y, se.x].join(',') :
66 | [nw.x, se.y, se.x, nw.y].join(','),
67 |
68 | url = L.Util.template(this._url, {s: this._getSubdomain(tilePoint)});
69 |
70 | return url + L.Util.getParamString(this.wmsParams, url, true) + '&BBOX=' + bbox;
71 | },
72 |
73 | setParams: function (params, noRedraw) {
74 |
75 | L.extend(this.wmsParams, params);
76 |
77 | if (!noRedraw) {
78 | this.redraw();
79 | }
80 |
81 | return this;
82 | }
83 | });
84 |
85 | L.tileLayer.wms = function (url, options) {
86 | return new L.TileLayer.WMS(url, options);
87 | };
88 |
--------------------------------------------------------------------------------
/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 only external ring
40 | expect(polygon.getLatLngs()).to.eql([L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])]);
41 | });
42 | });
43 |
44 | describe("#setLatLngs", function () {
45 | it("doesn't overwrite the given latlng array", function () {
46 | var originalLatLngs = [
47 | [1, 2],
48 | [3, 4]
49 | ];
50 | var sourceLatLngs = originalLatLngs.slice();
51 |
52 | var polygon = new L.Polygon(sourceLatLngs);
53 |
54 | polygon.setLatLngs(sourceLatLngs);
55 |
56 | expect(sourceLatLngs).to.eql(originalLatLngs);
57 | });
58 |
59 | it("can be set external ring and holes", function () {
60 | var latLngs = [
61 | [ //external rink
62 | [0, 10], [10, 10], [10, 0]
63 | ], [ //hole
64 | [2, 3], [2, 4], [3, 4]
65 | ]
66 | ];
67 |
68 | var polygon = new L.Polygon([]);
69 | polygon.setLatLngs(latLngs);
70 |
71 | //getLatLngs() returns only external ring
72 | expect(polygon.getLatLngs()).to.eql([L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])]);
73 | });
74 | });
75 |
76 | describe("#spliceLatLngs", function () {
77 | it("splices the internal latLngs", function () {
78 | var latLngs = [
79 | [1, 2],
80 | [3, 4],
81 | [5, 6]
82 | ];
83 |
84 | var polygon = new L.Polygon(latLngs);
85 |
86 | polygon.spliceLatLngs(1, 1, [7, 8]);
87 |
88 | expect(polygon._latlngs).to.eql([L.latLng([1, 2]), L.latLng([7, 8]), L.latLng([5, 6])]);
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/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 | includes: L.Mixin.Events,
8 |
9 | statics: {
10 | EVENTS: 'click dblclick mouseover mouseout mousemove contextmenu popupopen popupclose'
11 | },
12 |
13 | addLayer: function (layer) {
14 | if (this.hasLayer(layer)) {
15 | return this;
16 | }
17 |
18 | if ('on' in layer) {
19 | layer.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
20 | }
21 |
22 | L.LayerGroup.prototype.addLayer.call(this, layer);
23 |
24 | if (this._popupContent && layer.bindPopup) {
25 | layer.bindPopup(this._popupContent, this._popupOptions);
26 | }
27 |
28 | return this.fire('layeradd', {layer: layer});
29 | },
30 |
31 | removeLayer: function (layer) {
32 | if (!this.hasLayer(layer)) {
33 | return this;
34 | }
35 | if (layer in this._layers) {
36 | layer = this._layers[layer];
37 | }
38 |
39 | layer.off(L.FeatureGroup.EVENTS, this._propagateEvent, this);
40 |
41 | L.LayerGroup.prototype.removeLayer.call(this, layer);
42 |
43 | if (this._popupContent) {
44 | this.invoke('unbindPopup');
45 | }
46 |
47 | return this.fire('layerremove', {layer: layer});
48 | },
49 |
50 | bindPopup: function (content, options) {
51 | this._popupContent = content;
52 | this._popupOptions = options;
53 | return this.invoke('bindPopup', content, options);
54 | },
55 |
56 | openPopup: function (latlng) {
57 | // open popup on the first layer
58 | for (var id in this._layers) {
59 | this._layers[id].openPopup(latlng);
60 | break;
61 | }
62 | return this;
63 | },
64 |
65 | setStyle: function (style) {
66 | return this.invoke('setStyle', style);
67 | },
68 |
69 | bringToFront: function () {
70 | return this.invoke('bringToFront');
71 | },
72 |
73 | bringToBack: function () {
74 | return this.invoke('bringToBack');
75 | },
76 |
77 | getBounds: function () {
78 | var bounds = new L.LatLngBounds();
79 |
80 | this.eachLayer(function (layer) {
81 | bounds.extend(layer instanceof L.Marker ? layer.getLatLng() : layer.getBounds());
82 | });
83 |
84 | return bounds;
85 | },
86 |
87 | _propagateEvent: function (e) {
88 | e = L.extend({}, e, {
89 | layer: e.target,
90 | target: this
91 | });
92 | this.fire(e.type, e);
93 | }
94 | });
95 |
96 | L.featureGroup = function (layers) {
97 | return new L.FeatureGroup(layers);
98 | };
99 |
--------------------------------------------------------------------------------
/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/dom/DomUtilSpec.js:
--------------------------------------------------------------------------------
1 | describe('DomUtil', function () {
2 | var el;
3 |
4 | beforeEach(function () {
5 | el = document.createElement('div');
6 | el.style.position = 'absolute';
7 | el.style.top = el.style.left = '-10000px';
8 | document.body.appendChild(el);
9 | });
10 |
11 | afterEach(function () {
12 | document.body.removeChild(el);
13 | });
14 |
15 | describe('#get', function () {
16 | it('gets element by id if the given argument is string', function () {
17 | el.id = 'testId';
18 | expect(L.DomUtil.get(el.id)).to.eql(el);
19 | });
20 |
21 | it('returns the element if it is given as an argument', function () {
22 | expect(L.DomUtil.get(el)).to.eql(el);
23 | });
24 | });
25 |
26 | describe('#addClass, #removeClass, #hasClass', function () {
27 | it('has defined class for test element', function () {
28 | el.className = 'bar foo baz ';
29 | expect(L.DomUtil.hasClass(el, 'foo')).to.be.ok();
30 | expect(L.DomUtil.hasClass(el, 'bar')).to.be.ok();
31 | expect(L.DomUtil.hasClass(el, 'baz')).to.be.ok();
32 | expect(L.DomUtil.hasClass(el, 'boo')).to.not.be.ok();
33 | });
34 |
35 | it('adds or removes the class', function () {
36 | el.className = '';
37 | L.DomUtil.addClass(el, 'foo');
38 |
39 | expect(el.className).to.eql('foo');
40 | expect(L.DomUtil.hasClass(el, 'foo')).to.be.ok();
41 |
42 | L.DomUtil.addClass(el, 'bar');
43 | expect(el.className).to.eql('foo bar');
44 | expect(L.DomUtil.hasClass(el, 'foo')).to.be.ok();
45 |
46 | L.DomUtil.removeClass(el, 'foo');
47 | expect(el.className).to.eql('bar');
48 | expect(L.DomUtil.hasClass(el, 'foo')).to.not.be.ok();
49 |
50 | el.className = 'foo bar barz';
51 | L.DomUtil.removeClass(el, 'bar');
52 | expect(el.className).to.eql('foo barz');
53 | });
54 | });
55 |
56 | describe('#getViewportOffset', function () {
57 | it('calculates the viewport offset of an element', function () {
58 | var div = document.createElement('div');
59 | div.style.position = 'absolute';
60 | div.style.top = '100px';
61 | div.style.left = '200px';
62 | div.style.border = '10px solid black';
63 | div.style.padding = '50px';
64 | div.style.visibility = 'hidden';
65 |
66 | var div2 = document.createElement('div');
67 | div.style.marginTop = '100px';
68 |
69 | div.appendChild(div2);
70 |
71 | document.body.appendChild(div);
72 |
73 | expect(L.DomUtil.getViewportOffset(div2)).to.eql(new L.Point(260, 260));
74 |
75 | document.body.removeChild(div);
76 | });
77 | });
78 |
79 | // describe('#setPosition', noSpecs);
80 |
81 | // describe('#getStyle', noSpecs);
82 | });
83 |
--------------------------------------------------------------------------------
/src/layer/vector/Circle.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Circle is a circle overlay (with a certain radius in meters).
3 | */
4 |
5 | L.Circle = L.Path.extend({
6 | initialize: function (latlng, radius, options) {
7 | L.Path.prototype.initialize.call(this, options);
8 |
9 | this._latlng = L.latLng(latlng);
10 | this._mRadius = radius;
11 | },
12 |
13 | options: {
14 | fill: true
15 | },
16 |
17 | setLatLng: function (latlng) {
18 | this._latlng = L.latLng(latlng);
19 | return this.redraw();
20 | },
21 |
22 | setRadius: function (radius) {
23 | this._mRadius = radius;
24 | return this.redraw();
25 | },
26 |
27 | projectLatlngs: function () {
28 | var lngRadius = this._getLngRadius(),
29 | latlng = this._latlng,
30 | pointLeft = this._map.latLngToLayerPoint([latlng.lat, latlng.lng - lngRadius]);
31 |
32 | this._point = this._map.latLngToLayerPoint(latlng);
33 | this._radius = Math.max(this._point.x - pointLeft.x, 1);
34 | },
35 |
36 | getBounds: function () {
37 | var lngRadius = this._getLngRadius(),
38 | latRadius = (this._mRadius / 40075017) * 360,
39 | latlng = this._latlng;
40 |
41 | return new L.LatLngBounds(
42 | [latlng.lat - latRadius, latlng.lng - lngRadius],
43 | [latlng.lat + latRadius, latlng.lng + lngRadius]);
44 | },
45 |
46 | getLatLng: function () {
47 | return this._latlng;
48 | },
49 |
50 | getPathString: function () {
51 | var p = this._point,
52 | r = this._radius;
53 |
54 | if (this._checkIfEmpty()) {
55 | return '';
56 | }
57 |
58 | if (L.Browser.svg) {
59 | return 'M' + p.x + ',' + (p.y - r) +
60 | 'A' + r + ',' + r + ',0,1,1,' +
61 | (p.x - 0.1) + ',' + (p.y - r) + ' z';
62 | } else {
63 | p._round();
64 | r = Math.round(r);
65 | return 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r + ' 0,' + (65535 * 360);
66 | }
67 | },
68 |
69 | getRadius: function () {
70 | return this._mRadius;
71 | },
72 |
73 | // TODO Earth hardcoded, move into projection code!
74 |
75 | _getLatRadius: function () {
76 | return (this._mRadius / 40075017) * 360;
77 | },
78 |
79 | _getLngRadius: function () {
80 | return this._getLatRadius() / Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat);
81 | },
82 |
83 | _checkIfEmpty: function () {
84 | if (!this._map) {
85 | return false;
86 | }
87 | var vp = this._map._pathViewport,
88 | r = this._radius,
89 | p = this._point;
90 |
91 | return p.x - r > vp.max.x || p.y - r > vp.max.y ||
92 | p.x + r < vp.min.x || p.y + r < vp.min.y;
93 | }
94 | });
95 |
96 | L.circle = function (latlng, radius, options) {
97 | return new L.Circle(latlng, radius, options);
98 | };
99 |
--------------------------------------------------------------------------------
/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.Class.extend({
7 | initialize: function (layers) {
8 | this._layers = {};
9 |
10 | var i, len;
11 |
12 | if (layers) {
13 | for (i = 0, len = layers.length; i < len; i++) {
14 | this.addLayer(layers[i]);
15 | }
16 | }
17 | },
18 |
19 | addLayer: function (layer) {
20 | var id = this.getLayerId(layer);
21 |
22 | this._layers[id] = layer;
23 |
24 | if (this._map) {
25 | this._map.addLayer(layer);
26 | }
27 |
28 | return this;
29 | },
30 |
31 | removeLayer: function (layer) {
32 | var id = layer in this._layers ? layer : this.getLayerId(layer);
33 |
34 | if (this._map && this._layers[id]) {
35 | this._map.removeLayer(this._layers[id]);
36 | }
37 |
38 | delete this._layers[id];
39 |
40 | return this;
41 | },
42 |
43 | hasLayer: function (layer) {
44 | if (!layer) { return false; }
45 |
46 | return (layer in this._layers || this.getLayerId(layer) in this._layers);
47 | },
48 |
49 | clearLayers: function () {
50 | this.eachLayer(this.removeLayer, this);
51 | return this;
52 | },
53 |
54 | invoke: function (methodName) {
55 | var args = Array.prototype.slice.call(arguments, 1),
56 | i, layer;
57 |
58 | for (i in this._layers) {
59 | layer = this._layers[i];
60 |
61 | if (layer[methodName]) {
62 | layer[methodName].apply(layer, args);
63 | }
64 | }
65 |
66 | return this;
67 | },
68 |
69 | onAdd: function (map) {
70 | this._map = map;
71 | this.eachLayer(map.addLayer, map);
72 | },
73 |
74 | onRemove: function (map) {
75 | this.eachLayer(map.removeLayer, map);
76 | this._map = null;
77 | },
78 |
79 | addTo: function (map) {
80 | map.addLayer(this);
81 | return this;
82 | },
83 |
84 | eachLayer: function (method, context) {
85 | for (var i in this._layers) {
86 | method.call(context, this._layers[i]);
87 | }
88 | return this;
89 | },
90 |
91 | getLayer: function (id) {
92 | return this._layers[id];
93 | },
94 |
95 | getLayers: function () {
96 | var layers = [];
97 |
98 | for (var i in this._layers) {
99 | layers.push(this._layers[i]);
100 | }
101 | return layers;
102 | },
103 |
104 | setZIndex: function (zIndex) {
105 | return this.invoke('setZIndex', zIndex);
106 | },
107 |
108 | getLayerId: function (layer) {
109 | return L.stamp(layer);
110 | }
111 | });
112 |
113 | L.layerGroup = function (layers) {
114 | return new L.LayerGroup(layers);
115 | };
116 |
--------------------------------------------------------------------------------
/src/control/Control.Zoom.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Control.Zoom is used for the default zoom buttons on the map.
3 | */
4 |
5 | L.Control.Zoom = L.Control.extend({
6 | options: {
7 | position: 'topleft',
8 | zoomInText: '+',
9 | zoomInTitle: 'Zoom in',
10 | zoomOutText: '-',
11 | zoomOutTitle: 'Zoom out'
12 | },
13 |
14 | onAdd: function (map) {
15 | var zoomName = 'leaflet-control-zoom',
16 | container = L.DomUtil.create('div', zoomName + ' leaflet-bar');
17 |
18 | this._map = map;
19 |
20 | this._zoomInButton = this._createButton(
21 | this.options.zoomInText, this.options.zoomInTitle,
22 | zoomName + '-in', container, this._zoomIn, this);
23 | this._zoomOutButton = this._createButton(
24 | this.options.zoomOutText, this.options.zoomOutTitle,
25 | zoomName + '-out', container, this._zoomOut, this);
26 |
27 | this._updateDisabled();
28 | map.on('zoomend zoomlevelschange', this._updateDisabled, this);
29 |
30 | return container;
31 | },
32 |
33 | onRemove: function (map) {
34 | map.off('zoomend zoomlevelschange', this._updateDisabled, this);
35 | },
36 |
37 | _zoomIn: function (e) {
38 | this._map.zoomIn(e.shiftKey ? 3 : 1);
39 | },
40 |
41 | _zoomOut: function (e) {
42 | this._map.zoomOut(e.shiftKey ? 3 : 1);
43 | },
44 |
45 | _createButton: function (html, title, className, container, fn, context) {
46 | var link = L.DomUtil.create('a', className, container);
47 | link.innerHTML = html;
48 | link.href = '#';
49 | link.title = title;
50 |
51 | var stop = L.DomEvent.stopPropagation;
52 |
53 | L.DomEvent
54 | .on(link, 'click', stop)
55 | .on(link, 'mousedown', stop)
56 | .on(link, 'dblclick', stop)
57 | .on(link, 'click', L.DomEvent.preventDefault)
58 | .on(link, 'click', fn, context)
59 | .on(link, 'click', this._refocusOnMap, context);
60 |
61 | return link;
62 | },
63 |
64 | _updateDisabled: function () {
65 | var map = this._map,
66 | className = 'leaflet-disabled';
67 |
68 | L.DomUtil.removeClass(this._zoomInButton, className);
69 | L.DomUtil.removeClass(this._zoomOutButton, className);
70 |
71 | if (map._zoom === map.getMinZoom()) {
72 | L.DomUtil.addClass(this._zoomOutButton, className);
73 | }
74 | if (map._zoom === map.getMaxZoom()) {
75 | L.DomUtil.addClass(this._zoomInButton, className);
76 | }
77 | }
78 | });
79 |
80 | L.Map.mergeOptions({
81 | zoomControl: true
82 | });
83 |
84 | L.Map.addInitHook(function () {
85 | if (this.options.zoomControl) {
86 | this.zoomControl = new L.Control.Zoom();
87 | this.addControl(this.zoomControl);
88 | }
89 | });
90 |
91 | L.control.zoom = function (options) {
92 | return new L.Control.Zoom(options);
93 | };
94 |
95 |
--------------------------------------------------------------------------------
/src/layer/vector/Polygon.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Polygon is used to display polygons on a map.
3 | */
4 |
5 | L.Polygon = L.Polyline.extend({
6 | options: {
7 | fill: true
8 | },
9 |
10 | initialize: function (latlngs, options) {
11 | L.Polyline.prototype.initialize.call(this, latlngs, options);
12 | this._initWithHoles(latlngs);
13 | },
14 |
15 | _initWithHoles: function (latlngs) {
16 | var i, len, hole;
17 | if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) {
18 | this._latlngs = this._convertLatLngs(latlngs[0]);
19 | this._holes = latlngs.slice(1);
20 |
21 | for (i = 0, len = this._holes.length; i < len; i++) {
22 | hole = this._holes[i] = this._convertLatLngs(this._holes[i]);
23 | if (hole[0].equals(hole[hole.length - 1])) {
24 | hole.pop();
25 | }
26 | }
27 | }
28 |
29 | // filter out last point if its equal to the first one
30 | latlngs = this._latlngs;
31 |
32 | if (latlngs.length >= 2 && latlngs[0].equals(latlngs[latlngs.length - 1])) {
33 | latlngs.pop();
34 | }
35 | },
36 |
37 | projectLatlngs: function () {
38 | L.Polyline.prototype.projectLatlngs.call(this);
39 |
40 | // project polygon holes points
41 | // TODO move this logic to Polyline to get rid of duplication
42 | this._holePoints = [];
43 |
44 | if (!this._holes) { return; }
45 |
46 | var i, j, len, len2;
47 |
48 | for (i = 0, len = this._holes.length; i < len; i++) {
49 | this._holePoints[i] = [];
50 |
51 | for (j = 0, len2 = this._holes[i].length; j < len2; j++) {
52 | this._holePoints[i][j] = this._map.latLngToLayerPoint(this._holes[i][j]);
53 | }
54 | }
55 | },
56 |
57 | setLatLngs: function (latlngs) {
58 | if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) {
59 | this._initWithHoles(latlngs);
60 | return this.redraw();
61 | } else {
62 | return L.Polyline.prototype.setLatLngs.call(this, latlngs);
63 | }
64 | },
65 |
66 | _clipPoints: function () {
67 | var points = this._originalPoints,
68 | newParts = [];
69 |
70 | this._parts = [points].concat(this._holePoints);
71 |
72 | if (this.options.noClip) { return; }
73 |
74 | for (var i = 0, len = this._parts.length; i < len; i++) {
75 | var clipped = L.PolyUtil.clipPolygon(this._parts[i], this._map._pathViewport);
76 | if (clipped.length) {
77 | newParts.push(clipped);
78 | }
79 | }
80 |
81 | this._parts = newParts;
82 | },
83 |
84 | _getPathPartStr: function (points) {
85 | var str = L.Polyline.prototype._getPathPartStr.call(this, points);
86 | return str + (L.Browser.svg ? 'z' : 'x');
87 | }
88 | });
89 |
90 | L.polygon = function (latlngs, options) {
91 | return new L.Polygon(latlngs, options);
92 | };
93 |
--------------------------------------------------------------------------------
/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 | options: {
7 | /*
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 | */
18 | className: ''
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;
44 | if (!oldIcon || oldIcon.tagName !== 'IMG') {
45 | img = this._createImg(src);
46 | } else {
47 | img = this._createImg(src, oldIcon);
48 | }
49 | this._setIconStyles(img, name);
50 |
51 | return img;
52 | },
53 |
54 | _setIconStyles: function (img, name) {
55 | var options = this.options,
56 | size = L.point(options[name + 'Size']),
57 | anchor;
58 |
59 | if (name === 'shadow') {
60 | anchor = L.point(options.shadowAnchor || options.iconAnchor);
61 | } else {
62 | anchor = L.point(options.iconAnchor);
63 | }
64 |
65 | if (!anchor && size) {
66 | anchor = size.divideBy(2, true);
67 | }
68 |
69 | img.className = 'leaflet-marker-' + name + ' ' + options.className;
70 |
71 | if (anchor) {
72 | img.style.marginLeft = (-anchor.x) + 'px';
73 | img.style.marginTop = (-anchor.y) + 'px';
74 | }
75 |
76 | if (size) {
77 | img.style.width = size.x + 'px';
78 | img.style.height = size.y + 'px';
79 | }
80 | },
81 |
82 | _createImg: function (src, el) {
83 | el = el || document.createElement('img');
84 | el.src = src;
85 | return el;
86 | },
87 |
88 | _getIconUrl: function (name) {
89 | if (L.Browser.retina && this.options[name + 'RetinaUrl']) {
90 | return this.options[name + 'RetinaUrl'];
91 | }
92 | return this.options[name + 'Url'];
93 | }
94 | });
95 |
96 | L.icon = function (options) {
97 | return new L.Icon(options);
98 | };
99 |
--------------------------------------------------------------------------------
/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 | watch: false,
8 | setView: false,
9 | maxZoom: Infinity,
10 | timeout: 10000,
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 |
70 | latAccuracy = 180 * pos.coords.accuracy / 40075017,
71 | lngAccuracy = latAccuracy / Math.cos(L.LatLng.DEG_TO_RAD * lat),
72 |
73 | bounds = L.latLngBounds(
74 | [lat - latAccuracy, lng - lngAccuracy],
75 | [lat + latAccuracy, lng + lngAccuracy]),
76 |
77 | options = this._locateOptions;
78 |
79 | if (options.setView) {
80 | var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom);
81 | this.setView(latlng, zoom);
82 | }
83 |
84 | var data = {
85 | latlng: latlng,
86 | bounds: bounds,
87 | timestamp: pos.timestamp
88 | };
89 |
90 | for (var i in pos.coords) {
91 | if (typeof pos.coords[i] === 'number') {
92 | data[i] = pos.coords[i];
93 | }
94 | }
95 |
96 | this.fire('locationfound', data);
97 | }
98 | });
99 |
--------------------------------------------------------------------------------
/src/map/anim/Map.PanAnimation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Extends L.Map to handle panning animations.
3 | */
4 |
5 | L.Map.include({
6 |
7 | setView: function (center, zoom, options) {
8 |
9 | zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
10 | center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds);
11 | options = options || {};
12 |
13 | if (this._panAnim) {
14 | this._panAnim.stop();
15 | }
16 |
17 | if (this._loaded && !options.reset && options !== true) {
18 |
19 | if (options.animate !== undefined) {
20 | options.zoom = L.extend({animate: options.animate}, options.zoom);
21 | options.pan = L.extend({animate: options.animate}, options.pan);
22 | }
23 |
24 | // try animating pan or zoom
25 | var animated = (this._zoom !== zoom) ?
26 | this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
27 | this._tryAnimatedPan(center, options.pan);
28 |
29 | if (animated) {
30 | // prevent resize handler call, the view will refresh after animation anyway
31 | clearTimeout(this._sizeTimer);
32 | return this;
33 | }
34 | }
35 |
36 | // animation didn't start, just reset the map view
37 | this._resetView(center, zoom);
38 |
39 | return this;
40 | },
41 |
42 | panBy: function (offset, options) {
43 | offset = L.point(offset).round();
44 | options = options || {};
45 |
46 | if (!offset.x && !offset.y) {
47 | return this;
48 | }
49 |
50 | if (!this._panAnim) {
51 | this._panAnim = new L.PosAnimation();
52 |
53 | this._panAnim.on({
54 | 'step': this._onPanTransitionStep,
55 | 'end': this._onPanTransitionEnd
56 | }, this);
57 | }
58 |
59 | // don't fire movestart if animating inertia
60 | if (!options.noMoveStart) {
61 | this.fire('movestart');
62 | }
63 |
64 | // animate pan unless animate: false specified
65 | if (options.animate !== false) {
66 | L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim');
67 |
68 | var newPos = this._getMapPanePos().subtract(offset);
69 | this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
70 | } else {
71 | this._rawPanBy(offset);
72 | this.fire('move').fire('moveend');
73 | }
74 |
75 | return this;
76 | },
77 |
78 | _onPanTransitionStep: function () {
79 | this.fire('move');
80 | },
81 |
82 | _onPanTransitionEnd: function () {
83 | L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim');
84 | this.fire('moveend');
85 | },
86 |
87 | _tryAnimatedPan: function (center, options) {
88 | // difference between the new and current centers in pixels
89 | var offset = this._getCenterOffset(center)._floor();
90 |
91 | // don't animate too far unless animate: true specified in options
92 | if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }
93 |
94 | this.panBy(offset, options);
95 |
96 | return true;
97 | }
98 | });
99 |
--------------------------------------------------------------------------------
/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 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/debug/vector/vector-canvas.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/src/control/Control.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Control is a base class for implementing map controls. Handles positioning.
3 | * All other controls extend from this class.
4 | */
5 |
6 | L.Control = L.Class.extend({
7 | options: {
8 | position: 'topright'
9 | },
10 |
11 | initialize: function (options) {
12 | L.setOptions(this, options);
13 | },
14 |
15 | getPosition: function () {
16 | return this.options.position;
17 | },
18 |
19 | setPosition: function (position) {
20 | var map = this._map;
21 |
22 | if (map) {
23 | map.removeControl(this);
24 | }
25 |
26 | this.options.position = position;
27 |
28 | if (map) {
29 | map.addControl(this);
30 | }
31 |
32 | return this;
33 | },
34 |
35 | getContainer: function () {
36 | return this._container;
37 | },
38 |
39 | addTo: function (map) {
40 | this._map = map;
41 |
42 | var container = this._container = this.onAdd(map),
43 | pos = this.getPosition(),
44 | corner = map._controlCorners[pos];
45 |
46 | L.DomUtil.addClass(container, 'leaflet-control');
47 |
48 | if (pos.indexOf('bottom') !== -1) {
49 | corner.insertBefore(container, corner.firstChild);
50 | } else {
51 | corner.appendChild(container);
52 | }
53 |
54 | return this;
55 | },
56 |
57 | removeFrom: function (map) {
58 | var pos = this.getPosition(),
59 | corner = map._controlCorners[pos];
60 |
61 | corner.removeChild(this._container);
62 | this._map = null;
63 |
64 | if (this.onRemove) {
65 | this.onRemove(map);
66 | }
67 |
68 | return this;
69 | },
70 |
71 | _refocusOnMap: function () {
72 | if (this._map) {
73 | this._map.getContainer().focus();
74 | }
75 | }
76 | });
77 |
78 | L.control = function (options) {
79 | return new L.Control(options);
80 | };
81 |
82 |
83 | // adds control-related methods to L.Map
84 |
85 | L.Map.include({
86 | addControl: function (control) {
87 | control.addTo(this);
88 | return this;
89 | },
90 |
91 | removeControl: function (control) {
92 | control.removeFrom(this);
93 | return this;
94 | },
95 |
96 | _initControlPos: function () {
97 | var corners = this._controlCorners = {},
98 | l = 'leaflet-',
99 | container = this._controlContainer =
100 | L.DomUtil.create('div', l + 'control-container', this._container);
101 |
102 | function createCorner(vSide, hSide) {
103 | var className = l + vSide + ' ' + l + hSide;
104 |
105 | corners[vSide + hSide] = L.DomUtil.create('div', className, container);
106 | }
107 |
108 | createCorner('top', 'left');
109 | createCorner('top', 'right');
110 | createCorner('bottom', 'left');
111 | createCorner('bottom', 'right');
112 | },
113 |
114 | _clearControlPos: function () {
115 | this._container.removeChild(this._controlContainer);
116 | }
117 | });
118 |
--------------------------------------------------------------------------------
/src/geometry/Point.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Point represents a point with x and y coordinates.
3 | */
4 |
5 | L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
6 | this.x = (round ? Math.round(x) : x);
7 | this.y = (round ? Math.round(y) : y);
8 | };
9 |
10 | L.Point.prototype = {
11 |
12 | clone: function () {
13 | return new L.Point(this.x, this.y);
14 | },
15 |
16 | // non-destructive, returns a new point
17 | add: function (point) {
18 | return this.clone()._add(L.point(point));
19 | },
20 |
21 | // destructive, used directly for performance in situations where it's safe to modify existing point
22 | _add: function (point) {
23 | this.x += point.x;
24 | this.y += point.y;
25 | return this;
26 | },
27 |
28 | subtract: function (point) {
29 | return this.clone()._subtract(L.point(point));
30 | },
31 |
32 | _subtract: function (point) {
33 | this.x -= point.x;
34 | this.y -= point.y;
35 | return this;
36 | },
37 |
38 | divideBy: function (num) {
39 | return this.clone()._divideBy(num);
40 | },
41 |
42 | _divideBy: function (num) {
43 | this.x /= num;
44 | this.y /= num;
45 | return this;
46 | },
47 |
48 | multiplyBy: function (num) {
49 | return this.clone()._multiplyBy(num);
50 | },
51 |
52 | _multiplyBy: function (num) {
53 | this.x *= num;
54 | this.y *= num;
55 | return this;
56 | },
57 |
58 | round: function () {
59 | return this.clone()._round();
60 | },
61 |
62 | _round: function () {
63 | this.x = Math.round(this.x);
64 | this.y = Math.round(this.y);
65 | return this;
66 | },
67 |
68 | floor: function () {
69 | return this.clone()._floor();
70 | },
71 |
72 | _floor: function () {
73 | this.x = Math.floor(this.x);
74 | this.y = Math.floor(this.y);
75 | return this;
76 | },
77 |
78 | distanceTo: function (point) {
79 | point = L.point(point);
80 |
81 | var x = point.x - this.x,
82 | y = point.y - this.y;
83 |
84 | return Math.sqrt(x * x + y * y);
85 | },
86 |
87 | equals: function (point) {
88 | point = L.point(point);
89 |
90 | return point.x === this.x &&
91 | point.y === this.y;
92 | },
93 |
94 | contains: function (point) {
95 | point = L.point(point);
96 |
97 | return Math.abs(point.x) <= Math.abs(this.x) &&
98 | Math.abs(point.y) <= Math.abs(this.y);
99 | },
100 |
101 | toString: function () {
102 | return 'Point(' +
103 | L.Util.formatNum(this.x) + ', ' +
104 | L.Util.formatNum(this.y) + ')';
105 | }
106 | };
107 |
108 | L.point = function (x, y, round) {
109 | if (x instanceof L.Point) {
110 | return x;
111 | }
112 | if (L.Util.isArray(x)) {
113 | return new L.Point(x[0], x[1]);
114 | }
115 | if (x === undefined || x === null) {
116 | return x;
117 | }
118 | return new L.Point(x, y, round);
119 | };
120 |
--------------------------------------------------------------------------------
/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 | if (this._initHooks) {
20 | this.callInitHooks();
21 | }
22 | };
23 |
24 | // instantiate class without calling constructor
25 | var F = function () {};
26 | F.prototype = this.prototype;
27 |
28 | var proto = new F();
29 | proto.constructor = NewClass;
30 |
31 | NewClass.prototype = proto;
32 |
33 | //inherit parent's statics
34 | for (var i in this) {
35 | if (this.hasOwnProperty(i) && i !== 'prototype') {
36 | NewClass[i] = this[i];
37 | }
38 | }
39 |
40 | // mix static properties into the class
41 | if (props.statics) {
42 | L.extend(NewClass, props.statics);
43 | delete props.statics;
44 | }
45 |
46 | // mix includes into the prototype
47 | if (props.includes) {
48 | L.Util.extend.apply(null, [proto].concat(props.includes));
49 | delete props.includes;
50 | }
51 |
52 | // merge options
53 | if (props.options && proto.options) {
54 | props.options = L.extend({}, proto.options, props.options);
55 | }
56 |
57 | // mix given properties into the prototype
58 | L.extend(proto, props);
59 |
60 | proto._initHooks = [];
61 |
62 | var parent = this;
63 | // jshint camelcase: false
64 | NewClass.__super__ = parent.prototype;
65 |
66 | // add method for calling all hooks
67 | proto.callInitHooks = function () {
68 |
69 | if (this._initHooksCalled) { return; }
70 |
71 | if (parent.prototype.callInitHooks) {
72 | parent.prototype.callInitHooks.call(this);
73 | }
74 |
75 | this._initHooksCalled = true;
76 |
77 | for (var i = 0, len = proto._initHooks.length; i < len; i++) {
78 | proto._initHooks[i].call(this);
79 | }
80 | };
81 |
82 | return NewClass;
83 | };
84 |
85 |
86 | // method for adding properties to prototype
87 | L.Class.include = function (props) {
88 | L.extend(this.prototype, props);
89 | };
90 |
91 | // merge new default options to the Class
92 | L.Class.mergeOptions = function (options) {
93 | L.extend(this.prototype.options, options);
94 | };
95 |
96 | // add a constructor hook
97 | L.Class.addInitHook = function (fn) { // (Function) || (String, args...)
98 | var args = Array.prototype.slice.call(arguments, 1);
99 |
100 | var init = typeof fn === 'function' ? fn : function () {
101 | this[fn].apply(this, args);
102 | };
103 |
104 | this.prototype._initHooks = this.prototype._initHooks || [];
105 | this.prototype._initHooks.push(init);
106 | };
107 |
--------------------------------------------------------------------------------
/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) { // (Number, Number, Number)
6 | lat = parseFloat(lat);
7 | lng = parseFloat(lng);
8 |
9 | if (isNaN(lat) || isNaN(lng)) {
10 | throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
11 | }
12 |
13 | this.lat = lat;
14 | this.lng = lng;
15 |
16 | if (alt !== undefined) {
17 | this.alt = parseFloat(alt);
18 | }
19 | };
20 |
21 | L.extend(L.LatLng, {
22 | DEG_TO_RAD: Math.PI / 180,
23 | RAD_TO_DEG: 180 / Math.PI,
24 | MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check
25 | });
26 |
27 | L.LatLng.prototype = {
28 | equals: function (obj) { // (LatLng) -> Boolean
29 | if (!obj) { return false; }
30 |
31 | obj = L.latLng(obj);
32 |
33 | var margin = Math.max(
34 | Math.abs(this.lat - obj.lat),
35 | Math.abs(this.lng - obj.lng));
36 |
37 | return margin <= L.LatLng.MAX_MARGIN;
38 | },
39 |
40 | toString: function (precision) { // (Number) -> String
41 | return 'LatLng(' +
42 | L.Util.formatNum(this.lat, precision) + ', ' +
43 | L.Util.formatNum(this.lng, precision) + ')';
44 | },
45 |
46 | // Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula
47 | // TODO move to projection code, LatLng shouldn't know about Earth
48 | distanceTo: function (other) { // (LatLng) -> Number
49 | other = L.latLng(other);
50 |
51 | var R = 6378137, // earth radius in meters
52 | d2r = L.LatLng.DEG_TO_RAD,
53 | dLat = (other.lat - this.lat) * d2r,
54 | dLon = (other.lng - this.lng) * d2r,
55 | lat1 = this.lat * d2r,
56 | lat2 = other.lat * d2r,
57 | sin1 = Math.sin(dLat / 2),
58 | sin2 = Math.sin(dLon / 2);
59 |
60 | var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2);
61 |
62 | return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
63 | },
64 |
65 | wrap: function (a, b) { // (Number, Number) -> LatLng
66 | var lng = this.lng;
67 |
68 | a = a || -180;
69 | b = b || 180;
70 |
71 | lng = (lng + b) % (b - a) + (lng < a || lng === b ? b : a);
72 |
73 | return new L.LatLng(this.lat, lng);
74 | }
75 | };
76 |
77 | L.latLng = function (a, b) { // (LatLng) or ([Number, Number]) or (Number, Number)
78 | if (a instanceof L.LatLng) {
79 | return a;
80 | }
81 | if (L.Util.isArray(a)) {
82 | if (typeof a[0] === 'number' || typeof a[0] === 'string') {
83 | return new L.LatLng(a[0], a[1], a[2]);
84 | } else {
85 | return null;
86 | }
87 | }
88 | if (a === undefined || a === null) {
89 | return a;
90 | }
91 | if (typeof a === 'object' && 'lat' in a) {
92 | return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon);
93 | }
94 | if (b === undefined) {
95 | return null;
96 | }
97 | return new L.LatLng(a, b);
98 | };
99 |
100 |
--------------------------------------------------------------------------------
/debug/vector/bounds-extend.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/spec/suites/dom/DomEventSpec.js:
--------------------------------------------------------------------------------
1 | describe('DomEvent', function () {
2 | var el;
3 |
4 | function simulateClick(el) {
5 | if (document.createEvent) {
6 | var e = document.createEvent('MouseEvents');
7 | e.initMouseEvent('click', true, true, window,
8 | 0, 0, 0, 0, 0, false, false, false, false, 0, null);
9 | return el.dispatchEvent(e);
10 | } else if (el.fireEvent) {
11 | return el.fireEvent('onclick');
12 | }
13 | }
14 |
15 | beforeEach(function () {
16 | el = document.createElement('div');
17 | el.style.position = 'absolute';
18 | el.style.top = el.style.left = '-10000px';
19 | document.body.appendChild(el);
20 | });
21 |
22 | afterEach(function () {
23 | document.body.removeChild(el);
24 | });
25 |
26 | describe('#addListener', function () {
27 | it('adds a listener and calls it on event', function () {
28 | var listener1 = sinon.spy(),
29 | listener2 = sinon.spy();
30 |
31 | L.DomEvent.addListener(el, 'click', listener1);
32 | L.DomEvent.addListener(el, 'click', listener2);
33 |
34 | simulateClick(el);
35 |
36 | expect(listener1.called).to.be.ok();
37 | expect(listener2.called).to.be.ok();
38 | });
39 |
40 | it('binds "this" to the given context', function () {
41 | var obj = {foo: 'bar'},
42 | result;
43 |
44 | L.DomEvent.addListener(el, 'click', function () {
45 | result = this;
46 | }, obj);
47 |
48 | simulateClick(el);
49 |
50 | expect(result).to.eql(obj);
51 | });
52 |
53 | it('passes an event object to the listener', function () {
54 | var type;
55 |
56 | L.DomEvent.addListener(el, 'click', function (e) {
57 | type = e && e.type;
58 | });
59 | simulateClick(el);
60 |
61 | expect(type).to.eql('click');
62 | });
63 | });
64 |
65 | describe('#removeListener', function () {
66 | it('removes a previously added listener', function () {
67 | var listener = sinon.spy();
68 |
69 | L.DomEvent.addListener(el, 'click', listener);
70 | L.DomEvent.removeListener(el, 'click', listener);
71 |
72 | simulateClick(el);
73 |
74 | expect(listener.called).to.not.be.ok();
75 | });
76 | });
77 |
78 | describe('#stopPropagation', function () {
79 | it('stops propagation of the given event', function () {
80 | var child = document.createElement('div'),
81 | listener = sinon.spy();
82 |
83 | el.appendChild(child);
84 |
85 | L.DomEvent.addListener(child, 'click', L.DomEvent.stopPropagation);
86 | L.DomEvent.addListener(el, 'click', listener);
87 |
88 | simulateClick(child);
89 |
90 | expect(listener.called).to.not.be.ok();
91 |
92 | el.removeChild(child);
93 | });
94 | });
95 | describe('#preventDefault', function () {
96 | it('prevents the default action of event', function () {
97 | L.DomEvent.addListener(el, 'click', L.DomEvent.preventDefault);
98 |
99 | expect(simulateClick(el)).to.be(false);
100 | });
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/src/dom/PosAnimation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.PosAnimation is used by Leaflet internally for pan animations.
3 | */
4 |
5 | L.PosAnimation = L.Class.extend({
6 | includes: L.Mixin.Events,
7 |
8 | run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number])
9 | this.stop();
10 |
11 | this._el = el;
12 | this._inProgress = true;
13 | this._newPos = newPos;
14 |
15 | this.fire('start');
16 |
17 | el.style[L.DomUtil.TRANSITION] = 'all ' + (duration || 0.25) +
18 | 's cubic-bezier(0,0,' + (easeLinearity || 0.5) + ',1)';
19 |
20 | L.DomEvent.on(el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
21 | L.DomUtil.setPosition(el, newPos);
22 |
23 | // toggle reflow, Chrome flickers for some reason if you don't do this
24 | L.Util.falseFn(el.offsetWidth);
25 |
26 | // there's no native way to track value updates of transitioned properties, so we imitate this
27 | this._stepTimer = setInterval(L.bind(this._onStep, this), 50);
28 | },
29 |
30 | stop: function () {
31 | if (!this._inProgress) { return; }
32 |
33 | // if we just removed the transition property, the element would jump to its final position,
34 | // so we need to make it stay at the current position
35 |
36 | L.DomUtil.setPosition(this._el, this._getPos());
37 | this._onTransitionEnd();
38 | L.Util.falseFn(this._el.offsetWidth); // force reflow in case we are about to start a new animation
39 | },
40 |
41 | _onStep: function () {
42 | var stepPos = this._getPos();
43 | if (!stepPos) {
44 | this._onTransitionEnd();
45 | return;
46 | }
47 | // jshint camelcase: false
48 | // make L.DomUtil.getPosition return intermediate position value during animation
49 | this._el._leaflet_pos = stepPos;
50 |
51 | this.fire('step');
52 | },
53 |
54 | // you can't easily get intermediate values of properties animated with CSS3 Transitions,
55 | // we need to parse computed style (in case of transform it returns matrix string)
56 |
57 | _transformRe: /([-+]?(?:\d*\.)?\d+)\D*, ([-+]?(?:\d*\.)?\d+)\D*\)/,
58 |
59 | _getPos: function () {
60 | var left, top, matches,
61 | el = this._el,
62 | style = window.getComputedStyle(el);
63 |
64 | if (L.Browser.any3d) {
65 | matches = style[L.DomUtil.TRANSFORM].match(this._transformRe);
66 | if (!matches) { return; }
67 | left = parseFloat(matches[1]);
68 | top = parseFloat(matches[2]);
69 | } else {
70 | left = parseFloat(style.left);
71 | top = parseFloat(style.top);
72 | }
73 |
74 | return new L.Point(left, top, true);
75 | },
76 |
77 | _onTransitionEnd: function () {
78 | L.DomEvent.off(this._el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
79 |
80 | if (!this._inProgress) { return; }
81 | this._inProgress = false;
82 |
83 | this._el.style[L.DomUtil.TRANSITION] = '';
84 |
85 | // jshint camelcase: false
86 | // make sure L.DomUtil.getPosition returns the final position value after animation
87 | this._el._leaflet_pos = this._newPos;
88 |
89 | clearInterval(this._stepTimer);
90 |
91 | this.fire('step').fire('end');
92 | }
93 |
94 | });
95 |
--------------------------------------------------------------------------------
/spec/suites/layer/TileLayerSpec.js:
--------------------------------------------------------------------------------
1 |
2 | describe('TileLayer', function () {
3 | var tileUrl = '';
4 |
5 | describe("#getMaxZoom, #getMinZoom", function () {
6 | var map;
7 | beforeEach(function () {
8 | map = L.map(document.createElement('div'));
9 | });
10 | describe("when a tilelayer is added to a map with no other layers", function () {
11 | it("has the same zoomlevels as the tilelayer", function () {
12 | var maxZoom = 10,
13 | minZoom = 5;
14 |
15 | map.setView([0, 0], 1);
16 |
17 | L.tileLayer(tileUrl, {
18 | maxZoom: maxZoom,
19 | minZoom: minZoom
20 | }).addTo(map);
21 |
22 | expect(map.getMaxZoom()).to.be(maxZoom);
23 | expect(map.getMinZoom()).to.be(minZoom);
24 | });
25 | });
26 |
27 | describe("accessing a tilelayer's properties", function () {
28 | it('provides a container', function () {
29 | map.setView([0, 0], 1);
30 |
31 | var layer = L.tileLayer(tileUrl).addTo(map);
32 | expect(layer.getContainer()).to.be.ok();
33 | });
34 | });
35 |
36 | describe("when a tilelayer is added to a map that already has a tilelayer", function () {
37 | it("has its zoomlevels updated to fit the new layer", function () {
38 | map.setView([0, 0], 1);
39 |
40 | L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 15}).addTo(map);
41 | expect(map.getMinZoom()).to.be(10);
42 | expect(map.getMaxZoom()).to.be(15);
43 |
44 | L.tileLayer(tileUrl, {minZoom: 5, maxZoom: 10}).addTo(map);
45 | expect(map.getMinZoom()).to.be(5); // changed
46 | expect(map.getMaxZoom()).to.be(15); // unchanged
47 |
48 | L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 20}).addTo(map);
49 | expect(map.getMinZoom()).to.be(5); // unchanged
50 | expect(map.getMaxZoom()).to.be(20); // changed
51 |
52 |
53 | L.tileLayer(tileUrl, {minZoom: 0, maxZoom: 25}).addTo(map);
54 | expect(map.getMinZoom()).to.be(0); // changed
55 | expect(map.getMaxZoom()).to.be(25); // changed
56 | });
57 | });
58 | describe("when a tilelayer is removed from a map", function () {
59 | it("has its zoomlevels updated to only fit the layers it currently has", function () {
60 | var tiles = [ L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 15}).addTo(map),
61 | L.tileLayer(tileUrl, {minZoom: 5, maxZoom: 10}).addTo(map),
62 | L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 20}).addTo(map),
63 | L.tileLayer(tileUrl, {minZoom: 0, maxZoom: 25}).addTo(map)
64 | ];
65 | map.whenReady(function () {
66 | expect(map.getMinZoom()).to.be(0);
67 | expect(map.getMaxZoom()).to.be(25);
68 |
69 | map.removeLayer(tiles[0]);
70 | expect(map.getMinZoom()).to.be(0);
71 | expect(map.getMaxZoom()).to.be(25);
72 |
73 | map.removeLayer(tiles[3]);
74 | expect(map.getMinZoom()).to.be(5);
75 | expect(map.getMaxZoom()).to.be(20);
76 |
77 | map.removeLayer(tiles[2]);
78 | expect(map.getMinZoom()).to.be(5);
79 | expect(map.getMaxZoom()).to.be(10);
80 |
81 | map.removeLayer(tiles[1]);
82 | expect(map.getMinZoom()).to.be(0);
83 | expect(map.getMaxZoom()).to.be(Infinity);
84 | });
85 | });
86 | });
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/src/layer/tile/TileLayer.Anim.js:
--------------------------------------------------------------------------------
1 | /*
2 | Zoom animation logic for L.TileLayer.
3 | */
4 |
5 | L.TileLayer.include({
6 | _animateZoom: function (e) {
7 | if (!this._animating) {
8 | this._animating = true;
9 | this._prepareBgBuffer();
10 | }
11 |
12 | var bg = this._bgBuffer,
13 | transform = L.DomUtil.TRANSFORM,
14 | initialTransform = e.delta ? L.DomUtil.getTranslateString(e.delta) : bg.style[transform],
15 | scaleStr = L.DomUtil.getScaleString(e.scale, e.origin);
16 |
17 | bg.style[transform] = e.backwards ?
18 | scaleStr + ' ' + initialTransform :
19 | initialTransform + ' ' + scaleStr;
20 | },
21 |
22 | _endZoomAnim: function () {
23 | var front = this._tileContainer,
24 | bg = this._bgBuffer;
25 |
26 | front.style.visibility = '';
27 | front.parentNode.appendChild(front); // Bring to fore
28 |
29 | // force reflow
30 | L.Util.falseFn(bg.offsetWidth);
31 |
32 | this._animating = false;
33 | },
34 |
35 | _clearBgBuffer: function () {
36 | var map = this._map;
37 |
38 | if (map && !map._animatingZoom && !map.touchZoom._zooming) {
39 | this._bgBuffer.innerHTML = '';
40 | this._bgBuffer.style[L.DomUtil.TRANSFORM] = '';
41 | }
42 | },
43 |
44 | _prepareBgBuffer: function () {
45 |
46 | var front = this._tileContainer,
47 | bg = this._bgBuffer;
48 |
49 | // if foreground layer doesn't have many tiles but bg layer does,
50 | // keep the existing bg layer and just zoom it some more
51 |
52 | var bgLoaded = this._getLoadedTilesPercentage(bg),
53 | frontLoaded = this._getLoadedTilesPercentage(front);
54 |
55 | if (bg && bgLoaded > 0.5 && frontLoaded < 0.5) {
56 |
57 | front.style.visibility = 'hidden';
58 | this._stopLoadingImages(front);
59 | return;
60 | }
61 |
62 | // prepare the buffer to become the front tile pane
63 | bg.style.visibility = 'hidden';
64 | bg.style[L.DomUtil.TRANSFORM] = '';
65 |
66 | // switch out the current layer to be the new bg layer (and vice-versa)
67 | this._tileContainer = bg;
68 | bg = this._bgBuffer = front;
69 |
70 | this._stopLoadingImages(bg);
71 |
72 | //prevent bg buffer from clearing right after zoom
73 | clearTimeout(this._clearBgBufferTimer);
74 | },
75 |
76 | _getLoadedTilesPercentage: function (container) {
77 | var tiles = container.getElementsByTagName('img'),
78 | i, len, count = 0;
79 |
80 | for (i = 0, len = tiles.length; i < len; i++) {
81 | if (tiles[i].complete) {
82 | count++;
83 | }
84 | }
85 | return count / len;
86 | },
87 |
88 | // stops loading all tiles in the background layer
89 | _stopLoadingImages: function (container) {
90 | var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
91 | i, len, tile;
92 |
93 | for (i = 0, len = tiles.length; i < len; i++) {
94 | tile = tiles[i];
95 |
96 | if (!tile.complete) {
97 | tile.onload = L.Util.falseFn;
98 | tile.onerror = L.Util.falseFn;
99 | tile.src = L.Util.emptyImageUrl;
100 |
101 | tile.parentNode.removeChild(tile);
102 | }
103 | }
104 | }
105 | });
106 |
--------------------------------------------------------------------------------
/src/map/handler/Map.Tap.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Map.Tap is used to enable mobile hacks like quick taps and long hold.
3 | */
4 |
5 | L.Map.mergeOptions({
6 | tap: true,
7 | tapTolerance: 15
8 | });
9 |
10 | L.Map.Tap = L.Handler.extend({
11 | addHooks: function () {
12 | L.DomEvent.on(this._map._container, 'touchstart', this._onDown, this);
13 | },
14 |
15 | removeHooks: function () {
16 | L.DomEvent.off(this._map._container, 'touchstart', this._onDown, this);
17 | },
18 |
19 | _onDown: function (e) {
20 | if (!e.touches) { return; }
21 |
22 | L.DomEvent.preventDefault(e);
23 |
24 | this._fireClick = true;
25 |
26 | // don't simulate click or track longpress if more than 1 touch
27 | if (e.touches.length > 1) {
28 | this._fireClick = false;
29 | clearTimeout(this._holdTimeout);
30 | return;
31 | }
32 |
33 | var first = e.touches[0],
34 | el = first.target;
35 |
36 | this._startPos = this._newPos = new L.Point(first.clientX, first.clientY);
37 |
38 | // if touching a link, highlight it
39 | if (el.tagName && el.tagName.toLowerCase() === 'a') {
40 | L.DomUtil.addClass(el, 'leaflet-active');
41 | }
42 |
43 | // simulate long hold but setting a timeout
44 | this._holdTimeout = setTimeout(L.bind(function () {
45 | if (this._isTapValid()) {
46 | this._fireClick = false;
47 | this._onUp();
48 | this._simulateEvent('contextmenu', first);
49 | }
50 | }, this), 1000);
51 |
52 | L.DomEvent
53 | .on(document, 'touchmove', this._onMove, this)
54 | .on(document, 'touchend', this._onUp, this);
55 | },
56 |
57 | _onUp: function (e) {
58 | clearTimeout(this._holdTimeout);
59 |
60 | L.DomEvent
61 | .off(document, 'touchmove', this._onMove, this)
62 | .off(document, 'touchend', this._onUp, this);
63 |
64 | if (this._fireClick && e && e.changedTouches) {
65 |
66 | var first = e.changedTouches[0],
67 | el = first.target;
68 |
69 | if (el && el.tagName && el.tagName.toLowerCase() === 'a') {
70 | L.DomUtil.removeClass(el, 'leaflet-active');
71 | }
72 |
73 | // simulate click if the touch didn't move too much
74 | if (this._isTapValid()) {
75 | this._simulateEvent('click', first);
76 | }
77 | }
78 | },
79 |
80 | _isTapValid: function () {
81 | return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
82 | },
83 |
84 | _onMove: function (e) {
85 | var first = e.touches[0];
86 | this._newPos = new L.Point(first.clientX, first.clientY);
87 | },
88 |
89 | _simulateEvent: function (type, e) {
90 | var simulatedEvent = document.createEvent('MouseEvents');
91 |
92 | simulatedEvent._simulated = true;
93 | e.target._simulatedClick = true;
94 |
95 | simulatedEvent.initMouseEvent(
96 | type, true, true, window, 1,
97 | e.screenX, e.screenY,
98 | e.clientX, e.clientY,
99 | false, false, false, false, 0, null);
100 |
101 | e.target.dispatchEvent(simulatedEvent);
102 | }
103 | });
104 |
105 | if (L.Browser.touch && !L.Browser.pointer) {
106 | L.Map.addInitHook('addHandler', 'tap', L.Map.Tap);
107 | }
108 |
--------------------------------------------------------------------------------
/spec/suites/layer/marker/MarkerSpec.js:
--------------------------------------------------------------------------------
1 | describe("Marker", function () {
2 | var map,
3 | spy,
4 | icon1,
5 | icon2;
6 |
7 | beforeEach(function () {
8 | map = L.map(document.createElement('div')).setView([0, 0], 0);
9 | icon1 = new L.Icon.Default();
10 | icon2 = new L.Icon.Default({
11 | iconUrl: icon1._getIconUrl('icon') + '?2',
12 | shadowUrl: icon1._getIconUrl('shadow') + '?2'
13 | });
14 | });
15 |
16 | describe("#setIcon", function () {
17 | it("changes the icon to another image", function () {
18 | var marker = new L.Marker([0, 0], {icon: icon1});
19 | map.addLayer(marker);
20 |
21 | var beforeIcon = marker._icon;
22 | marker.setIcon(icon2);
23 | var afterIcon = marker._icon;
24 |
25 | expect(beforeIcon).to.be(afterIcon);
26 | expect(afterIcon.src).to.contain(icon2._getIconUrl('icon'));
27 | });
28 |
29 | it("changes the icon to another DivIcon", function () {
30 | var marker = new L.Marker([0, 0], {icon: new L.DivIcon({html: 'Inner1Text' }) });
31 | map.addLayer(marker);
32 |
33 | var beforeIcon = marker._icon;
34 | marker.setIcon(new L.DivIcon({html: 'Inner2Text' }));
35 | var afterIcon = marker._icon;
36 |
37 | expect(beforeIcon).to.be(afterIcon);
38 | expect(afterIcon.innerHTML).to.contain('Inner2Text');
39 | });
40 |
41 | it("removes text when changing to a blank DivIcon", function () {
42 | var marker = new L.Marker([0, 0], {icon: new L.DivIcon({html: 'Inner1Text' }) });
43 | map.addLayer(marker);
44 |
45 | marker.setIcon(new L.DivIcon());
46 | var afterIcon = marker._icon;
47 |
48 | expect(marker._icon.innerHTML).to.not.contain('Inner1Text');
49 | });
50 |
51 | it("changes a DivIcon to an image", function () {
52 | var marker = new L.Marker([0, 0], {icon: new L.DivIcon({html: 'Inner1Text' }) });
53 | map.addLayer(marker);
54 | var oldIcon = marker._icon;
55 |
56 | marker.setIcon(icon1);
57 |
58 | expect(oldIcon).to.not.be(marker._icon);
59 | expect(oldIcon.parentNode).to.be(null);
60 |
61 | expect(marker._icon.src).to.contain('marker-icon.png');
62 | expect(marker._icon.parentNode).to.be(map._panes.markerPane);
63 | });
64 |
65 | it("changes an image to a DivIcon", function () {
66 | var marker = new L.Marker([0, 0], {icon: icon1});
67 | map.addLayer(marker);
68 | var oldIcon = marker._icon;
69 |
70 | marker.setIcon(new L.DivIcon({html: 'Inner1Text' }));
71 |
72 | expect(oldIcon).to.not.be(marker._icon);
73 | expect(oldIcon.parentNode).to.be(null);
74 |
75 | expect(marker._icon.innerHTML).to.contain('Inner1Text');
76 | expect(marker._icon.parentNode).to.be(map._panes.markerPane);
77 | });
78 |
79 | it("reuses the icon/shadow when changing icon", function () {
80 | var marker = new L.Marker([0, 0], { icon: icon1});
81 | map.addLayer(marker);
82 | var oldIcon = marker._icon;
83 | var oldShadow = marker._shadow;
84 |
85 | marker.setIcon(icon2);
86 |
87 | expect(oldIcon).to.be(marker._icon);
88 | expect(oldShadow).to.be(marker._shadow);
89 |
90 | expect(marker._icon.parentNode).to.be(map._panes.markerPane);
91 | expect(marker._shadow.parentNode).to.be(map._panes.shadowPane);
92 | });
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/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 | L.DomEvent.disableClickPropagation(this._container);
20 |
21 | for (var i in map._layers) {
22 | if (map._layers[i].getAttribution) {
23 | this.addAttribution(map._layers[i].getAttribution());
24 | }
25 | }
26 |
27 | map
28 | .on('layeradd', this._onLayerAdd, this)
29 | .on('layerremove', this._onLayerRemove, this);
30 |
31 | this._update();
32 |
33 | return this._container;
34 | },
35 |
36 | onRemove: function (map) {
37 | map
38 | .off('layeradd', this._onLayerAdd)
39 | .off('layerremove', this._onLayerRemove);
40 |
41 | },
42 |
43 | setPrefix: function (prefix) {
44 | this.options.prefix = prefix;
45 | this._update();
46 | return this;
47 | },
48 |
49 | addAttribution: function (text) {
50 | if (!text) { return; }
51 |
52 | if (!this._attributions[text]) {
53 | this._attributions[text] = 0;
54 | }
55 | this._attributions[text]++;
56 |
57 | this._update();
58 |
59 | return this;
60 | },
61 |
62 | removeAttribution: function (text) {
63 | if (!text) { return; }
64 |
65 | if (this._attributions[text]) {
66 | this._attributions[text]--;
67 | this._update();
68 | }
69 |
70 | return this;
71 | },
72 |
73 | _update: function () {
74 | if (!this._map) { return; }
75 |
76 | var attribs = [];
77 |
78 | for (var i in this._attributions) {
79 | if (this._attributions[i]) {
80 | attribs.push(i);
81 | }
82 | }
83 |
84 | var prefixAndAttribs = [];
85 |
86 | if (this.options.prefix) {
87 | prefixAndAttribs.push(this.options.prefix);
88 | }
89 | if (attribs.length) {
90 | prefixAndAttribs.push(attribs.join(', '));
91 | }
92 |
93 | this._container.innerHTML = prefixAndAttribs.join(' | ');
94 | },
95 |
96 | _onLayerAdd: function (e) {
97 | if (e.layer.getAttribution) {
98 | this.addAttribution(e.layer.getAttribution());
99 | }
100 | },
101 |
102 | _onLayerRemove: function (e) {
103 | if (e.layer.getAttribution) {
104 | this.removeAttribution(e.layer.getAttribution());
105 | }
106 | }
107 | });
108 |
109 | L.Map.mergeOptions({
110 | attributionControl: true
111 | });
112 |
113 | L.Map.addInitHook(function () {
114 | if (this.options.attributionControl) {
115 | this.attributionControl = (new L.Control.Attribution()).addTo(this);
116 | }
117 | });
118 |
119 | L.control.attribution = function (options) {
120 | return new L.Control.Attribution(options);
121 | };
122 |
--------------------------------------------------------------------------------
/src/layer/vector/Path.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Path is a base class for rendering vector paths on a map. Inherited by Polyline, Circle, etc.
3 | */
4 |
5 | L.Path = L.Class.extend({
6 | includes: [L.Mixin.Events],
7 |
8 | statics: {
9 | // how much to extend the clip area around the map view
10 | // (relative to its size, e.g. 0.5 is half the screen in each direction)
11 | // set it so that SVG element doesn't exceed 1280px (vectors flicker on dragend if it is)
12 | CLIP_PADDING: (function () {
13 | var max = L.Browser.mobile ? 1280 : 2000,
14 | target = (max / Math.max(window.outerWidth, window.outerHeight) - 1) / 2;
15 | return Math.max(0, Math.min(0.5, target));
16 | })()
17 | },
18 |
19 | options: {
20 | stroke: true,
21 | color: '#0033ff',
22 | dashArray: null,
23 | lineCap: null,
24 | lineJoin: null,
25 | weight: 5,
26 | opacity: 0.5,
27 |
28 | fill: false,
29 | fillColor: null, //same as color by default
30 | fillOpacity: 0.2,
31 |
32 | clickable: true
33 | },
34 |
35 | initialize: function (options) {
36 | L.setOptions(this, options);
37 | },
38 |
39 | onAdd: function (map) {
40 | this._map = map;
41 |
42 | if (!this._container) {
43 | this._initElements();
44 | this._initEvents();
45 | }
46 |
47 | this.projectLatlngs();
48 | this._updatePath();
49 |
50 | if (this._container) {
51 | this._map._pathRoot.appendChild(this._container);
52 | }
53 |
54 | this.fire('add');
55 |
56 | map.on({
57 | 'viewreset': this.projectLatlngs,
58 | 'moveend': this._updatePath
59 | }, this);
60 | },
61 |
62 | addTo: function (map) {
63 | map.addLayer(this);
64 | return this;
65 | },
66 |
67 | onRemove: function (map) {
68 | map._pathRoot.removeChild(this._container);
69 |
70 | // Need to fire remove event before we set _map to null as the event hooks might need the object
71 | this.fire('remove');
72 | this._map = null;
73 |
74 | if (L.Browser.vml) {
75 | this._container = null;
76 | this._stroke = null;
77 | this._fill = null;
78 | }
79 |
80 | map.off({
81 | 'viewreset': this.projectLatlngs,
82 | 'moveend': this._updatePath
83 | }, this);
84 | },
85 |
86 | projectLatlngs: function () {
87 | // do all projection stuff here
88 | },
89 |
90 | setStyle: function (style) {
91 | L.setOptions(this, style);
92 |
93 | if (this._container) {
94 | this._updateStyle();
95 | }
96 |
97 | return this;
98 | },
99 |
100 | redraw: function () {
101 | if (this._map) {
102 | this.projectLatlngs();
103 | this._updatePath();
104 | }
105 | return this;
106 | }
107 | });
108 |
109 | L.Map.include({
110 | _updatePathViewport: function () {
111 | var p = L.Path.CLIP_PADDING,
112 | size = this.getSize(),
113 | panePos = L.DomUtil.getPosition(this._mapPane),
114 | min = panePos.multiplyBy(-1)._subtract(size.multiplyBy(p)._round()),
115 | max = min.add(size.multiplyBy(1 + p * 2)._round());
116 |
117 | this._pathViewport = new L.Bounds(min, max);
118 | }
119 | });
120 |
--------------------------------------------------------------------------------
/debug/vector/feature-group-bounds.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Leaflet debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/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 ie = 'ActiveXObject' in window,
8 | ielt9 = ie && !document.addEventListener,
9 |
10 | // terrible browser detection to work around Safari / iOS / Android browser bugs
11 | ua = navigator.userAgent.toLowerCase(),
12 | webkit = ua.indexOf('webkit') !== -1,
13 | chrome = ua.indexOf('chrome') !== -1,
14 | phantomjs = ua.indexOf('phantom') !== -1,
15 | android = ua.indexOf('android') !== -1,
16 | android23 = ua.search('android [23]') !== -1,
17 | gecko = ua.indexOf('gecko') !== -1,
18 |
19 | mobile = typeof orientation !== undefined + '',
20 | msPointer = window.navigator && window.navigator.msPointerEnabled &&
21 | window.navigator.msMaxTouchPoints && !window.PointerEvent,
22 | pointer = (window.PointerEvent && window.navigator.pointerEnabled && window.navigator.maxTouchPoints) ||
23 | msPointer,
24 | retina = ('devicePixelRatio' in window && window.devicePixelRatio > 1) ||
25 | ('matchMedia' in window && window.matchMedia('(min-resolution:144dpi)') &&
26 | window.matchMedia('(min-resolution:144dpi)').matches),
27 |
28 | doc = document.documentElement,
29 | ie3d = ie && ('transition' in doc.style),
30 | webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
31 | gecko3d = 'MozPerspective' in doc.style,
32 | opera3d = 'OTransition' in doc.style,
33 | any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs;
34 |
35 |
36 | // PhantomJS has 'ontouchstart' in document.documentElement, but doesn't actually support touch.
37 | // https://github.com/Leaflet/Leaflet/pull/1434#issuecomment-13843151
38 |
39 | var touch = !window.L_NO_TOUCH && !phantomjs && (function () {
40 |
41 | var startName = 'ontouchstart';
42 |
43 | // IE10+ (We simulate these into touch* events in L.DomEvent and L.DomEvent.Pointer) or WebKit, etc.
44 | if (pointer || (startName in doc)) {
45 | return true;
46 | }
47 |
48 | // Firefox/Gecko
49 | var div = document.createElement('div'),
50 | supported = false;
51 |
52 | if (!div.setAttribute) {
53 | return false;
54 | }
55 | div.setAttribute(startName, 'return;');
56 |
57 | if (typeof div[startName] === 'function') {
58 | supported = true;
59 | }
60 |
61 | div.removeAttribute(startName);
62 | div = null;
63 |
64 | return supported;
65 | }());
66 |
67 |
68 | L.Browser = {
69 | ie: ie,
70 | ielt9: ielt9,
71 | webkit: webkit,
72 | gecko: gecko && !webkit && !window.opera && !ie,
73 |
74 | android: android,
75 | android23: android23,
76 |
77 | chrome: chrome,
78 |
79 | ie3d: ie3d,
80 | webkit3d: webkit3d,
81 | gecko3d: gecko3d,
82 | opera3d: opera3d,
83 | any3d: any3d,
84 |
85 | mobile: mobile,
86 | mobileWebkit: mobile && webkit,
87 | mobileWebkit3d: mobile && webkit3d,
88 | mobileOpera: mobile && window.opera,
89 |
90 | touch: touch,
91 | msPointer: msPointer,
92 | pointer: pointer,
93 |
94 | retina: retina
95 | };
96 |
97 | }());
98 |
--------------------------------------------------------------------------------
/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,
13 | doubleTap = false,
14 | delay = 250,
15 | touch,
16 | pre = '_leaflet_',
17 | touchstart = this._touchstart,
18 | touchend = this._touchend,
19 | trackedTouches = [];
20 |
21 | function onTouchStart(e) {
22 | var count;
23 |
24 | if (L.Browser.pointer) {
25 | trackedTouches.push(e.pointerId);
26 | count = trackedTouches.length;
27 | } else {
28 | count = e.touches.length;
29 | }
30 | if (count > 1) {
31 | return;
32 | }
33 |
34 | var now = Date.now(),
35 | delta = now - (last || now);
36 |
37 | touch = e.touches ? e.touches[0] : e;
38 | doubleTap = (delta > 0 && delta <= delay);
39 | last = now;
40 | }
41 |
42 | function onTouchEnd(e) {
43 | if (L.Browser.pointer) {
44 | var idx = trackedTouches.indexOf(e.pointerId);
45 | if (idx === -1) {
46 | return;
47 | }
48 | trackedTouches.splice(idx, 1);
49 | }
50 |
51 | if (doubleTap) {
52 | if (L.Browser.pointer) {
53 | // work around .type being readonly with MSPointer* events
54 | var newTouch = { },
55 | prop;
56 |
57 | // jshint forin:false
58 | for (var i in touch) {
59 | prop = touch[i];
60 | if (typeof prop === 'function') {
61 | newTouch[i] = prop.bind(touch);
62 | } else {
63 | newTouch[i] = prop;
64 | }
65 | }
66 | touch = newTouch;
67 | }
68 | touch.type = 'dblclick';
69 | handler(touch);
70 | last = null;
71 | }
72 | }
73 | obj[pre + touchstart + id] = onTouchStart;
74 | obj[pre + touchend + id] = onTouchEnd;
75 |
76 | // on pointer we need to listen on the document, otherwise a drag starting on the map and moving off screen
77 | // will not come through to us, so we will lose track of how many touches are ongoing
78 | var endElement = L.Browser.pointer ? document.documentElement : obj;
79 |
80 | obj.addEventListener(touchstart, onTouchStart, false);
81 | endElement.addEventListener(touchend, onTouchEnd, false);
82 |
83 | if (L.Browser.pointer) {
84 | endElement.addEventListener(L.DomEvent.POINTER_CANCEL, onTouchEnd, false);
85 | }
86 |
87 | return this;
88 | },
89 |
90 | removeDoubleTapListener: function (obj, id) {
91 | var pre = '_leaflet_';
92 |
93 | obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false);
94 | (L.Browser.pointer ? document.documentElement : obj).removeEventListener(
95 | this._touchend, obj[pre + this._touchend + id], false);
96 |
97 | if (L.Browser.pointer) {
98 | document.documentElement.removeEventListener(L.DomEvent.POINTER_CANCEL, obj[pre + this._touchend + id],
99 | false);
100 | }
101 |
102 | return this;
103 | }
104 | });
105 |
--------------------------------------------------------------------------------
/src/control/Control.Scale.js:
--------------------------------------------------------------------------------
1 | /*
2 | * L.Control.Scale is used for displaying metric/imperial scale on the map.
3 | */
4 |
5 | L.Control.Scale = L.Control.extend({
6 | options: {
7 | position: 'bottomleft',
8 | maxWidth: 100,
9 | metric: true,
10 | imperial: true,
11 | updateWhenIdle: false
12 | },
13 |
14 | onAdd: function (map) {
15 | this._map = map;
16 |
17 | var className = 'leaflet-control-scale',
18 | container = L.DomUtil.create('div', className),
19 | options = this.options;
20 |
21 | this._addScales(options, className, container);
22 |
23 | map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
24 | map.whenReady(this._update, this);
25 |
26 | return container;
27 | },
28 |
29 | onRemove: function (map) {
30 | map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
31 | },
32 |
33 | _addScales: function (options, className, container) {
34 | if (options.metric) {
35 | this._mScale = L.DomUtil.create('div', className + '-line', container);
36 | }
37 | if (options.imperial) {
38 | this._iScale = L.DomUtil.create('div', className + '-line', container);
39 | }
40 | },
41 |
42 | _update: function () {
43 | var bounds = this._map.getBounds(),
44 | centerLat = bounds.getCenter().lat,
45 | halfWorldMeters = 6378137 * Math.PI * Math.cos(centerLat * Math.PI / 180),
46 | dist = halfWorldMeters * (bounds.getNorthEast().lng - bounds.getSouthWest().lng) / 180,
47 |
48 | size = this._map.getSize(),
49 | options = this.options,
50 | maxMeters = 0;
51 |
52 | if (size.x > 0) {
53 | maxMeters = dist * (options.maxWidth / size.x);
54 | }
55 |
56 | this._updateScales(options, maxMeters);
57 | },
58 |
59 | _updateScales: function (options, maxMeters) {
60 | if (options.metric && maxMeters) {
61 | this._updateMetric(maxMeters);
62 | }
63 |
64 | if (options.imperial && maxMeters) {
65 | this._updateImperial(maxMeters);
66 | }
67 | },
68 |
69 | _updateMetric: function (maxMeters) {
70 | var meters = this._getRoundNum(maxMeters);
71 |
72 | this._mScale.style.width = this._getScaleWidth(meters / maxMeters) + 'px';
73 | this._mScale.innerHTML = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
74 | },
75 |
76 | _updateImperial: function (maxMeters) {
77 | var maxFeet = maxMeters * 3.2808399,
78 | scale = this._iScale,
79 | maxMiles, miles, feet;
80 |
81 | if (maxFeet > 5280) {
82 | maxMiles = maxFeet / 5280;
83 | miles = this._getRoundNum(maxMiles);
84 |
85 | scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px';
86 | scale.innerHTML = miles + ' mi';
87 |
88 | } else {
89 | feet = this._getRoundNum(maxFeet);
90 |
91 | scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px';
92 | scale.innerHTML = feet + ' ft';
93 | }
94 | },
95 |
96 | _getScaleWidth: function (ratio) {
97 | return Math.round(this.options.maxWidth * ratio) - 10;
98 | },
99 |
100 | _getRoundNum: function (num) {
101 | var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1),
102 | d = num / pow10;
103 |
104 | d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1;
105 |
106 | return pow10 * d;
107 | }
108 | });
109 |
110 | L.control.scale = function (options) {
111 | return new L.Control.Scale(options);
112 | };
113 |
--------------------------------------------------------------------------------
/src/map/anim/Map.ZoomAnimation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Extends L.Map to handle zoom animations.
3 | */
4 |
5 | L.Map.mergeOptions({
6 | zoomAnimation: true,
7 | zoomAnimationThreshold: 4
8 | });
9 |
10 | if (L.DomUtil.TRANSITION) {
11 |
12 | L.Map.addInitHook(function () {
13 | // don't animate on browsers without hardware-accelerated transitions or old Android/Opera
14 | this._zoomAnimated = this.options.zoomAnimation && L.DomUtil.TRANSITION &&
15 | L.Browser.any3d && !L.Browser.android23 && !L.Browser.mobileOpera;
16 |
17 | // zoom transitions run with the same duration for all layers, so if one of transitionend events
18 | // happens after starting zoom animation (propagating to the map pane), we know that it ended globally
19 | if (this._zoomAnimated) {
20 | L.DomEvent.on(this._mapPane, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
21 | }
22 | });
23 | }
24 |
25 | L.Map.include(!L.DomUtil.TRANSITION ? {} : {
26 |
27 | _catchTransitionEnd: function () {
28 | if (this._animatingZoom) {
29 | this._onZoomTransitionEnd();
30 | }
31 | },
32 |
33 | _nothingToAnimate: function () {
34 | return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
35 | },
36 |
37 | _tryAnimatedZoom: function (center, zoom, options) {
38 |
39 | if (this._animatingZoom) { return true; }
40 |
41 | options = options || {};
42 |
43 | // don't animate if disabled, not supported or zoom difference is too large
44 | if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
45 | Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }
46 |
47 | // offset is the pixel coords of the zoom origin relative to the current center
48 | var scale = this.getZoomScale(zoom),
49 | offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale),
50 | origin = this._getCenterLayerPoint()._add(offset);
51 |
52 | // don't animate if the zoom origin isn't within one screen from the current center, unless forced
53 | if (options.animate !== true && !this.getSize().contains(offset)) { return false; }
54 |
55 | this
56 | .fire('movestart')
57 | .fire('zoomstart');
58 |
59 | this._animateZoom(center, zoom, origin, scale, null, true);
60 |
61 | return true;
62 | },
63 |
64 | _animateZoom: function (center, zoom, origin, scale, delta, backwards) {
65 |
66 | this._animatingZoom = true;
67 |
68 | // put transform transition on all layers with leaflet-zoom-animated class
69 | L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
70 |
71 | // remember what center/zoom to set after animation
72 | this._animateToCenter = center;
73 | this._animateToZoom = zoom;
74 |
75 | // disable any dragging during animation
76 | if (L.Draggable) {
77 | L.Draggable._disabled = true;
78 | }
79 |
80 | this.fire('zoomanim', {
81 | center: center,
82 | zoom: zoom,
83 | origin: origin,
84 | scale: scale,
85 | delta: delta,
86 | backwards: backwards
87 | });
88 | },
89 |
90 | _onZoomTransitionEnd: function () {
91 |
92 | this._animatingZoom = false;
93 |
94 | L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
95 |
96 | this._resetView(this._animateToCenter, this._animateToZoom, true, true);
97 |
98 | if (L.Draggable) {
99 | L.Draggable._disabled = false;
100 | }
101 | }
102 | });
103 |
--------------------------------------------------------------------------------