├── dist └── images │ ├── layers.png │ ├── layers-2x.png │ ├── marker-icon.png │ ├── marker-icon-2x.png │ └── marker-shadow.png ├── spec ├── before.js ├── after.js ├── suites │ ├── control │ │ ├── Control.ScaleSpec.js │ │ ├── Control.LayersSpec.js │ │ └── Control.AttributionSpec.js │ ├── LeafletSpec.js │ ├── layer │ │ ├── vector │ │ │ ├── CircleSpec.js │ │ │ ├── PolylineGeometrySpec.js │ │ │ ├── PolylineSpec.js │ │ │ ├── CircleMarkerSpec.js │ │ │ └── PolygonSpec.js │ │ ├── LayerGroupSpec.js │ │ ├── FeatureGroupSpec.js │ │ ├── TileLayerSpec.js │ │ └── marker │ │ │ └── MarkerSpec.js │ ├── geometry │ │ ├── PolyUtilSpec.js │ │ ├── TransformationSpec.js │ │ ├── LineUtilSpec.js │ │ └── BoundsSpec.js │ ├── dom │ │ ├── PosAnimationSpec.js │ │ ├── DomUtilSpec.js │ │ └── DomEventSpec.js │ ├── SpecHelper.js │ ├── map │ │ └── handler │ │ │ └── Map.DragSpec.js │ └── geo │ │ ├── CRSSpec.js │ │ └── ProjectionSpec.js ├── spec.hintrc.js └── karma.conf.js ├── debug ├── css │ ├── screen.css │ └── mobile.css ├── map │ ├── iframe.html │ ├── wms-marble.html │ ├── simple-proj.html │ ├── geolocation.html │ ├── max-bounds.html │ ├── canvas.html │ ├── image-overlay.html │ ├── zoomlevels.html │ ├── map-mobile.html │ ├── scroll.html │ ├── wms.html │ ├── controls.html │ └── map.html ├── tests │ ├── rtl2.html │ ├── remove_while_dragging.html │ ├── rtl.html │ ├── reuse_popups.html │ ├── bringtoback.html │ ├── canvasloop.html │ ├── removetilewhilepan.html │ ├── svg_clicks.html │ ├── set_icon_reuse_dom.html │ ├── click_on_canvas_broken.html │ ├── popupcontextmenuclicks.html │ ├── opacity.html │ ├── dragging_and_copyworldjump.html │ ├── click_on_canvas.html │ └── add_remove_layers.html ├── vector │ ├── vector.html │ ├── vector-mobile.html │ ├── geojson-sample.js │ ├── vector-simple.html │ ├── rectangle.html │ ├── vector-bounds.html │ ├── vector-canvas.html │ ├── bounds-extend.html │ └── feature-group-bounds.html ├── hacks │ └── jitter.html └── leaflet-include.js ├── .npmignore ├── src ├── geo │ ├── projection │ │ ├── Projection.js │ │ ├── Projection.LonLat.js │ │ ├── Projection.SphericalMercator.js │ │ └── Projection.Mercator.js │ ├── crs │ │ ├── CRS.EPSG4326.js │ │ ├── CRS.Simple.js │ │ ├── CRS.EPSG3395.js │ │ ├── CRS.EPSG3857.js │ │ └── CRS.js │ └── LatLng.js ├── copyright.js ├── layer │ ├── vector │ │ ├── canvas │ │ │ ├── CircleMarker.Canvas.js │ │ │ ├── Circle.Canvas.js │ │ │ ├── Polyline.Canvas.js │ │ │ └── Polygon.Canvas.js │ │ ├── Rectangle.js │ │ ├── CircleMarker.js │ │ ├── MultiPoly.js │ │ ├── Path.Popup.js │ │ ├── Circle.js │ │ ├── Polygon.js │ │ └── Path.js │ ├── marker │ │ ├── DivIcon.js │ │ ├── Icon.Default.js │ │ ├── Marker.Drag.js │ │ ├── Marker.Popup.js │ │ └── Icon.js │ ├── tile │ │ ├── TileLayer.Canvas.js │ │ ├── TileLayer.WMS.js │ │ └── TileLayer.Anim.js │ ├── FeatureGroup.js │ └── LayerGroup.js ├── images │ └── layers.svg ├── Leaflet.js ├── core │ ├── Handler.js │ ├── Class.js │ └── Browser.js ├── map │ ├── handler │ │ ├── Map.DoubleClickZoom.js │ │ ├── Map.ScrollWheelZoom.js │ │ └── Map.Tap.js │ ├── ext │ │ └── Map.Geolocation.js │ └── anim │ │ ├── Map.PanAnimation.js │ │ └── Map.ZoomAnimation.js ├── geometry │ ├── Transformation.js │ ├── PolyUtil.js │ ├── Bounds.js │ └── Point.js ├── dom │ ├── PosAnimation.Timer.js │ ├── PosAnimation.js │ └── DomEvent.DoubleTap.js └── control │ ├── Control.Zoom.js │ ├── Control.js │ ├── Control.Attribution.js │ └── Control.Scale.js ├── .gitignore ├── package.json ├── .travis.yml ├── LICENSE ├── Jakefile.js └── README.md /dist/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/Leaflet/master/dist/images/layers.png -------------------------------------------------------------------------------- /spec/before.js: -------------------------------------------------------------------------------- 1 | // set up before Leaflet files to test L#noConflict later 2 | L = 'test'; 3 | 4 | -------------------------------------------------------------------------------- /dist/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/Leaflet/master/dist/images/layers-2x.png -------------------------------------------------------------------------------- /debug/css/screen.css: -------------------------------------------------------------------------------- 1 | #map { 2 | width: 800px; 3 | height: 600px; 4 | border: 1px solid #ccc; 5 | } -------------------------------------------------------------------------------- /dist/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/Leaflet/master/dist/images/marker-icon.png -------------------------------------------------------------------------------- /dist/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/Leaflet/master/dist/images/marker-icon-2x.png -------------------------------------------------------------------------------- /dist/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/Leaflet/master/dist/images/marker-shadow.png -------------------------------------------------------------------------------- /debug/css/mobile.css: -------------------------------------------------------------------------------- 1 | html, body, #map { 2 | margin: 0; 3 | padding: 0; 4 | width: 100%; 5 | height: 100%; 6 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | tmp/**/* 4 | .idea 5 | .idea/**/* 6 | *.iml 7 | *.sublime-* 8 | _site 9 | coverage/ 10 | -------------------------------------------------------------------------------- /spec/after.js: -------------------------------------------------------------------------------- 1 | // put after Leaflet files as imagePath can't be detected in a PhantomJS env 2 | L.Icon.Default.imagePath = "/base/dist/images"; 3 | -------------------------------------------------------------------------------- /src/geo/projection/Projection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Projection contains various geographical projections used by CRS classes. 3 | */ 4 | 5 | L.Projection = {}; 6 | -------------------------------------------------------------------------------- /src/copyright.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet, a JavaScript library for mobile-friendly interactive maps. http://leafletjs.com 3 | (c) 2010-2013, Vladimir Agafonkin 4 | (c) 2010-2011, CloudMade 5 | */ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | tmp/**/* 4 | .idea 5 | .idea/**/* 6 | *.iml 7 | *.sublime-* 8 | _site 9 | dist/*.js 10 | coverage/ 11 | *.js.html 12 | index.html 13 | .mailmap 14 | -------------------------------------------------------------------------------- /debug/map/iframe.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /spec/suites/control/Control.ScaleSpec.js: -------------------------------------------------------------------------------- 1 | describe("Control.Scale", function () { 2 | it("can be added to an unloaded map", function () { 3 | var map = L.map(document.createElement('div')); 4 | new L.Control.Scale().addTo(map); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/layer/vector/canvas/CircleMarker.Canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CircleMarker canvas specific drawing parts. 3 | */ 4 | 5 | L.CircleMarker.include(!L.Path.CANVAS ? {} : { 6 | _updateStyle: function () { 7 | L.Path.prototype._updateStyle.call(this); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.EPSG4326.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CRS.EPSG4326 is a CRS popular among advanced GIS specialists. 3 | */ 4 | 5 | L.CRS.EPSG4326 = L.extend({}, L.CRS, { 6 | code: 'EPSG:4326', 7 | 8 | projection: L.Projection.LonLat, 9 | transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5) 10 | }); 11 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.Simple.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A simple CRS that can be used for flat non-Earth maps like panoramas or game maps. 3 | */ 4 | 5 | L.CRS.Simple = L.extend({}, L.CRS, { 6 | projection: L.Projection.LonLat, 7 | transformation: new L.Transformation(1, 0, -1, 0), 8 | 9 | scale: function (zoom) { 10 | return Math.pow(2, zoom); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /spec/suites/LeafletSpec.js: -------------------------------------------------------------------------------- 1 | describe('L#noConflict', function () { 2 | it('restores the previous L value and returns Leaflet namespace', function () { 3 | 4 | expect(L.version).to.be.ok(); 5 | 6 | var L2 = L.noConflict(); 7 | 8 | expect(L).to.eql('test'); 9 | expect(L2.version).to.be.ok(); 10 | 11 | window.L = L2; 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.EPSG3395.js: -------------------------------------------------------------------------------- 1 | 2 | L.CRS.EPSG3395 = L.extend({}, L.CRS, { 3 | code: 'EPSG:3395', 4 | 5 | projection: L.Projection.Mercator, 6 | 7 | transformation: (function () { 8 | var m = L.Projection.Mercator, 9 | r = m.R_MAJOR, 10 | scale = 0.5 / (Math.PI * r); 11 | 12 | return new L.Transformation(scale, 0.5, -scale, 0.5); 13 | }()) 14 | }); 15 | -------------------------------------------------------------------------------- /src/geo/projection/Projection.LonLat.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple equirectangular (Plate Carree) projection, used by CRS like EPSG:4326 and Simple. 3 | */ 4 | 5 | L.Projection.LonLat = { 6 | project: function (latlng) { 7 | return new L.Point(latlng.lng, latlng.lat); 8 | }, 9 | 10 | unproject: function (point) { 11 | return new L.LatLng(point.y, point.x); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/suites/layer/vector/CircleSpec.js: -------------------------------------------------------------------------------- 1 | describe('Circle', function () { 2 | describe('#getBounds', function () { 3 | 4 | var circle; 5 | 6 | beforeEach(function () { 7 | circle = L.circle([50, 30], 200); 8 | }); 9 | 10 | it('returns bounds', function () { 11 | var bounds = circle.getBounds(); 12 | 13 | expect(bounds.getSouthWest().equals([49.998203369, 29.997204939])).to.be.ok(); 14 | expect(bounds.getNorthEast().equals([50.001796631, 30.002795061])).to.be.ok(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/images/layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/layer/vector/canvas/Circle.Canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extends L.Circle with Canvas-specific code. 3 | */ 4 | 5 | L.Circle.include(!L.Path.CANVAS ? {} : { 6 | _drawPath: function () { 7 | var p = this._point; 8 | this._ctx.beginPath(); 9 | this._ctx.arc(p.x, p.y, this._radius, 0, Math.PI * 2, false); 10 | }, 11 | 12 | _containsPoint: function (p) { 13 | var center = this._point, 14 | w2 = this.options.stroke ? this.options.weight / 2 : 0; 15 | 16 | return (p.distanceTo(center) <= this._radius + w2); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /spec/spec.hintrc.js: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | "predef": ["define", "L", "expect", "describe", "it", "sinon", "happen", "beforeEach", "afterEach"], 5 | "strict": false, 6 | "bitwise": true, 7 | "camelcase": true, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "forin": false, 11 | "immed": true, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "noempty": true, 16 | "nonew": true, 17 | "undef": true, 18 | // "unused": true, 19 | // "quotmark": "single", 20 | "indent": 4, 21 | "trailing": true, 22 | "white": true, 23 | "smarttabs": true 24 | // "maxlen": 120 25 | } 26 | -------------------------------------------------------------------------------- /src/Leaflet.js: -------------------------------------------------------------------------------- 1 | 2 | var oldL = window.L, 3 | L = {}; 4 | 5 | L.version = '0.7'; 6 | 7 | // define Leaflet for Node module pattern loaders, including Browserify 8 | if (typeof module === 'object' && typeof module.exports === 'object') { 9 | module.exports = L; 10 | 11 | // define Leaflet as an AMD module 12 | } else if (typeof define === 'function' && define.amd) { 13 | define(L); 14 | } 15 | 16 | // define Leaflet as a global L variable, saving the original L to restore later if needed 17 | 18 | L.noConflict = function () { 19 | window.L = oldL; 20 | return this; 21 | }; 22 | 23 | window.L = L; 24 | -------------------------------------------------------------------------------- /src/core/Handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | L.Handler is a base class for handler classes that are used internally to inject 3 | interaction features like dragging to classes like Map and Marker. 4 | */ 5 | 6 | L.Handler = L.Class.extend({ 7 | initialize: function (map) { 8 | this._map = map; 9 | }, 10 | 11 | enable: function () { 12 | if (this._enabled) { return; } 13 | 14 | this._enabled = true; 15 | this.addHooks(); 16 | }, 17 | 18 | disable: function () { 19 | if (!this._enabled) { return; } 20 | 21 | this._enabled = false; 22 | this.removeHooks(); 23 | }, 24 | 25 | enabled: function () { 26 | return !!this._enabled; 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet", 3 | "version": "0.7.0", 4 | "description": "JavaScript library for mobile-friendly interactive maps", 5 | "devDependencies": { 6 | "jake": "~0.7.4", 7 | "jshint": "~2.3.0", 8 | "uglify-js": "~2.4.3", 9 | "mocha": "~1.14.0", 10 | "happen": "~0.1.3", 11 | "karma": "~0.10.4", 12 | "karma-mocha": "~0.1.0" 13 | }, 14 | "main": "dist/leaflet-src.js", 15 | "scripts": { 16 | "test": "jake test", 17 | "prepublish": "jake build" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/Leaflet/Leaflet.git" 22 | }, 23 | "keywords": [ 24 | "gis", 25 | "map" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.EPSG3857.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CRS.EPSG3857 (Spherical Mercator) is the most common CRS for web mapping 3 | * and is used by Leaflet by default. 4 | */ 5 | 6 | L.CRS.EPSG3857 = L.extend({}, L.CRS, { 7 | code: 'EPSG:3857', 8 | 9 | projection: L.Projection.SphericalMercator, 10 | transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5), 11 | 12 | project: function (latlng) { // (LatLng) -> Point 13 | var projectedPoint = this.projection.project(latlng), 14 | earthRadius = 6378137; 15 | return projectedPoint.multiplyBy(earthRadius); 16 | } 17 | }); 18 | 19 | L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { 20 | code: 'EPSG:900913' 21 | }); 22 | -------------------------------------------------------------------------------- /spec/suites/geometry/PolyUtilSpec.js: -------------------------------------------------------------------------------- 1 | describe('PolyUtil', function () { 2 | 3 | describe('#clipPolygon', function () { 4 | it('clips polygon by bounds', function () { 5 | var bounds = L.bounds([0, 0], [10, 10]); 6 | 7 | var points = [ 8 | new L.Point(5, 5), 9 | new L.Point(15, 10), 10 | new L.Point(10, 15) 11 | ]; 12 | 13 | var clipped = L.PolyUtil.clipPolygon(points, bounds); 14 | 15 | for (var i = 0, len = clipped.length; i < len; i++) { 16 | delete clipped[i]._code; 17 | } 18 | 19 | expect(clipped).to.eql([ 20 | new L.Point(7.5, 10), 21 | new L.Point(5, 5), 22 | new L.Point(10, 7.5), 23 | new L.Point(10, 10) 24 | ]); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /debug/tests/rtl2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /debug/tests/remove_while_dragging.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/map/handler/Map.DoubleClickZoom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default. 3 | */ 4 | 5 | L.Map.mergeOptions({ 6 | doubleClickZoom: true 7 | }); 8 | 9 | L.Map.DoubleClickZoom = L.Handler.extend({ 10 | addHooks: function () { 11 | this._map.on('dblclick', this._onDoubleClick, this); 12 | }, 13 | 14 | removeHooks: function () { 15 | this._map.off('dblclick', this._onDoubleClick, this); 16 | }, 17 | 18 | _onDoubleClick: function (e) { 19 | var map = this._map, 20 | zoom = map.getZoom() + (e.originalEvent.shiftKey ? -1 : 1); 21 | 22 | if (map.options.doubleClickZoom === 'center') { 23 | map.setZoom(zoom); 24 | } else { 25 | map.setZoomAround(e.containerPoint, zoom); 26 | } 27 | } 28 | }); 29 | 30 | L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom); 31 | -------------------------------------------------------------------------------- /src/geo/projection/Projection.SphericalMercator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Spherical Mercator is the most popular map projection, used by EPSG:3857 CRS used by default. 3 | */ 4 | 5 | L.Projection.SphericalMercator = { 6 | MAX_LATITUDE: 85.0511287798, 7 | 8 | project: function (latlng) { // (LatLng) -> Point 9 | var d = L.LatLng.DEG_TO_RAD, 10 | max = this.MAX_LATITUDE, 11 | lat = Math.max(Math.min(max, latlng.lat), -max), 12 | x = latlng.lng * d, 13 | y = lat * d; 14 | 15 | y = Math.log(Math.tan((Math.PI / 4) + (y / 2))); 16 | 17 | return new L.Point(x, y); 18 | }, 19 | 20 | unproject: function (point) { // (Point, Boolean) -> LatLng 21 | var d = L.LatLng.RAD_TO_DEG, 22 | lng = point.x * d, 23 | lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d; 24 | 25 | return new L.LatLng(lat, lng); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/layer/vector/Rectangle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object. 3 | */ 4 | 5 | L.Rectangle = L.Polygon.extend({ 6 | initialize: function (latLngBounds, options) { 7 | L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options); 8 | }, 9 | 10 | setBounds: function (latLngBounds) { 11 | this.setLatLngs(this._boundsToLatLngs(latLngBounds)); 12 | }, 13 | 14 | _boundsToLatLngs: function (latLngBounds) { 15 | latLngBounds = L.latLngBounds(latLngBounds); 16 | return [ 17 | latLngBounds.getSouthWest(), 18 | latLngBounds.getNorthWest(), 19 | latLngBounds.getNorthEast(), 20 | latLngBounds.getSouthEast() 21 | ]; 22 | } 23 | }); 24 | 25 | L.rectangle = function (latLngBounds, options) { 26 | return new L.Rectangle(latLngBounds, options); 27 | }; 28 | -------------------------------------------------------------------------------- /src/layer/vector/canvas/Polyline.Canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extends L.Polyline to be able to manually detect clicks on Canvas-rendered polylines. 3 | */ 4 | 5 | L.Polyline.include(!L.Path.CANVAS ? {} : { 6 | _containsPoint: function (p, closed) { 7 | var i, j, k, len, len2, dist, part, 8 | w = this.options.weight / 2; 9 | 10 | if (L.Browser.touch) { 11 | w += 10; // polyline click tolerance on touch devices 12 | } 13 | 14 | for (i = 0, len = this._parts.length; i < len; i++) { 15 | part = this._parts[i]; 16 | for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { 17 | if (!closed && (j === 0)) { 18 | continue; 19 | } 20 | 21 | dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]); 22 | 23 | if (dist <= w) { 24 | return true; 25 | } 26 | } 27 | } 28 | return false; 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /debug/map/wms-marble.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/geometry/Transformation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Transformation is an utility class to perform simple point transformations through a 2d-matrix. 3 | */ 4 | 5 | L.Transformation = function (a, b, c, d) { 6 | this._a = a; 7 | this._b = b; 8 | this._c = c; 9 | this._d = d; 10 | }; 11 | 12 | L.Transformation.prototype = { 13 | transform: function (point, scale) { // (Point, Number) -> Point 14 | return this._transform(point.clone(), scale); 15 | }, 16 | 17 | // destructive transform (faster) 18 | _transform: function (point, scale) { 19 | scale = scale || 1; 20 | point.x = scale * (this._a * point.x + this._b); 21 | point.y = scale * (this._c * point.y + this._d); 22 | return point; 23 | }, 24 | 25 | untransform: function (point, scale) { 26 | scale = scale || 1; 27 | return new L.Point( 28 | (point.x / scale - this._b) / this._a, 29 | (point.y / scale - this._d) / this._c); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CRS is a base object for all defined CRS (Coordinate Reference Systems) in Leaflet. 3 | */ 4 | 5 | L.CRS = { 6 | latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point 7 | var projectedPoint = this.projection.project(latlng), 8 | scale = this.scale(zoom); 9 | 10 | return this.transformation._transform(projectedPoint, scale); 11 | }, 12 | 13 | pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng 14 | var scale = this.scale(zoom), 15 | untransformedPoint = this.transformation.untransform(point, scale); 16 | 17 | return this.projection.unproject(untransformedPoint); 18 | }, 19 | 20 | project: function (latlng) { 21 | return this.projection.project(latlng); 22 | }, 23 | 24 | scale: function (zoom) { 25 | return 256 * Math.pow(2, zoom); 26 | }, 27 | 28 | getSize: function (zoom) { 29 | var s = this.scale(zoom); 30 | return L.point(s, s); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /spec/suites/geometry/TransformationSpec.js: -------------------------------------------------------------------------------- 1 | describe("Transformation", function () { 2 | var t, p; 3 | 4 | beforeEach(function () { 5 | t = new L.Transformation(1, 2, 3, 4); 6 | p = new L.Point(10, 20); 7 | }); 8 | 9 | describe('#transform', function () { 10 | it("performs a transformation", function () { 11 | var p2 = t.transform(p, 2); 12 | expect(p2).to.eql(new L.Point(24, 128)); 13 | }); 14 | it('assumes a scale of 1 if not specified', function () { 15 | var p2 = t.transform(p); 16 | expect(p2).to.eql(new L.Point(12, 64)); 17 | }); 18 | }); 19 | 20 | describe('#untransform', function () { 21 | it("performs a reverse transformation", function () { 22 | var p2 = t.transform(p, 2); 23 | var p3 = t.untransform(p2, 2); 24 | expect(p3).to.eql(p); 25 | }); 26 | it('assumes a scale of 1 if not specified', function () { 27 | var p2 = t.transform(p); 28 | expect(t.untransform(new L.Point(12, 64))).to.eql(new L.Point(10, 20)); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/suites/dom/PosAnimationSpec.js: -------------------------------------------------------------------------------- 1 | describe('PosAnimation', function () { 2 | var el; 3 | 4 | beforeEach(function () { 5 | el = document.createElement('div'); 6 | this.subject = new L.PosAnimation(); 7 | this.subject._el = el; 8 | }); 9 | 10 | describe('#_onStep', function () { 11 | it("sets element position and fires step event if it is able to get current position", function () { 12 | var point = new L.Point(5, 5, true); 13 | sinon.stub(this.subject, '_getPos').returns(point); 14 | this.subject.fire = sinon.stub(); 15 | this.subject._onStep(); 16 | expect(this.subject.fire.withArgs('step').calledOnce).to.be(true); 17 | expect(L.DomUtil.getPosition(this.subject._el)).to.be(point); 18 | }); 19 | 20 | it('stops transition if a position returned', function () { 21 | sinon.stub(this.subject, '_onTransitionEnd'); 22 | sinon.stub(this.subject, '_getPos').returns(undefined); 23 | this.subject._onStep(); 24 | expect(this.subject._onTransitionEnd.calledOnce).to.be(true); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/layer/vector/canvas/Polygon.Canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extends L.Polygon to be able to manually detect clicks on Canvas-rendered polygons. 3 | */ 4 | 5 | L.Polygon.include(!L.Path.CANVAS ? {} : { 6 | _containsPoint: function (p) { 7 | var inside = false, 8 | part, p1, p2, 9 | i, j, k, 10 | len, len2; 11 | 12 | // TODO optimization: check if within bounds first 13 | 14 | if (L.Polyline.prototype._containsPoint.call(this, p, true)) { 15 | // click on polygon border 16 | return true; 17 | } 18 | 19 | // ray casting algorithm for detecting if point is in polygon 20 | 21 | for (i = 0, len = this._parts.length; i < len; i++) { 22 | part = this._parts[i]; 23 | 24 | for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { 25 | p1 = part[j]; 26 | p2 = part[k]; 27 | 28 | if (((p1.y > p.y) !== (p2.y > p.y)) && 29 | (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { 30 | inside = !inside; 31 | } 32 | } 33 | } 34 | 35 | return inside; 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /debug/map/simple-proj.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/layer/marker/DivIcon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.DivIcon is a lightweight HTML-based icon class (as opposed to the image-based L.Icon) 3 | * to use with L.Marker. 4 | */ 5 | 6 | L.DivIcon = L.Icon.extend({ 7 | options: { 8 | iconSize: [12, 12], // also can be set through CSS 9 | /* 10 | iconAnchor: (Point) 11 | popupAnchor: (Point) 12 | html: (String) 13 | bgPos: (Point) 14 | */ 15 | className: 'leaflet-div-icon', 16 | html: false 17 | }, 18 | 19 | createIcon: function (oldIcon) { 20 | var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'), 21 | options = this.options; 22 | 23 | if (options.html !== false) { 24 | div.innerHTML = options.html; 25 | } else { 26 | div.innerHTML = ''; 27 | } 28 | 29 | if (options.bgPos) { 30 | div.style.backgroundPosition = 31 | (-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px'; 32 | } 33 | 34 | this._setIconStyles(div, 'icon'); 35 | return div; 36 | }, 37 | 38 | createShadow: function () { 39 | return null; 40 | } 41 | }); 42 | 43 | L.divIcon = function (options) { 44 | return new L.DivIcon(options); 45 | }; 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | env: 6 | global: 7 | - "ARTIFACTS_S3_BUCKET=leaflet-cdn" 8 | - secure: |- 9 | LnvY/vWpmAIObabLrpu1mWYw1udllVpZJrrhzsn3traL+qU6yaGniQOn6u+l 10 | iWSCuu7kXX3xv1GD7Fc6lTfQCg9F9dukWv9zlc4gFciyRpiUBuluuqtdV51A 11 | 5yqpLkMpX2PMG7vwrOYttVW0uDlUcwGjyHxWZvnBOXCnnHSpnbI= 12 | - secure: |- 13 | EQ4c2c8VklzFZRxKnizI0/VK0anHhlyc1Rv0vqkMj/YPKxmbWNfOlsOCN2gM 14 | p+q8QzCG1Np9D1Kq9K0miYqHgZxgu4D/4Mwy04bh1UfyoUcDfB1tJmEtsKY/ 15 | 8Bl46ZfhxbTG39b6Y315GuU+49QdFMEXhSqx/G7on1xC4aYLXLc= 16 | before_script: > 17 | test ${TRAVIS_BRANCH} = master || 18 | test ${TRAVIS_BRANCH} = stable && 19 | test ${TRAVIS_PULL_REQUEST} = false && 20 | gem install travis-artifacts || true 21 | after_success: > 22 | test ${TRAVIS_BRANCH} = master || 23 | test ${TRAVIS_BRANCH} = stable && 24 | test ${TRAVIS_PULL_REQUEST} = false && 25 | travis-artifacts upload --path dist --target-path build/${TRAVIS_BRANCH} && 26 | cd dist && zip -x .DS_Store -r leaflet-${TRAVIS_BRANCH}.zip . && 27 | travis-artifacts upload --path leaflet-${TRAVIS_BRANCH}.zip --target-path build 28 | -------------------------------------------------------------------------------- /debug/map/geolocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet geolocation debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /debug/tests/rtl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 |

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 |
Click in field then scroll map (in up/left direction) to see shift of map tiles. 19 |
: 20 | 21 |
22 |
23 | Bug tested to occur on: Safari on Mac (Tested in 5.1.7), iPad/iPhone 5.1.1., Android 4 Browser. Hack is in L.Browser.chrome and TileLayer._addTile 24 | 25 |
26 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Jakefile.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet building, testing and linting scripts. 3 | 4 | To use, install Node, then run the following commands in the project root: 5 | 6 | npm install -g jake 7 | npm install 8 | 9 | To check the code for errors and build Leaflet from source, run "jake". 10 | To run the tests, run "jake test". 11 | 12 | For a custom build, open build/build.html in the browser and follow the instructions. 13 | */ 14 | 15 | var build = require('./build/build.js'); 16 | 17 | function hint(msg, paths) { 18 | return function () { 19 | console.log(msg); 20 | jake.exec('node node_modules/jshint/bin/jshint -c ' + paths, 21 | {printStdout: true}, function () { 22 | console.log('\tCheck passed.\n'); 23 | complete(); 24 | }); 25 | } 26 | } 27 | 28 | desc('Check Leaflet source for errors with JSHint'); 29 | task('lint', {async: true}, hint('Checking for JS errors...', 'build/hintrc.js src')); 30 | 31 | desc('Check Leaflet specs source for errors with JSHint'); 32 | task('lintspec', {async: true}, hint('Checking for specs JS errors...', 'spec/spec.hintrc.js spec/suites')); 33 | 34 | desc('Combine and compress Leaflet source files'); 35 | task('build', {async: true}, function () { 36 | build.build(complete); 37 | }); 38 | 39 | desc('Run PhantomJS tests'); 40 | task('test', ['lint', 'lintspec'], {async: true}, function () { 41 | build.test(complete); 42 | }); 43 | 44 | task('default', ['test', 'build']); 45 | 46 | jake.addListener('complete', function () { 47 | process.exit(); 48 | }); 49 | -------------------------------------------------------------------------------- /debug/vector/vector-simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /debug/tests/canvasloop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /debug/tests/removetilewhilepan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /debug/map/zoomlevels.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /debug/map/map-mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /debug/map/scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /spec/suites/layer/vector/PolylineSpec.js: -------------------------------------------------------------------------------- 1 | describe('Polyline', 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 polyline = new L.Polyline(sourceLatLngs); 18 | 19 | expect(sourceLatLngs).to.eql(originalLatLngs); 20 | expect(polyline._latlngs).to.not.eql(sourceLatLngs); 21 | }); 22 | }); 23 | 24 | describe("#setLatLngs", function () { 25 | it("doesn't overwrite the given latlng array", function () { 26 | var originalLatLngs = [ 27 | [1, 2], 28 | [3, 4] 29 | ]; 30 | var sourceLatLngs = originalLatLngs.slice(); 31 | 32 | var polyline = new L.Polyline(sourceLatLngs); 33 | 34 | polyline.setLatLngs(sourceLatLngs); 35 | 36 | expect(sourceLatLngs).to.eql(originalLatLngs); 37 | }); 38 | }); 39 | 40 | describe("#spliceLatLngs", function () { 41 | it("splices the internal latLngs", function () { 42 | var latLngs = [ 43 | [1, 2], 44 | [3, 4], 45 | [5, 6] 46 | ]; 47 | 48 | var polyline = new L.Polyline(latLngs); 49 | 50 | polyline.spliceLatLngs(1, 1, [7, 8]); 51 | 52 | expect(polyline._latlngs).to.eql([L.latLng([1, 2]), L.latLng([7, 8]), L.latLng([5, 6])]); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/layer/tile/TileLayer.Canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.TileLayer.Canvas is a class that you can use as a base for creating 3 | * dynamically drawn Canvas-based tile layers. 4 | */ 5 | 6 | L.TileLayer.Canvas = L.TileLayer.extend({ 7 | options: { 8 | async: false 9 | }, 10 | 11 | initialize: function (options) { 12 | L.setOptions(this, options); 13 | }, 14 | 15 | redraw: function () { 16 | if (this._map) { 17 | this._reset({hard: true}); 18 | this._update(); 19 | } 20 | 21 | for (var i in this._tiles) { 22 | this._redrawTile(this._tiles[i]); 23 | } 24 | return this; 25 | }, 26 | 27 | _redrawTile: function (tile) { 28 | this.drawTile(tile, tile._tilePoint, this._map._zoom); 29 | }, 30 | 31 | _createTile: function () { 32 | var tile = L.DomUtil.create('canvas', 'leaflet-tile'); 33 | tile.width = tile.height = this.options.tileSize; 34 | tile.onselectstart = tile.onmousemove = L.Util.falseFn; 35 | return tile; 36 | }, 37 | 38 | _loadTile: function (tile, tilePoint) { 39 | tile._layer = this; 40 | tile._tilePoint = tilePoint; 41 | 42 | this._redrawTile(tile); 43 | 44 | if (!this.options.async) { 45 | this.tileDrawn(tile); 46 | } 47 | }, 48 | 49 | drawTile: function (/*tile, tilePoint*/) { 50 | // override with rendering code 51 | }, 52 | 53 | tileDrawn: function (tile) { 54 | this._tileOnLoad.call(tile); 55 | } 56 | }); 57 | 58 | 59 | L.tileLayer.canvas = function (options) { 60 | return new L.TileLayer.Canvas(options); 61 | }; 62 | -------------------------------------------------------------------------------- /debug/map/wms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /spec/suites/layer/LayerGroupSpec.js: -------------------------------------------------------------------------------- 1 | describe('LayerGroup', function () { 2 | describe("#addLayer", function () { 3 | it('adds a layer', function () { 4 | var lg = L.layerGroup(), 5 | marker = L.marker([0, 0]); 6 | 7 | expect(lg.addLayer(marker)).to.eql(lg); 8 | 9 | expect(lg.hasLayer(marker)).to.be(true); 10 | }); 11 | }); 12 | describe("#removeLayer", function () { 13 | it('removes a layer', function () { 14 | var lg = L.layerGroup(), 15 | marker = L.marker([0, 0]); 16 | 17 | lg.addLayer(marker); 18 | expect(lg.removeLayer(marker)).to.eql(lg); 19 | 20 | expect(lg.hasLayer(marker)).to.be(false); 21 | }); 22 | }); 23 | describe("#clearLayers", function () { 24 | it('removes all layers', function () { 25 | var lg = L.layerGroup(), 26 | marker = L.marker([0, 0]); 27 | 28 | lg.addLayer(marker); 29 | expect(lg.clearLayers()).to.eql(lg); 30 | 31 | expect(lg.hasLayer(marker)).to.be(false); 32 | }); 33 | }); 34 | describe("#getLayers", function () { 35 | it('gets all layers', function () { 36 | var lg = L.layerGroup(), 37 | marker = L.marker([0, 0]); 38 | 39 | lg.addLayer(marker); 40 | 41 | expect(lg.getLayers()).to.eql([marker]); 42 | }); 43 | }); 44 | describe("#eachLayer", function () { 45 | it('iterates over all layers', function () { 46 | var lg = L.layerGroup(), 47 | marker = L.marker([0, 0]), 48 | ctx = { foo: 'bar' }; 49 | 50 | lg.addLayer(marker); 51 | 52 | lg.eachLayer(function (layer) { 53 | expect(layer).to.eql(marker); 54 | expect(this).to.eql(ctx); 55 | }, ctx); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /debug/leaflet-include.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function getFiles() { 3 | var memo = {}, 4 | files = [], 5 | i, src; 6 | 7 | function addFiles(srcs) { 8 | for (var j = 0, len = srcs.length; j < len; j++) { 9 | memo[srcs[j]] = true; 10 | } 11 | } 12 | 13 | for (i in deps) { 14 | addFiles(deps[i].src); 15 | } 16 | 17 | for (src in memo) { 18 | files.push(src); 19 | } 20 | 21 | return files; 22 | } 23 | var scripts = getFiles(); 24 | 25 | function getSrcUrl() { 26 | var scripts = document.getElementsByTagName('script'); 27 | for (var i = 0; i < scripts.length; i++) { 28 | var src = scripts[i].src; 29 | if (src) { 30 | var res = src.match(/^(.*)leaflet-include\.js$/); 31 | if (res) { 32 | return res[1] + '../src/'; 33 | } 34 | } 35 | } 36 | } 37 | 38 | var path = getSrcUrl(); 39 | for (var i = 0; i < scripts.length; i++) { 40 | document.writeln(""); 41 | } 42 | document.writeln(''); 43 | })(); 44 | 45 | function getRandomLatLng(map) { 46 | var bounds = map.getBounds(), 47 | southWest = bounds.getSouthWest(), 48 | northEast = bounds.getNorthEast(), 49 | lngSpan = northEast.lng - southWest.lng, 50 | latSpan = northEast.lat - southWest.lat; 51 | 52 | return new L.LatLng( 53 | southWest.lat + latSpan * Math.random(), 54 | southWest.lng + lngSpan * Math.random()); 55 | } 56 | 57 | function logEvent(e) { 58 | console.log(e.type); 59 | } 60 | -------------------------------------------------------------------------------- /debug/tests/svg_clicks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/layer/vector/Path.Popup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Popup extension to L.Path (polylines, polygons, circles), adding popup-related methods. 3 | */ 4 | 5 | L.Path.include({ 6 | 7 | bindPopup: function (content, options) { 8 | 9 | if (content instanceof L.Popup) { 10 | this._popup = content; 11 | } else { 12 | if (!this._popup || options) { 13 | this._popup = new L.Popup(options, this); 14 | } 15 | this._popup.setContent(content); 16 | } 17 | 18 | if (!this._popupHandlersAdded) { 19 | this 20 | .on('click', this._openPopup, this) 21 | .on('remove', this.closePopup, this); 22 | 23 | this._popupHandlersAdded = true; 24 | } 25 | 26 | return this; 27 | }, 28 | 29 | unbindPopup: function () { 30 | if (this._popup) { 31 | this._popup = null; 32 | this 33 | .off('click', this._openPopup) 34 | .off('remove', this.closePopup); 35 | 36 | this._popupHandlersAdded = false; 37 | } 38 | return this; 39 | }, 40 | 41 | openPopup: function (latlng) { 42 | 43 | if (this._popup) { 44 | // open the popup from one of the path's points if not specified 45 | latlng = latlng || this._latlng || 46 | this._latlngs[Math.floor(this._latlngs.length / 2)]; 47 | 48 | this._openPopup({latlng: latlng}); 49 | } 50 | 51 | return this; 52 | }, 53 | 54 | closePopup: function () { 55 | if (this._popup) { 56 | this._popup._close(); 57 | } 58 | return this; 59 | }, 60 | 61 | _openPopup: function (e) { 62 | this._popup.setLatLng(e.latlng); 63 | this._map.openPopup(this._popup); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /debug/tests/set_icon_reuse_dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test for preservation of Icon DOM element 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /debug/tests/click_on_canvas_broken.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /src/geometry/PolyUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.PolyUtil contains utility functions for polygons (clipping, etc.). 3 | */ 4 | 5 | /*jshint bitwise:false */ // allow bitwise operations here 6 | 7 | L.PolyUtil = {}; 8 | 9 | /* 10 | * Sutherland-Hodgeman polygon clipping algorithm. 11 | * Used to avoid rendering parts of a polygon that are not currently visible. 12 | */ 13 | L.PolyUtil.clipPolygon = function (points, bounds) { 14 | var clippedPoints, 15 | edges = [1, 4, 2, 8], 16 | i, j, k, 17 | a, b, 18 | len, edge, p, 19 | lu = L.LineUtil; 20 | 21 | for (i = 0, len = points.length; i < len; i++) { 22 | points[i]._code = lu._getBitCode(points[i], bounds); 23 | } 24 | 25 | // for each edge (left, bottom, right, top) 26 | for (k = 0; k < 4; k++) { 27 | edge = edges[k]; 28 | clippedPoints = []; 29 | 30 | for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { 31 | a = points[i]; 32 | b = points[j]; 33 | 34 | // if a is inside the clip window 35 | if (!(a._code & edge)) { 36 | // if b is outside the clip window (a->b goes out of screen) 37 | if (b._code & edge) { 38 | p = lu._getEdgeIntersection(b, a, edge, bounds); 39 | p._code = lu._getBitCode(p, bounds); 40 | clippedPoints.push(p); 41 | } 42 | clippedPoints.push(a); 43 | 44 | // else if b is inside the clip window (a->b enters the screen) 45 | } else if (!(b._code & edge)) { 46 | p = lu._getEdgeIntersection(b, a, edge, bounds); 47 | p._code = lu._getBitCode(p, bounds); 48 | clippedPoints.push(p); 49 | } 50 | } 51 | points = clippedPoints; 52 | } 53 | 54 | return points; 55 | }; 56 | -------------------------------------------------------------------------------- /spec/suites/geo/CRSSpec.js: -------------------------------------------------------------------------------- 1 | describe("CRS.EPSG3395", function () { 2 | var crs = L.CRS.EPSG3395; 3 | 4 | describe("#latLngToPoint", function () { 5 | it("projects a center point", function () { 6 | expect(crs.latLngToPoint(L.latLng(0, 0), 0)).near(new L.Point(128, 128), 0.01); 7 | }); 8 | 9 | it("projects the northeast corner of the world", function () { 10 | expect(crs.latLngToPoint(L.latLng(85.0840591556, 180), 0)).near(new L.Point(256, 0)); 11 | }); 12 | }); 13 | 14 | describe("#pointToLatLng", function () { 15 | it("reprojects a center point", function () { 16 | expect(crs.pointToLatLng(new L.Point(128, 128), 0)).nearLatLng(L.latLng(0, 0), 0.01); 17 | }); 18 | 19 | it("reprojects the northeast corner of the world", function () { 20 | expect(crs.pointToLatLng(new L.Point(256, 0), 0)).nearLatLng(L.latLng(85.0840591556, 180)); 21 | }); 22 | }); 23 | }); 24 | 25 | describe("CRS.EPSG3857", function () { 26 | var crs = L.CRS.EPSG3857; 27 | 28 | describe("#latLngToPoint", function () { 29 | it("projects a center point", function () { 30 | expect(crs.latLngToPoint(L.latLng(0, 0), 0)).near(new L.Point(128, 128), 0.01); 31 | }); 32 | 33 | it("projects the northeast corner of the world", function () { 34 | expect(crs.latLngToPoint(L.latLng(85.0511287798, 180), 0)).near(new L.Point(256, 0)); 35 | }); 36 | }); 37 | 38 | describe("#pointToLatLng", function () { 39 | it("reprojects a center point", function () { 40 | expect(crs.pointToLatLng(new L.Point(128, 128), 0)).nearLatLng(L.latLng(0, 0), 0.01); 41 | }); 42 | 43 | it("reprojects the northeast corner of the world", function () { 44 | expect(crs.pointToLatLng(new L.Point(256, 0), 0)).nearLatLng(L.latLng(85.0511287798, 180)); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /debug/tests/popupcontextmenuclicks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/geo/projection/Projection.Mercator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mercator projection that takes into account that the Earth is not a perfect sphere. 3 | * Less popular than spherical mercator; used by projections like EPSG:3395. 4 | */ 5 | 6 | L.Projection.Mercator = { 7 | MAX_LATITUDE: 85.0840591556, 8 | 9 | R_MINOR: 6356752.314245179, 10 | R_MAJOR: 6378137, 11 | 12 | project: function (latlng) { // (LatLng) -> Point 13 | var d = L.LatLng.DEG_TO_RAD, 14 | max = this.MAX_LATITUDE, 15 | lat = Math.max(Math.min(max, latlng.lat), -max), 16 | r = this.R_MAJOR, 17 | r2 = this.R_MINOR, 18 | x = latlng.lng * d * r, 19 | y = lat * d, 20 | tmp = r2 / r, 21 | eccent = Math.sqrt(1.0 - tmp * tmp), 22 | con = eccent * Math.sin(y); 23 | 24 | con = Math.pow((1 - con) / (1 + con), eccent * 0.5); 25 | 26 | var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con; 27 | y = -r * Math.log(ts); 28 | 29 | return new L.Point(x, y); 30 | }, 31 | 32 | unproject: function (point) { // (Point, Boolean) -> LatLng 33 | var d = L.LatLng.RAD_TO_DEG, 34 | r = this.R_MAJOR, 35 | r2 = this.R_MINOR, 36 | lng = point.x * d / r, 37 | tmp = r2 / r, 38 | eccent = Math.sqrt(1 - (tmp * tmp)), 39 | ts = Math.exp(- point.y / r), 40 | phi = (Math.PI / 2) - 2 * Math.atan(ts), 41 | numIter = 15, 42 | tol = 1e-7, 43 | i = numIter, 44 | dphi = 0.1, 45 | con; 46 | 47 | while ((Math.abs(dphi) > tol) && (--i > 0)) { 48 | con = eccent * Math.sin(phi); 49 | dphi = (Math.PI / 2) - 2 * Math.atan(ts * 50 | Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi; 51 | phi += dphi; 52 | } 53 | 54 | return new L.LatLng(phi * d, lng); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/dom/PosAnimation.Timer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.PosAnimation fallback implementation that powers Leaflet pan animations 3 | * in browsers that don't support CSS3 Transitions. 4 | */ 5 | 6 | L.PosAnimation = L.DomUtil.TRANSITION ? L.PosAnimation : L.PosAnimation.extend({ 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._duration = duration || 0.25; 14 | this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2); 15 | 16 | this._startPos = L.DomUtil.getPosition(el); 17 | this._offset = newPos.subtract(this._startPos); 18 | this._startTime = +new Date(); 19 | 20 | this.fire('start'); 21 | 22 | this._animate(); 23 | }, 24 | 25 | stop: function () { 26 | if (!this._inProgress) { return; } 27 | 28 | this._step(); 29 | this._complete(); 30 | }, 31 | 32 | _animate: function () { 33 | // animation loop 34 | this._animId = L.Util.requestAnimFrame(this._animate, this); 35 | this._step(); 36 | }, 37 | 38 | _step: function () { 39 | var elapsed = (+new Date()) - this._startTime, 40 | duration = this._duration * 1000; 41 | 42 | if (elapsed < duration) { 43 | this._runFrame(this._easeOut(elapsed / duration)); 44 | } else { 45 | this._runFrame(1); 46 | this._complete(); 47 | } 48 | }, 49 | 50 | _runFrame: function (progress) { 51 | var pos = this._startPos.add(this._offset.multiplyBy(progress)); 52 | L.DomUtil.setPosition(this._el, pos); 53 | 54 | this.fire('step'); 55 | }, 56 | 57 | _complete: function () { 58 | L.Util.cancelAnimFrame(this._animId); 59 | 60 | this._inProgress = false; 61 | this.fire('end'); 62 | }, 63 | 64 | _easeOut: function (t) { 65 | return 1 - Math.pow(1 - t, this._easeOutPower); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /debug/vector/rectangle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/layer/marker/Marker.Drag.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable. 3 | */ 4 | 5 | L.Handler.MarkerDrag = L.Handler.extend({ 6 | initialize: function (marker) { 7 | this._marker = marker; 8 | }, 9 | 10 | addHooks: function () { 11 | var icon = this._marker._icon; 12 | if (!this._draggable) { 13 | this._draggable = new L.Draggable(icon, icon); 14 | } 15 | 16 | this._draggable 17 | .on('dragstart', this._onDragStart, this) 18 | .on('drag', this._onDrag, this) 19 | .on('dragend', this._onDragEnd, this); 20 | this._draggable.enable(); 21 | L.DomUtil.addClass(this._marker._icon, 'leaflet-marker-draggable'); 22 | }, 23 | 24 | removeHooks: function () { 25 | this._draggable 26 | .off('dragstart', this._onDragStart, this) 27 | .off('drag', this._onDrag, this) 28 | .off('dragend', this._onDragEnd, this); 29 | 30 | this._draggable.disable(); 31 | L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable'); 32 | }, 33 | 34 | moved: function () { 35 | return this._draggable && this._draggable._moved; 36 | }, 37 | 38 | _onDragStart: function () { 39 | this._marker 40 | .closePopup() 41 | .fire('movestart') 42 | .fire('dragstart'); 43 | }, 44 | 45 | _onDrag: function () { 46 | var marker = this._marker, 47 | shadow = marker._shadow, 48 | iconPos = L.DomUtil.getPosition(marker._icon), 49 | latlng = marker._map.layerPointToLatLng(iconPos); 50 | 51 | // update shadow position 52 | if (shadow) { 53 | L.DomUtil.setPosition(shadow, iconPos); 54 | } 55 | 56 | marker._latlng = latlng; 57 | 58 | marker 59 | .fire('move', {latlng: latlng}) 60 | .fire('drag'); 61 | }, 62 | 63 | _onDragEnd: function (e) { 64 | this._marker 65 | .fire('moveend') 66 | .fire('dragend', e); 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /spec/suites/geo/ProjectionSpec.js: -------------------------------------------------------------------------------- 1 | describe("Projection.Mercator", function () { 2 | var p = L.Projection.Mercator; 3 | 4 | describe("#project", function () { 5 | it("projects a center point", function () { 6 | //edge cases 7 | expect(p.project(new L.LatLng(0, 0))).near(new L.Point(0, 0)); 8 | }); 9 | 10 | it("projects the northeast corner of the world", function () { 11 | expect(p.project(new L.LatLng(90, 180))).near(new L.Point(20037508, 20037508)); 12 | }); 13 | 14 | it("projects the southwest corner of the world", function () { 15 | expect(p.project(new L.LatLng(-90, -180))).near(new L.Point(-20037508, -20037508)); 16 | }); 17 | 18 | it("projects other points", function () { 19 | expect(p.project(new L.LatLng(50, 30))).near(new L.Point(3339584, 6413524)); 20 | 21 | // from https://github.com/Leaflet/Leaflet/issues/1578 22 | expect(p.project(new L.LatLng(51.9371170300465, 80.11230468750001))) 23 | .near(new L.Point(8918060.964088084, 6755099.410887127)); 24 | }); 25 | }); 26 | 27 | describe("#unproject", function () { 28 | function pr(point) { 29 | return p.project(p.unproject(point)); 30 | } 31 | 32 | it("unprojects a center point", function () { 33 | expect(pr(new L.Point(0, 0))).near(new L.Point(0, 0)); 34 | }); 35 | 36 | it("unprojects pi points", function () { 37 | expect(pr(new L.Point(-Math.PI, Math.PI))).near(new L.Point(-Math.PI, Math.PI)); 38 | expect(pr(new L.Point(-Math.PI, -Math.PI))).near(new L.Point(-Math.PI, -Math.PI)); 39 | 40 | expect(pr(new L.Point(0.523598775598, 1.010683188683))).near(new L.Point(0.523598775598, 1.010683188683)); 41 | }); 42 | 43 | it('unprojects other points', function () { 44 | // from https://github.com/Leaflet/Leaflet/issues/1578 45 | expect(pr(new L.Point(8918060.964088084, 6755099.410887127))); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /debug/tests/opacity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /spec/suites/layer/vector/CircleMarkerSpec.js: -------------------------------------------------------------------------------- 1 | describe('CircleMarker', function () { 2 | describe("#_radius", function () { 3 | var map; 4 | beforeEach(function () { 5 | map = L.map(document.createElement('div')); 6 | map.setView([0, 0], 1); 7 | }); 8 | describe("when a CircleMarker is added to the map ", function () { 9 | describe("with a radius set as an option", function () { 10 | it("takes that radius", function () { 11 | var marker = L.circleMarker([0, 0], { radius: 20 }).addTo(map); 12 | 13 | expect(marker._radius).to.be(20); 14 | }); 15 | }); 16 | 17 | describe("and radius is set before adding it", function () { 18 | it("takes that radius", function () { 19 | var marker = L.circleMarker([0, 0], { radius: 20 }); 20 | marker.setRadius(15); 21 | marker.addTo(map); 22 | expect(marker._radius).to.be(15); 23 | }); 24 | }); 25 | 26 | describe("and radius is set after adding it", function () { 27 | it("takes that radius", function () { 28 | var marker = L.circleMarker([0, 0], { radius: 20 }); 29 | marker.addTo(map); 30 | marker.setRadius(15); 31 | expect(marker._radius).to.be(15); 32 | }); 33 | }); 34 | 35 | describe("and setStyle is used to change the radius after adding", function () { 36 | it("takes the given radius", function () { 37 | var marker = L.circleMarker([0, 0], { radius: 20 }); 38 | marker.addTo(map); 39 | marker.setStyle({ radius: 15 }); 40 | expect(marker._radius).to.be(15); 41 | }); 42 | }); 43 | describe("and setStyle is used to change the radius before adding", function () { 44 | it("takes the given radius", function () { 45 | var marker = L.circleMarker([0, 0], { radius: 20 }); 46 | marker.setStyle({ radius: 15 }); 47 | marker.addTo(map); 48 | expect(marker._radius).to.be(15); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /debug/map/controls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /spec/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function (config) { 3 | 4 | var libSources = require(__dirname+'/../build/build.js').getFiles(); 5 | 6 | var files = [ 7 | "spec/before.js", 8 | "spec/sinon.js", 9 | "spec/expect.js" 10 | ].concat(libSources, [ 11 | "spec/after.js", 12 | "node_modules/happen/happen.js", 13 | "spec/suites/SpecHelper.js", 14 | "spec/suites/**/*.js", 15 | {pattern: "dist/images/*.png", included: false} 16 | ]); 17 | 18 | config.set({ 19 | // base path, that will be used to resolve files and exclude 20 | basePath: '../', 21 | 22 | plugins: ['karma-mocha', 'karma-phantomjs-launcher', 'karma-chrome-launcher'], 23 | 24 | // frameworks to use 25 | frameworks: ['mocha'], 26 | 27 | // list of files / patterns to load in the browser 28 | files: files, 29 | exclude: [], 30 | 31 | // test results reporter to use 32 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 33 | reporters: ['dots'], 34 | 35 | // web server port 36 | port: 9876, 37 | 38 | // level of logging 39 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 40 | logLevel: config.LOG_WARN, 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | // enable / disable watching file and executing tests whenever any file changes 46 | autoWatch: false, 47 | 48 | // Start these browsers, currently available: 49 | // - Chrome 50 | // - ChromeCanary 51 | // - Firefox 52 | // - Opera 53 | // - Safari (only Mac) 54 | // - PhantomJS 55 | // - IE (only Windows) 56 | browsers: ['PhantomJS'], 57 | 58 | // If browser does not capture in given timeout [ms], kill it 59 | captureTimeout: 5000, 60 | 61 | // Continuous Integration mode 62 | // if true, it capture browsers, run tests and exit 63 | singleRun: true 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /src/map/handler/Map.ScrollWheelZoom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map. 3 | */ 4 | 5 | L.Map.mergeOptions({ 6 | scrollWheelZoom: true 7 | }); 8 | 9 | L.Map.ScrollWheelZoom = L.Handler.extend({ 10 | addHooks: function () { 11 | L.DomEvent.on(this._map._container, 'mousewheel', this._onWheelScroll, this); 12 | L.DomEvent.on(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault); 13 | this._delta = 0; 14 | }, 15 | 16 | removeHooks: function () { 17 | L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll); 18 | L.DomEvent.off(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault); 19 | }, 20 | 21 | _onWheelScroll: function (e) { 22 | var delta = L.DomEvent.getWheelDelta(e); 23 | 24 | this._delta += delta; 25 | this._lastMousePos = this._map.mouseEventToContainerPoint(e); 26 | 27 | if (!this._startTime) { 28 | this._startTime = +new Date(); 29 | } 30 | 31 | var left = Math.max(40 - (+new Date() - this._startTime), 0); 32 | 33 | clearTimeout(this._timer); 34 | this._timer = setTimeout(L.bind(this._performZoom, this), left); 35 | 36 | L.DomEvent.preventDefault(e); 37 | L.DomEvent.stopPropagation(e); 38 | }, 39 | 40 | _performZoom: function () { 41 | var map = this._map, 42 | delta = this._delta, 43 | zoom = map.getZoom(); 44 | 45 | delta = delta > 0 ? Math.ceil(delta) : Math.floor(delta); 46 | delta = Math.max(Math.min(delta, 4), -4); 47 | delta = map._limitZoom(zoom + delta) - zoom; 48 | 49 | this._delta = 0; 50 | this._startTime = null; 51 | 52 | if (!delta) { return; } 53 | 54 | if (map.options.scrollWheelZoom === 'center') { 55 | map.setZoom(zoom + delta); 56 | } else { 57 | map.setZoomAround(this._lastMousePos, zoom + delta); 58 | } 59 | } 60 | }); 61 | 62 | L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom); 63 | -------------------------------------------------------------------------------- /debug/map/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /debug/tests/dragging_and_copyworldjump.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leaflet debug page 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

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 | Leaflet 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 | [![Build Status](https://travis-ci.org/Leaflet/Leaflet.png?branch=master)](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 | --------------------------------------------------------------------------------