├── spec ├── suites │ ├── layer │ │ ├── tile │ │ │ └── TileLayerSpec.js │ │ ├── ImageOverlaySpec.js │ │ ├── vector │ │ │ ├── PathSpec.js │ │ │ ├── CircleSpec.js │ │ │ ├── PolylineGeometrySpec.js │ │ │ ├── PolylineSpec.js │ │ │ ├── CircleMarkerSpec.js │ │ │ └── PolygonSpec.js │ │ ├── LayerGroupSpec.js │ │ └── FeatureGroupSpec.js │ ├── control │ │ ├── Control.ScaleSpec.js │ │ ├── ControlSpec.js │ │ ├── Control.LayersSpec.js │ │ └── Control.AttributionSpec.js │ ├── geometry │ │ ├── TransformationSpec.js │ │ ├── PolyUtilSpec.js │ │ ├── LineUtilSpec.js │ │ ├── BoundsSpec.js │ │ └── PointSpec.js │ ├── SpecHelper.js │ ├── map │ │ └── handler │ │ │ └── Map.DragSpec.js │ ├── dom │ │ ├── DomUtilSpec.js │ │ └── DomEventSpec.js │ └── geo │ │ └── ProjectionSpec.js ├── after.js ├── spec.hintrc.js ├── eslintrc.json └── karma.conf.js ├── dist └── images │ ├── layers.png │ ├── layers-2x.png │ ├── marker-icon.png │ ├── marker-shadow.png │ └── marker-icon-2x.png ├── debug ├── css │ ├── screen.css │ └── mobile.css ├── map │ ├── iframe.html │ ├── wms-marble.html │ ├── geolocation.html │ ├── max-bounds.html │ ├── grid.html │ ├── zoom-remain-centered.html │ ├── zoomlevels.html │ ├── canvas.html │ ├── wms.html │ ├── scroll.html │ ├── map-mobile.html │ ├── layer_remove_add.html │ ├── image-overlay.html │ ├── simple-proj.html │ ├── controls.html │ ├── map.html │ ├── markers.html │ └── zoompan.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 │ ├── geojson-sample.js │ ├── vector-mobile.html │ ├── vector-simple.html │ ├── rectangle.html │ ├── vector2.html │ ├── vector-bounds.html │ ├── bounds-extend.html │ ├── vector-canvas.html │ └── feature-group-bounds.html ├── hacks │ └── jitter.html └── leaflet-include.js ├── .npmignore ├── src ├── copyright.js ├── geo │ ├── crs │ │ ├── CRS.EPSG4326.js │ │ ├── CRS.EPSG3395.js │ │ ├── CRS.Simple.js │ │ ├── CRS.EPSG3857.js │ │ ├── CRS.Earth.js │ │ └── CRS.js │ ├── projection │ │ ├── Projection.LonLat.js │ │ ├── Projection.SphericalMercator.js │ │ └── Projection.Mercator.js │ └── LatLng.js ├── layer │ ├── marker │ │ ├── Marker.Popup.js │ │ ├── DivIcon.js │ │ ├── Icon.Default.js │ │ ├── Marker.Drag.js │ │ └── Icon.js │ ├── vector │ │ ├── Rectangle.js │ │ ├── CircleMarker.js │ │ ├── Path.js │ │ ├── Polygon.js │ │ ├── Circle.js │ │ └── Renderer.js │ ├── Layer.Popup.js │ ├── FeatureGroup.js │ ├── tile │ │ └── TileLayer.WMS.js │ └── LayerGroup.js ├── images │ └── layers.svg ├── core │ ├── Handler.js │ ├── Browser.js │ └── Class.js ├── Leaflet.js ├── map │ ├── handler │ │ ├── Map.DoubleClickZoom.js │ │ ├── Map.ScrollWheelZoom.js │ │ ├── Map.BoxZoom.js │ │ ├── Map.Tap.js │ │ └── Map.TouchZoom.js │ ├── anim │ │ ├── Map.FlyTo.js │ │ └── Map.PanAnimation.js │ └── ext │ │ └── Map.Geolocation.js ├── geometry │ ├── Transformation.js │ ├── PolyUtil.js │ ├── Bounds.js │ └── Point.js ├── dom │ ├── PosAnimation.js │ └── DomEvent.DoubleTap.js └── control │ ├── Control.Attribution.js │ ├── Control.js │ ├── Control.Scale.js │ └── Control.Zoom.js ├── .gitignore ├── package.json ├── .travis.yml ├── LICENSE ├── Jakefile.js └── README.md /spec/suites/layer/tile/TileLayerSpec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eclipse1979/leaflet/HEAD/dist/images/layers.png -------------------------------------------------------------------------------- /debug/css/screen.css: -------------------------------------------------------------------------------- 1 | #map { 2 | width: 800px; 3 | height: 600px; 4 | border: 1px solid #ccc; 5 | } -------------------------------------------------------------------------------- /dist/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eclipse1979/leaflet/HEAD/dist/images/layers-2x.png -------------------------------------------------------------------------------- /dist/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eclipse1979/leaflet/HEAD/dist/images/marker-icon.png -------------------------------------------------------------------------------- /dist/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eclipse1979/leaflet/HEAD/dist/images/marker-shadow.png -------------------------------------------------------------------------------- /dist/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eclipse1979/leaflet/HEAD/dist/images/marker-icon-2x.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/copyright.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet {VERSION}, a JS library for interactive maps. http://leafletjs.com 3 | (c) 2010-2015 Vladimir Agafonkin, (c) 2010-2011 CloudMade 4 | */ 5 | -------------------------------------------------------------------------------- /debug/map/iframe.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.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 | bower.json 15 | component.json -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/suites/layer/ImageOverlaySpec.js: -------------------------------------------------------------------------------- 1 | describe('ImageOverlay', function () { 2 | describe('#setStyle', function () { 3 | it('sets opacity', function () { 4 | var overlay = L.imageOverlay().setStyle({opacity: 0.5}); 5 | expect(overlay.options.opacity).to.equal(0.5); 6 | }); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /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.Earth, { 6 | code: 'EPSG:4326', 7 | projection: L.Projection.LonLat, 8 | transformation: new L.Transformation(1 / 180, 1, -1 / 180, 0.5) 9 | }); 10 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.EPSG3395.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CRS.EPSG3857 (World Mercator) CRS implementation. 3 | */ 4 | 5 | L.CRS.EPSG3395 = L.extend({}, L.CRS.Earth, { 6 | code: 'EPSG:3395', 7 | projection: L.Projection.Mercator, 8 | 9 | transformation: (function () { 10 | var scale = 0.5 / (Math.PI * L.Projection.Mercator.R); 11 | return new L.Transformation(scale, 0.5, -scale, 0.5); 12 | }()) 13 | }); 14 | -------------------------------------------------------------------------------- /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 = {}; 6 | 7 | L.Projection.LonLat = { 8 | project: function (latlng) { 9 | return new L.Point(latlng.lng, latlng.lat); 10 | }, 11 | 12 | unproject: function (point) { 13 | return new L.LatLng(point.y, point.x); 14 | }, 15 | 16 | bounds: L.bounds([-180, -90], [180, 90]) 17 | }; 18 | -------------------------------------------------------------------------------- /spec/suites/layer/vector/PathSpec.js: -------------------------------------------------------------------------------- 1 | describe('Path', function () { 2 | describe('#bringToBack', function () { 3 | it('is a no-op for layers not on a map', function () { 4 | var path = new L.Path(); 5 | expect(path.bringToBack()).to.equal(path); 6 | }); 7 | }); 8 | 9 | describe('#bringToFront', function () { 10 | it('is a no-op for layers not on a map', function () { 11 | var path = new L.Path(); 12 | expect(path.bringToFront()).to.equal(path); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/layer/marker/Marker.Popup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Popup extension to L.Marker, adding popup-related methods. 3 | */ 4 | 5 | L.Marker.include({ 6 | bindPopup: function (content, options) { 7 | var anchor = L.point(this.options.icon.options.popupAnchor || [0, 0]) 8 | .add(L.Popup.prototype.options.offset); 9 | 10 | options = L.extend({offset: anchor}, options); 11 | 12 | return L.Layer.prototype.bindPopup.call(this, content, options); 13 | }, 14 | 15 | _openPopup: L.Layer.prototype.togglePopup 16 | }); 17 | -------------------------------------------------------------------------------- /src/images/layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | distance: function (latlng1, latlng2) { 14 | var dx = latlng2.lng - latlng1.lng, 15 | dy = latlng2.lat - latlng1.lat; 16 | 17 | return Math.sqrt(dx * dx + dy * dy); 18 | }, 19 | 20 | infinite: true 21 | }); 22 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.EPSG3857.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CRS.EPSG3857 (Spherical Mercator) is the most common CRS for web mapping and is used by Leaflet by default. 3 | */ 4 | 5 | L.CRS.EPSG3857 = L.extend({}, L.CRS.Earth, { 6 | code: 'EPSG:3857', 7 | projection: L.Projection.SphericalMercator, 8 | 9 | transformation: (function () { 10 | var scale = 0.5 / (Math.PI * L.Projection.SphericalMercator.R); 11 | return new L.Transformation(scale, 0.5, -scale, 0.5); 12 | }()) 13 | }); 14 | 15 | L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { 16 | code: 'EPSG:900913' 17 | }); 18 | -------------------------------------------------------------------------------- /spec/suites/layer/vector/CircleSpec.js: -------------------------------------------------------------------------------- 1 | describe('Circle', function () { 2 | describe('#getBounds', function () { 3 | 4 | var map, circle; 5 | 6 | beforeEach(function () { 7 | map = L.map(document.createElement('div')).setView([0, 0], 4); 8 | circle = L.circle([50, 30], 200).addTo(map); 9 | }); 10 | 11 | it('returns bounds', function () { 12 | var bounds = circle.getBounds(); 13 | 14 | expect(bounds.getSouthWest()).nearLatLng(new L.LatLng(49.94347, 29.91211)); 15 | expect(bounds.getNorthEast()).nearLatLng(new L.LatLng(50.05646, 30.08789)); 16 | }); 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/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 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.Earth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CRS.Earth is the base class for all CRS representing Earth. 3 | */ 4 | 5 | L.CRS.Earth = L.extend({}, L.CRS, { 6 | wrapLng: [-180, 180], 7 | 8 | R: 6378137, 9 | 10 | // distance between two geographical points using spherical law of cosines approximation 11 | distance: function (latlng1, latlng2) { 12 | var rad = Math.PI / 180, 13 | lat1 = latlng1.lat * rad, 14 | lat2 = latlng2.lat * rad, 15 | a = Math.sin(lat1) * Math.sin(lat2) + 16 | Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad); 17 | 18 | return this.R * Math.acos(Math.min(a, 1)); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/Leaflet.js: -------------------------------------------------------------------------------- 1 | 2 | var L = { 3 | version: '0.8-dev' 4 | }; 5 | 6 | function expose() { 7 | var oldL = window.L; 8 | 9 | L.noConflict = function () { 10 | window.L = oldL; 11 | return this; 12 | }; 13 | 14 | window.L = L; 15 | } 16 | 17 | // define Leaflet for Node module pattern loaders, including Browserify 18 | if (typeof module === 'object' && typeof module.exports === 'object') { 19 | module.exports = L; 20 | 21 | // define Leaflet as an AMD module 22 | } else if (typeof define === 'function' && define.amd) { 23 | define(L); 24 | } 25 | 26 | // define Leaflet as a global L variable, saving the original L to restore later if needed 27 | if (typeof window !== 'undefined') { 28 | expose(); 29 | } 30 | -------------------------------------------------------------------------------- /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/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/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 | 7 | R: 6378137, 8 | 9 | project: function (latlng) { 10 | var d = Math.PI / 180, 11 | max = 1 - 1E-15, 12 | sin = Math.max(Math.min(Math.sin(latlng.lat * d), max), -max); 13 | 14 | return new L.Point( 15 | this.R * latlng.lng * d, 16 | this.R * Math.log((1 + sin) / (1 - sin)) / 2); 17 | }, 18 | 19 | unproject: function (point) { 20 | var d = 180 / Math.PI; 21 | 22 | return new L.LatLng( 23 | (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, 24 | point.x * d / this.R); 25 | }, 26 | 27 | bounds: (function () { 28 | var d = 6378137 * Math.PI; 29 | return L.bounds([-d, -d], [d, d]); 30 | })() 31 | }; 32 | -------------------------------------------------------------------------------- /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 | oldZoom = map.getZoom(), 21 | zoom = e.originalEvent.shiftKey ? Math.ceil(oldZoom) - 1 : Math.floor(oldZoom) + 1; 22 | 23 | if (map.options.doubleClickZoom === 'center') { 24 | map.setZoom(zoom); 25 | } else { 26 | map.setZoomAround(e.containerPoint, zoom); 27 | } 28 | } 29 | }); 30 | 31 | L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom); 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "camelcase": 2, 4 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 5 | "space-after-function-name": 2, 6 | "space-in-parens": 2, 7 | "space-in-brackets": 2, 8 | "space-before-blocks": 2, 9 | "space-after-keywords": 2, 10 | "no-lonely-if": 2, 11 | "comma-style": 2, 12 | "no-unused-vars": 0, 13 | "quotes": 0, 14 | "no-underscore-dangle": 0, 15 | "no-constant-condition": 0, 16 | "no-multi-spaces": 0, 17 | "strict": 0, 18 | "key-spacing": 0, 19 | "no-shadow": 0, 20 | "no-irregular-whitespace": 0 21 | }, 22 | "globals": { 23 | "L": true, 24 | "module": false, 25 | "define": false, 26 | "expect": false, 27 | "it": false, 28 | "describe": false, 29 | "sinon": false, 30 | "happen": false, 31 | "beforeEach": false, 32 | "afterEach": false 33 | }, 34 | "env": { 35 | "browser": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | div.innerHTML = options.html !== false ? options.html : ''; 24 | 25 | if (options.bgPos) { 26 | div.style.backgroundPosition = (-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px'; 27 | } 28 | this._setIconStyles(div, 'icon'); 29 | 30 | return div; 31 | }, 32 | 33 | createShadow: function () { 34 | return null; 35 | } 36 | }); 37 | 38 | L.divIcon = function (options) { 39 | return new L.DivIcon(options); 40 | }; 41 | -------------------------------------------------------------------------------- /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 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /debug/tests/reuse_popups.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /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 | //check clip without rounding 14 | var clipped = L.PolyUtil.clipPolygon(points, bounds); 15 | 16 | for (var i = 0, len = clipped.length; i < len; i++) { 17 | delete clipped[i]._code; 18 | } 19 | 20 | expect(clipped).to.eql([ 21 | new L.Point(7.5, 10), 22 | new L.Point(5, 5), 23 | new L.Point(10, 7.5), 24 | new L.Point(10, 10) 25 | ]); 26 | 27 | //check clip with rounding 28 | var clippedRounded = L.PolyUtil.clipPolygon(points, bounds, true); 29 | 30 | for (i = 0, len = clippedRounded.length; i < len; i++) { 31 | delete clippedRounded[i]._code; 32 | } 33 | 34 | expect(clippedRounded).to.eql([ 35 | new L.Point(8, 10), 36 | new L.Point(5, 5), 37 | new L.Point(10, 8), 38 | new L.Point(10, 10) 39 | ]); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet", 3 | "version": "0.8.0-dev", 4 | "description": "JavaScript library for mobile-friendly interactive maps", 5 | "devDependencies": { 6 | "copyfiles": "0.1.0", 7 | "eslint": "^0.13.0", 8 | "happen": "~0.1.3", 9 | "jake": "~8.0.10", 10 | "karma": "~0.12.31", 11 | "karma-chrome-launcher": "^0.1.7", 12 | "karma-coverage": "~0.2.7", 13 | "karma-firefox-launcher": "~0.1.4", 14 | "karma-mocha": "~0.1.10", 15 | "karma-phantomjs-launcher": "^0.1.4", 16 | "karma-safari-launcher": "~0.1.1", 17 | "mocha": "~2.1.0", 18 | "tin": "^0.5.0", 19 | "uglify-js": "~2.4.16" 20 | }, 21 | "main": "dist/leaflet-src.js", 22 | "style": "dist/leaflet.css", 23 | "scripts": { 24 | "test": "jake test", 25 | "prepublish": "jake build", 26 | "publish": "./build/publish.sh" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git://github.com/Leaflet/Leaflet.git" 31 | }, 32 | "keywords": [ 33 | "gis", 34 | "map" 35 | ], 36 | "licenses": [ 37 | { 38 | "type": "BSD-2-Clause", 39 | "url": "https://github.com/Leaflet/Leaflet/blob/master/LICENSE" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /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 | shadowSize: [41, 41] 12 | }, 13 | 14 | _getIconUrl: function (name) { 15 | var key = name + 'Url'; 16 | 17 | if (this.options[key]) { 18 | return this.options[key]; 19 | } 20 | 21 | var path = L.Icon.Default.imagePath; 22 | 23 | if (!path) { 24 | throw new Error('Couldn\'t autodetect L.Icon.Default.imagePath, set it manually.'); 25 | } 26 | 27 | return path + '/marker-' + name + (L.Browser.retina && name === 'icon' ? '-2x' : '') + '.png'; 28 | } 29 | }); 30 | 31 | L.Icon.Default.imagePath = (function () { 32 | var scripts = document.getElementsByTagName('script'), 33 | leafletRe = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/; 34 | 35 | var i, len, src, path; 36 | 37 | for (i = 0, len = scripts.length; i < len; i++) { 38 | src = scripts[i].src; 39 | 40 | if (src.match(leafletRe)) { 41 | path = src.split(leafletRe)[0]; 42 | return (path ? path + '/' : '') + 'images'; 43 | } 44 | } 45 | }()); 46 | -------------------------------------------------------------------------------- /spec/suites/SpecHelper.js: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.map) { 2 | /*eslint no-extend-native:0*/ 3 | Array.prototype.map = function (fun /*, thisp */) { 4 | "use strict"; 5 | 6 | if (this === void 0 || this === null) { 7 | throw new TypeError(); 8 | } 9 | 10 | var t = Object(this); 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/map/grid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /debug/map/zoom-remain-centered.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: node_js 4 | node_js: 5 | - "0.10" 6 | env: 7 | global: 8 | - "ARTIFACTS_S3_BUCKET=leaflet-cdn" 9 | - secure: |- 10 | LnvY/vWpmAIObabLrpu1mWYw1udllVpZJrrhzsn3traL+qU6yaGniQOn6u+l 11 | iWSCuu7kXX3xv1GD7Fc6lTfQCg9F9dukWv9zlc4gFciyRpiUBuluuqtdV51A 12 | 5yqpLkMpX2PMG7vwrOYttVW0uDlUcwGjyHxWZvnBOXCnnHSpnbI= 13 | - secure: |- 14 | EQ4c2c8VklzFZRxKnizI0/VK0anHhlyc1Rv0vqkMj/YPKxmbWNfOlsOCN2gM 15 | p+q8QzCG1Np9D1Kq9K0miYqHgZxgu4D/4Mwy04bh1UfyoUcDfB1tJmEtsKY/ 16 | 8Bl46ZfhxbTG39b6Y315GuU+49QdFMEXhSqx/G7on1xC4aYLXLc= 17 | before_script: > 18 | test ${TRAVIS_BRANCH} = master || 19 | test ${TRAVIS_BRANCH} = stable && 20 | test ${TRAVIS_PULL_REQUEST} = false && 21 | gem install --no-document --version 0.8.9 faraday && 22 | gem install --no-document travis-artifacts || true 23 | after_success: > 24 | test ${TRAVIS_BRANCH} = master || 25 | test ${TRAVIS_BRANCH} = stable && 26 | test ${TRAVIS_PULL_REQUEST} = false && 27 | travis-artifacts upload --path dist --target-path build/${TRAVIS_BRANCH} && 28 | cd dist && zip -x .DS_Store -r leaflet-${TRAVIS_BRANCH}.zip . && 29 | travis-artifacts upload --path leaflet-${TRAVIS_BRANCH}.zip --target-path build || 30 | echo did not upload branch ${TRAVIS_BRANCH}, pr ${TRAVIS_PULL_REQUEST} 31 | -------------------------------------------------------------------------------- /debug/vector/vector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /debug/map/zoomlevels.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /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 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /debug/map/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /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 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /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 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /debug/map/wms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /debug/tests/bringtoback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2015, Vladimir Agafonkin 2 | Copyright (c) 2010-2011, CloudMade 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are 6 | permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of 9 | conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 12 | of conditions and the following disclaimer in the documentation and/or other materials 13 | provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 17 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 18 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 22 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /debug/map/scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /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 | R: 6378137, 8 | R_MINOR: 6356752.314245179, 9 | 10 | bounds: L.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), 11 | 12 | project: function (latlng) { 13 | var d = Math.PI / 180, 14 | r = this.R, 15 | y = latlng.lat * d, 16 | tmp = this.R_MINOR / r, 17 | e = Math.sqrt(1 - tmp * tmp), 18 | con = e * Math.sin(y); 19 | 20 | var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2); 21 | y = -r * Math.log(Math.max(ts, 1E-10)); 22 | 23 | return new L.Point(latlng.lng * d * r, y); 24 | }, 25 | 26 | unproject: function (point) { 27 | var d = 180 / Math.PI, 28 | r = this.R, 29 | tmp = this.R_MINOR / r, 30 | e = Math.sqrt(1 - tmp * tmp), 31 | ts = Math.exp(-point.y / r), 32 | phi = Math.PI / 2 - 2 * Math.atan(ts); 33 | 34 | for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) { 35 | con = e * Math.sin(phi); 36 | con = Math.pow((1 - con) / (1 + con), e / 2); 37 | dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi; 38 | phi += dphi; 39 | } 40 | 41 | return new L.LatLng(phi * d, point.x * d / r); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /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/layer_remove_add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /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 | 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/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 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/geometry/PolyUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.PolyUtil contains utility functions for polygons (clipping, etc.). 3 | */ 4 | 5 | L.PolyUtil = {}; 6 | 7 | /* 8 | * Sutherland-Hodgeman polygon clipping algorithm. 9 | * Used to avoid rendering parts of a polygon that are not currently visible. 10 | */ 11 | L.PolyUtil.clipPolygon = function (points, bounds, round) { 12 | var clippedPoints, 13 | edges = [1, 4, 2, 8], 14 | i, j, k, 15 | a, b, 16 | len, edge, p, 17 | lu = L.LineUtil; 18 | 19 | for (i = 0, len = points.length; i < len; i++) { 20 | points[i]._code = lu._getBitCode(points[i], bounds); 21 | } 22 | 23 | // for each edge (left, bottom, right, top) 24 | for (k = 0; k < 4; k++) { 25 | edge = edges[k]; 26 | clippedPoints = []; 27 | 28 | for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { 29 | a = points[i]; 30 | b = points[j]; 31 | 32 | // if a is inside the clip window 33 | if (!(a._code & edge)) { 34 | // if b is outside the clip window (a->b goes out of screen) 35 | if (b._code & edge) { 36 | p = lu._getEdgeIntersection(b, a, edge, bounds, round); 37 | p._code = lu._getBitCode(p, bounds); 38 | clippedPoints.push(p); 39 | } 40 | clippedPoints.push(a); 41 | 42 | // else if b is inside the clip window (a->b enters the screen) 43 | } else if (!(b._code & edge)) { 44 | p = lu._getEdgeIntersection(b, a, edge, bounds, round); 45 | p._code = lu._getBitCode(p, bounds); 46 | clippedPoints.push(p); 47 | } 48 | } 49 | points = clippedPoints; 50 | } 51 | 52 | return points; 53 | }; 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /debug/map/controls.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/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 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/suites/control/ControlSpec.js: -------------------------------------------------------------------------------- 1 | describe("Control", function () { 2 | var map; 3 | 4 | beforeEach(function () { 5 | map = L.map(document.createElement('div')); 6 | }); 7 | 8 | function onAdd() { 9 | return L.DomUtil.create('div', 'leaflet-test-control'); 10 | } 11 | 12 | describe("#addTo", function () { 13 | it("adds the container to the map", function () { 14 | var control = new L.Control(); 15 | control.onAdd = onAdd; 16 | control.addTo(map); 17 | expect(map.getContainer().querySelector('.leaflet-test-control')).to.equal(control.getContainer()); 18 | }); 19 | 20 | it("removes the control from any existing map", function () { 21 | var control = new L.Control(); 22 | control.onAdd = onAdd; 23 | control.addTo(map); 24 | control.addTo(map); 25 | expect(map.getContainer().querySelectorAll('.leaflet-test-control').length).to.equal(1); 26 | expect(map.getContainer().querySelector('.leaflet-test-control')).to.equal(control.getContainer()); 27 | }); 28 | }); 29 | 30 | describe("#remove", function () { 31 | it("removes the container from the map", function () { 32 | var control = new L.Control(); 33 | control.onAdd = onAdd; 34 | control.addTo(map).remove(); 35 | expect(map.getContainer().querySelector('.leaflet-test-control')).to.equal(null); 36 | }); 37 | 38 | it("calls onRemove if defined", function () { 39 | var control = new L.Control(); 40 | control.onAdd = onAdd; 41 | control.onRemove = sinon.spy(); 42 | control.addTo(map).remove(); 43 | expect(control.onRemove.called).to.be(true); 44 | }); 45 | 46 | it("is a no-op if the control has not been added", function () { 47 | var control = new L.Control(); 48 | expect(control.remove()).to.equal(control); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/dom/PosAnimation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.PosAnimation powers Leaflet pan animations internally. 3 | */ 4 | 5 | L.PosAnimation = L.Evented.extend({ 6 | 7 | run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number]) 8 | this.stop(); 9 | 10 | this._el = el; 11 | this._inProgress = true; 12 | this._duration = duration || 0.25; 13 | this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2); 14 | 15 | this._startPos = L.DomUtil.getPosition(el); 16 | this._offset = newPos.subtract(this._startPos); 17 | this._startTime = +new Date(); 18 | 19 | this.fire('start'); 20 | 21 | this._animate(); 22 | }, 23 | 24 | stop: function () { 25 | if (!this._inProgress) { return; } 26 | 27 | this._step(true); 28 | this._complete(); 29 | }, 30 | 31 | _animate: function () { 32 | // animation loop 33 | this._animId = L.Util.requestAnimFrame(this._animate, this); 34 | this._step(); 35 | }, 36 | 37 | _step: function (round) { 38 | var elapsed = (+new Date()) - this._startTime, 39 | duration = this._duration * 1000; 40 | 41 | if (elapsed < duration) { 42 | this._runFrame(this._easeOut(elapsed / duration), round); 43 | } else { 44 | this._runFrame(1); 45 | this._complete(); 46 | } 47 | }, 48 | 49 | _runFrame: function (progress, round) { 50 | var pos = this._startPos.add(this._offset.multiplyBy(progress)); 51 | if (round) { 52 | pos._round(); 53 | } 54 | L.DomUtil.setPosition(this._el, pos); 55 | 56 | this.fire('step'); 57 | }, 58 | 59 | _complete: function () { 60 | L.Util.cancelAnimFrame(this._animId); 61 | 62 | this._inProgress = false; 63 | this.fire('end'); 64 | }, 65 | 66 | _easeOut: function (t) { 67 | return 1 - Math.pow(1 - t, this._easeOutPower); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 13 | if (!this._draggable) { 14 | this._draggable = new L.Draggable(icon, icon); 15 | } 16 | 17 | this._draggable.on({ 18 | dragstart: this._onDragStart, 19 | drag: this._onDrag, 20 | dragend: this._onDragEnd 21 | }, this).enable(); 22 | 23 | L.DomUtil.addClass(icon, 'leaflet-marker-draggable'); 24 | }, 25 | 26 | removeHooks: function () { 27 | this._draggable.off({ 28 | dragstart: this._onDragStart, 29 | drag: this._onDrag, 30 | dragend: this._onDragEnd 31 | }, this).disable(); 32 | 33 | if (this._marker._icon) { 34 | L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable'); 35 | } 36 | }, 37 | 38 | moved: function () { 39 | return this._draggable && this._draggable._moved; 40 | }, 41 | 42 | _onDragStart: function () { 43 | this._marker 44 | .closePopup() 45 | .fire('movestart') 46 | .fire('dragstart'); 47 | }, 48 | 49 | _onDrag: function () { 50 | var marker = this._marker, 51 | shadow = marker._shadow, 52 | iconPos = L.DomUtil.getPosition(marker._icon), 53 | latlng = marker._map.layerPointToLatLng(iconPos); 54 | 55 | // update shadow position 56 | if (shadow) { 57 | L.DomUtil.setPosition(shadow, iconPos); 58 | } 59 | 60 | marker._latlng = latlng; 61 | 62 | marker 63 | .fire('move', {latlng: latlng}) 64 | .fire('drag'); 65 | }, 66 | 67 | _onDragEnd: function (e) { 68 | this._marker 69 | .fire('moveend') 70 | .fire('dragend', e); 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /src/layer/vector/CircleMarker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CircleMarker is a circle overlay with a permanent pixel radius. 3 | */ 4 | 5 | L.CircleMarker = L.Path.extend({ 6 | 7 | options: { 8 | fill: true, 9 | radius: 10 10 | }, 11 | 12 | initialize: function (latlng, options) { 13 | L.setOptions(this, options); 14 | this._latlng = L.latLng(latlng); 15 | this._radius = this.options.radius; 16 | }, 17 | 18 | setLatLng: function (latlng) { 19 | this._latlng = L.latLng(latlng); 20 | this.redraw(); 21 | return this.fire('move', {latlng: this._latlng}); 22 | }, 23 | 24 | getLatLng: function () { 25 | return this._latlng; 26 | }, 27 | 28 | setRadius: function (radius) { 29 | this.options.radius = this._radius = radius; 30 | return this.redraw(); 31 | }, 32 | 33 | getRadius: function () { 34 | return this._radius; 35 | }, 36 | 37 | setStyle : function (options) { 38 | var radius = options && options.radius || this._radius; 39 | L.Path.prototype.setStyle.call(this, options); 40 | this.setRadius(radius); 41 | return this; 42 | }, 43 | 44 | _project: function () { 45 | this._point = this._map.latLngToLayerPoint(this._latlng); 46 | this._updateBounds(); 47 | }, 48 | 49 | _updateBounds: function () { 50 | var r = this._radius, 51 | r2 = this._radiusY || r, 52 | w = this._clickTolerance(), 53 | p = [r + w, r2 + w]; 54 | this._pxBounds = new L.Bounds(this._point.subtract(p), this._point.add(p)); 55 | }, 56 | 57 | _update: function () { 58 | if (this._map) { 59 | this._updatePath(); 60 | } 61 | }, 62 | 63 | _updatePath: function () { 64 | this._renderer._updateCircle(this); 65 | }, 66 | 67 | _empty: function () { 68 | return this._radius && !this._renderer._bounds.intersects(this._pxBounds); 69 | } 70 | }); 71 | 72 | L.circleMarker = function (latlng, options) { 73 | return new L.CircleMarker(latlng, options); 74 | }; 75 | -------------------------------------------------------------------------------- /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 | version = require('./src/Leaflet.js').version; 17 | 18 | function hint(msg, args) { 19 | return function () { 20 | console.log(msg); 21 | jake.exec('node node_modules/eslint/bin/eslint.js ' + args, 22 | {printStdout: true}, function () { 23 | console.log('\tCheck passed.\n'); 24 | complete(); 25 | }); 26 | }; 27 | } 28 | 29 | desc('Check Leaflet source for errors with ESLint'); 30 | task('lint', {async: true}, hint('Checking for JS errors...', 'src --config build/eslintrc.json')); 31 | 32 | desc('Check Leaflet specs source for errors with ESLint'); 33 | task('lintspec', {async: true}, hint('Checking for specs JS errors...', 'spec/suites --config spec/eslintrc.json')); 34 | 35 | desc('Combine and compress Leaflet source files'); 36 | task('build', {async: true}, function (compsBase32, buildName) { 37 | var v; 38 | 39 | jake.exec('git log -1 --pretty=format:"%h"', {breakOnError: false}, function () { 40 | build.build(complete, v, compsBase32, buildName); 41 | 42 | }).on('stdout', function (data) { 43 | v = version + ' (' + data.toString() + ')'; 44 | }).on('error', function () { 45 | v = version; 46 | }); 47 | }); 48 | 49 | desc('Run PhantomJS tests'); 50 | task('test', ['lint', 'lintspec'], {async: true}, function () { 51 | build.test(complete); 52 | }); 53 | 54 | task('default', ['test', 'build']); 55 | 56 | jake.addListener('complete', function () { 57 | process.exit(); 58 | }); 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/map/anim/Map.FlyTo.js: -------------------------------------------------------------------------------- 1 | 2 | L.Map.include({ 3 | flyTo: function (targetCenter, targetZoom) { 4 | 5 | this.stop(); 6 | 7 | var from = this.project(this.getCenter()), 8 | to = this.project(targetCenter), 9 | size = this.getSize(), 10 | startZoom = this._zoom; 11 | 12 | targetCenter = L.latLng(targetCenter); 13 | targetZoom = targetZoom === undefined ? startZoom : targetZoom; 14 | 15 | var w0 = Math.max(size.x, size.y), 16 | w1 = w0 * this.getZoomScale(startZoom, targetZoom), 17 | u1 = to.distanceTo(from), 18 | rho = 1.42, 19 | rho2 = rho * rho; 20 | 21 | function r(i) { 22 | var b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1); 23 | return Math.log(Math.sqrt(b * b + 1) - b); 24 | } 25 | 26 | function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } 27 | function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } 28 | function tanh(n) { return sinh(n) / cosh(n); } 29 | 30 | var r0 = r(0); 31 | 32 | function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); } 33 | function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; } 34 | 35 | function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); } 36 | 37 | var start = Date.now(), 38 | S = (r(1) - r0) / rho, 39 | duration = 1000 * S * 0.8; 40 | 41 | function frame() { 42 | var t = (Date.now() - start) / duration, 43 | s = easeOut(t) * S; 44 | 45 | if (t <= 1) { 46 | this._flyToFrame = L.Util.requestAnimFrame(frame, this); 47 | 48 | this._resetView( 49 | this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom), 50 | this.getScaleZoom(w0 / w(s), startZoom), true, true); 51 | 52 | } else { 53 | this._resetView(targetCenter, targetZoom, true, true); 54 | } 55 | } 56 | 57 | this.fire('zoomstart'); 58 | frame.call(this); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /src/layer/Layer.Popup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Adds popup-related methods to all layers. 3 | */ 4 | 5 | L.Layer.include({ 6 | 7 | bindPopup: function (content, options) { 8 | 9 | if (content instanceof L.Popup) { 10 | this._popup = content; 11 | content._source = this; 12 | } else { 13 | if (!this._popup || options) { 14 | this._popup = new L.Popup(options, this); 15 | } 16 | this._popup.setContent(content); 17 | } 18 | 19 | if (!this._popupHandlersAdded) { 20 | this.on({ 21 | click: this._openPopup, 22 | remove: this.closePopup, 23 | move: this._movePopup 24 | }); 25 | this._popupHandlersAdded = true; 26 | } 27 | 28 | return this; 29 | }, 30 | 31 | unbindPopup: function () { 32 | if (this._popup) { 33 | this.off({ 34 | click: this._openPopup, 35 | remove: this.closePopup, 36 | move: this._movePopup 37 | }); 38 | this._popupHandlersAdded = false; 39 | this._popup = null; 40 | } 41 | return this; 42 | }, 43 | 44 | openPopup: function (latlng) { 45 | if (this._popup && this._map) { 46 | this._map.openPopup(this._popup, latlng || this._latlng || this.getCenter()); 47 | } 48 | return this; 49 | }, 50 | 51 | closePopup: function () { 52 | if (this._popup) { 53 | this._popup._close(); 54 | } 55 | return this; 56 | }, 57 | 58 | togglePopup: function () { 59 | if (this._popup) { 60 | if (this._popup._map) { 61 | this.closePopup(); 62 | } else { 63 | this.openPopup(); 64 | } 65 | } 66 | return this; 67 | }, 68 | 69 | setPopupContent: function (content) { 70 | if (this._popup) { 71 | this._popup.setContent(content); 72 | } 73 | return this; 74 | }, 75 | 76 | getPopup: function () { 77 | return this._popup; 78 | }, 79 | 80 | _openPopup: function (e) { 81 | this._map.openPopup(this._popup, e.latlng); 82 | }, 83 | 84 | _movePopup: function (e) { 85 | this._popup.setLatLng(e.latlng); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /src/layer/vector/Path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Path is the base class for all Leaflet vector layers like polygons and circles. 3 | */ 4 | 5 | L.Path = L.Layer.extend({ 6 | 7 | options: { 8 | stroke: true, 9 | color: '#3388ff', 10 | weight: 3, 11 | opacity: 1, 12 | lineCap: 'round', 13 | lineJoin: 'round', 14 | // dashArray: null 15 | // dashOffset: null 16 | 17 | // fill: false 18 | // fillColor: same as color by default 19 | fillOpacity: 0.2, 20 | fillRule: 'evenodd', 21 | 22 | // className: '' 23 | interactive: true 24 | }, 25 | 26 | onAdd: function () { 27 | this._renderer = this._map.getRenderer(this); 28 | this._renderer._initPath(this); 29 | 30 | // defined in children classes 31 | this._project(); 32 | this._update(); 33 | 34 | this._renderer._addPath(this); 35 | }, 36 | 37 | onRemove: function () { 38 | this._renderer._removePath(this); 39 | }, 40 | 41 | getEvents: function () { 42 | return { 43 | viewreset: this._project, 44 | moveend: this._update 45 | }; 46 | }, 47 | 48 | redraw: function () { 49 | if (this._map) { 50 | this._renderer._updatePath(this); 51 | } 52 | return this; 53 | }, 54 | 55 | setStyle: function (style) { 56 | L.setOptions(this, style); 57 | if (this._renderer) { 58 | this._renderer._updateStyle(this); 59 | } 60 | return this; 61 | }, 62 | 63 | bringToFront: function () { 64 | if (this._renderer) { 65 | this._renderer._bringToFront(this); 66 | } 67 | return this; 68 | }, 69 | 70 | bringToBack: function () { 71 | if (this._renderer) { 72 | this._renderer._bringToBack(this); 73 | } 74 | return this; 75 | }, 76 | 77 | _fireMouseEvent: function (e, type) { 78 | this._map._fireMouseEvent(this, e, type, true); 79 | }, 80 | 81 | _clickTolerance: function () { 82 | // used when doing hit detection for Canvas layers 83 | return (this.options.stroke ? this.options.weight / 2 : 0) + (L.Browser.touch ? 10 : 0); 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /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('#setPosition', noSpecs); 57 | 58 | // describe('#getStyle', noSpecs); 59 | }); 60 | -------------------------------------------------------------------------------- /spec/suites/layer/LayerGroupSpec.js: -------------------------------------------------------------------------------- 1 | describe('LayerGroup', function () { 2 | describe("#hasLayer", function () { 3 | it("returns false when passed undefined, null, or false", function () { 4 | var lg = L.layerGroup(); 5 | expect(lg.hasLayer(undefined)).to.equal(false); 6 | expect(lg.hasLayer(null)).to.equal(false); 7 | expect(lg.hasLayer(false)).to.equal(false); 8 | }); 9 | }); 10 | 11 | describe("#addLayer", function () { 12 | it('adds a layer', function () { 13 | var lg = L.layerGroup(), 14 | marker = L.marker([0, 0]); 15 | 16 | expect(lg.addLayer(marker)).to.eql(lg); 17 | 18 | expect(lg.hasLayer(marker)).to.be(true); 19 | }); 20 | }); 21 | 22 | describe("#removeLayer", function () { 23 | it('removes a layer', function () { 24 | var lg = L.layerGroup(), 25 | marker = L.marker([0, 0]); 26 | 27 | lg.addLayer(marker); 28 | expect(lg.removeLayer(marker)).to.eql(lg); 29 | 30 | expect(lg.hasLayer(marker)).to.be(false); 31 | }); 32 | }); 33 | 34 | describe("#clearLayers", function () { 35 | it('removes all layers', function () { 36 | var lg = L.layerGroup(), 37 | marker = L.marker([0, 0]); 38 | 39 | lg.addLayer(marker); 40 | expect(lg.clearLayers()).to.eql(lg); 41 | 42 | expect(lg.hasLayer(marker)).to.be(false); 43 | }); 44 | }); 45 | 46 | describe("#getLayers", function () { 47 | it('gets all layers', function () { 48 | var lg = L.layerGroup(), 49 | marker = L.marker([0, 0]); 50 | 51 | lg.addLayer(marker); 52 | 53 | expect(lg.getLayers()).to.eql([marker]); 54 | }); 55 | }); 56 | 57 | describe("#eachLayer", function () { 58 | it('iterates over all layers', function () { 59 | var lg = L.layerGroup(), 60 | marker = L.marker([0, 0]), 61 | ctx = {foo: 'bar'}; 62 | 63 | lg.addLayer(marker); 64 | 65 | lg.eachLayer(function (layer) { 66 | expect(layer).to.eql(marker); 67 | expect(this).to.eql(ctx); 68 | }, ctx); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /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 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /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 | wheelDebounceTime: 40 8 | }); 9 | 10 | L.Map.ScrollWheelZoom = L.Handler.extend({ 11 | addHooks: function () { 12 | L.DomEvent.on(this._map._container, { 13 | mousewheel: this._onWheelScroll, 14 | MozMousePixelScroll: L.DomEvent.preventDefault 15 | }, this); 16 | 17 | this._delta = 0; 18 | }, 19 | 20 | removeHooks: function () { 21 | L.DomEvent.off(this._map._container, { 22 | mousewheel: this._onWheelScroll, 23 | MozMousePixelScroll: L.DomEvent.preventDefault 24 | }, this); 25 | }, 26 | 27 | _onWheelScroll: function (e) { 28 | var delta = L.DomEvent.getWheelDelta(e); 29 | var debounce = this._map.options.wheelDebounceTime; 30 | 31 | this._delta += delta; 32 | this._lastMousePos = this._map.mouseEventToContainerPoint(e); 33 | 34 | if (!this._startTime) { 35 | this._startTime = +new Date(); 36 | } 37 | 38 | var left = Math.max(debounce - (+new Date() - this._startTime), 0); 39 | 40 | clearTimeout(this._timer); 41 | this._timer = setTimeout(L.bind(this._performZoom, this), left); 42 | 43 | L.DomEvent.stop(e); 44 | }, 45 | 46 | _performZoom: function () { 47 | var map = this._map, 48 | delta = this._delta, 49 | zoom = map.getZoom(); 50 | 51 | map.stop(); // stop panning and fly animations if any 52 | 53 | delta = delta > 0 ? Math.ceil(delta) : Math.floor(delta); 54 | delta = Math.max(Math.min(delta, 4), -4); 55 | delta = map._limitZoom(zoom + delta) - zoom; 56 | 57 | this._delta = 0; 58 | this._startTime = null; 59 | 60 | if (!delta) { return; } 61 | 62 | if (map.options.scrollWheelZoom === 'center') { 63 | map.setZoom(zoom + delta); 64 | } else { 65 | map.setZoomAround(this._lastMousePos, zoom + delta); 66 | } 67 | } 68 | }); 69 | 70 | L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom); 71 | -------------------------------------------------------------------------------- /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/sinon.js", 8 | "spec/expect.js" 9 | ].concat(libSources, [ 10 | "spec/after.js", 11 | "node_modules/happen/happen.js", 12 | "spec/suites/SpecHelper.js", 13 | "spec/suites/**/*.js", 14 | {pattern: "dist/images/*.png", included: false} 15 | ]); 16 | 17 | config.set({ 18 | // base path, that will be used to resolve files and exclude 19 | basePath: '../', 20 | 21 | plugins: [ 22 | 'karma-mocha', 23 | 'karma-coverage', 24 | 'karma-phantomjs-launcher', 25 | 'karma-chrome-launcher', 26 | 'karma-safari-launcher', 27 | 'karma-firefox-launcher'], 28 | 29 | // frameworks to use 30 | frameworks: ['mocha'], 31 | 32 | // list of files / patterns to load in the browser 33 | files: files, 34 | exclude: [], 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 38 | reporters: ['dots'], 39 | 40 | // web server port 41 | port: 9876, 42 | 43 | // level of logging 44 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 45 | logLevel: config.LOG_WARN, 46 | 47 | // enable / disable colors in the output (reporters and logs) 48 | colors: true, 49 | 50 | // enable / disable watching file and executing tests whenever any file changes 51 | autoWatch: false, 52 | 53 | // Start these browsers, currently available: 54 | // - Chrome 55 | // - ChromeCanary 56 | // - Firefox 57 | // - Opera 58 | // - Safari (only Mac) 59 | // - PhantomJS 60 | // - IE (only Windows) 61 | browsers: ['PhantomJS'], 62 | 63 | // If browser does not capture in given timeout [ms], kill it 64 | captureTimeout: 5000, 65 | 66 | // Continuous Integration mode 67 | // if true, it capture browsers, run tests and exit 68 | singleRun: true 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /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/vector/vector2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /debug/tests/click_on_canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /src/layer/vector/Polygon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Polygon implements polygon vector layer (closed polyline with a fill inside). 3 | */ 4 | 5 | L.Polygon = L.Polyline.extend({ 6 | 7 | options: { 8 | fill: true 9 | }, 10 | 11 | getCenter: function () { 12 | var i, j, len, p1, p2, f, area, x, y, 13 | points = this._rings[0]; 14 | 15 | // polygon centroid algorithm; only uses the first ring if there are multiple 16 | 17 | area = x = y = 0; 18 | 19 | for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { 20 | p1 = points[i]; 21 | p2 = points[j]; 22 | 23 | f = p1.y * p2.x - p2.y * p1.x; 24 | x += (p1.x + p2.x) * f; 25 | y += (p1.y + p2.y) * f; 26 | area += f * 3; 27 | } 28 | 29 | return this._map.layerPointToLatLng([x / area, y / area]); 30 | }, 31 | 32 | _convertLatLngs: function (latlngs) { 33 | var result = L.Polyline.prototype._convertLatLngs.call(this, latlngs), 34 | len = result.length; 35 | 36 | // remove last point if it equals first one 37 | if (len >= 2 && result[0] instanceof L.LatLng && result[0].equals(result[len - 1])) { 38 | result.pop(); 39 | } 40 | return result; 41 | }, 42 | 43 | _clipPoints: function () { 44 | if (this.options.noClip) { 45 | this._parts = this._rings; 46 | return; 47 | } 48 | 49 | // polygons need a different clipping algorithm so we redefine that 50 | 51 | var bounds = this._renderer._bounds, 52 | w = this.options.weight, 53 | p = new L.Point(w, w); 54 | 55 | // increase clip padding by stroke width to avoid stroke on clip edges 56 | bounds = new L.Bounds(bounds.min.subtract(p), bounds.max.add(p)); 57 | 58 | this._parts = []; 59 | 60 | for (var i = 0, len = this._rings.length, clipped; i < len; i++) { 61 | clipped = L.PolyUtil.clipPolygon(this._rings[i], bounds, true); 62 | if (clipped.length) { 63 | this._parts.push(clipped); 64 | } 65 | } 66 | }, 67 | 68 | _updatePath: function () { 69 | this._renderer._updatePoly(this, true); 70 | } 71 | }); 72 | 73 | L.polygon = function (latlngs, options) { 74 | return new L.Polygon(latlngs, options); 75 | }; 76 | -------------------------------------------------------------------------------- /src/core/Browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Browser handles different browser and feature detections for internal Leaflet use. 3 | */ 4 | 5 | (function () { 6 | 7 | var ua = navigator.userAgent.toLowerCase(), 8 | doc = document.documentElement, 9 | 10 | ie = 'ActiveXObject' in window, 11 | 12 | webkit = ua.indexOf('webkit') !== -1, 13 | phantomjs = ua.indexOf('phantom') !== -1, 14 | android23 = ua.search('android [23]') !== -1, 15 | chrome = ua.indexOf('chrome') !== -1, 16 | 17 | mobile = typeof orientation !== 'undefined', 18 | msPointer = navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent, 19 | pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer, 20 | 21 | ie3d = ie && ('transition' in doc.style), 22 | webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23, 23 | gecko3d = 'MozPerspective' in doc.style, 24 | opera3d = 'OTransition' in doc.style; 25 | 26 | var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window || 27 | (window.DocumentTouch && document instanceof window.DocumentTouch)); 28 | 29 | L.Browser = { 30 | ie: ie, 31 | ielt9: ie && !document.addEventListener, 32 | webkit: webkit, 33 | gecko: (ua.indexOf('gecko') !== -1) && !webkit && !window.opera && !ie, 34 | android: ua.indexOf('android') !== -1, 35 | android23: android23, 36 | chrome: chrome, 37 | safari: !chrome && ua.indexOf('safari') !== -1, 38 | 39 | ie3d: ie3d, 40 | webkit3d: webkit3d, 41 | gecko3d: gecko3d, 42 | opera3d: opera3d, 43 | any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs, 44 | 45 | mobile: mobile, 46 | mobileWebkit: mobile && webkit, 47 | mobileWebkit3d: mobile && webkit3d, 48 | mobileOpera: mobile && window.opera, 49 | 50 | touch: !!touch, 51 | msPointer: !!msPointer, 52 | pointer: !!pointer, 53 | 54 | retina: (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1 55 | }; 56 | 57 | }()); 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 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.svg?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 | -------------------------------------------------------------------------------- /debug/map/markers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/layer/FeatureGroup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.FeatureGroup extends L.LayerGroup by introducing mouse events and additional methods 3 | * shared between a group of interactive layers (like vectors or markers). 4 | */ 5 | 6 | L.FeatureGroup = L.LayerGroup.extend({ 7 | 8 | addLayer: function (layer) { 9 | if (this.hasLayer(layer)) { 10 | return this; 11 | } 12 | 13 | layer.addEventParent(this); 14 | 15 | L.LayerGroup.prototype.addLayer.call(this, layer); 16 | 17 | if (this._popupContent && layer.bindPopup) { 18 | layer.bindPopup(this._popupContent, this._popupOptions); 19 | } 20 | 21 | return this.fire('layeradd', {layer: layer}); 22 | }, 23 | 24 | removeLayer: function (layer) { 25 | if (!this.hasLayer(layer)) { 26 | return this; 27 | } 28 | if (layer in this._layers) { 29 | layer = this._layers[layer]; 30 | } 31 | 32 | layer.removeEventParent(this); 33 | 34 | L.LayerGroup.prototype.removeLayer.call(this, layer); 35 | 36 | if (this._popupContent) { 37 | this.invoke('unbindPopup'); 38 | } 39 | 40 | return this.fire('layerremove', {layer: layer}); 41 | }, 42 | 43 | bindPopup: function (content, options) { 44 | this._popupContent = content; 45 | this._popupOptions = options; 46 | return this.invoke('bindPopup', content, options); 47 | }, 48 | 49 | openPopup: function (latlng) { 50 | // open popup on the first layer 51 | for (var id in this._layers) { 52 | this._layers[id].openPopup(latlng); 53 | break; 54 | } 55 | return this; 56 | }, 57 | 58 | setStyle: function (style) { 59 | return this.invoke('setStyle', style); 60 | }, 61 | 62 | bringToFront: function () { 63 | return this.invoke('bringToFront'); 64 | }, 65 | 66 | bringToBack: function () { 67 | return this.invoke('bringToBack'); 68 | }, 69 | 70 | getBounds: function () { 71 | var bounds = new L.LatLngBounds(); 72 | 73 | this.eachLayer(function (layer) { 74 | bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng()); 75 | }); 76 | 77 | return bounds; 78 | } 79 | }); 80 | 81 | L.featureGroup = function (layers) { 82 | return new L.FeatureGroup(layers); 83 | }; 84 | -------------------------------------------------------------------------------- /src/geo/crs/CRS.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.CRS is the base object for all defined CRS (Coordinate Reference Systems) in Leaflet. 3 | */ 4 | 5 | L.CRS = { 6 | // converts geo coords to pixel ones 7 | latLngToPoint: function (latlng, zoom) { 8 | var projectedPoint = this.projection.project(latlng), 9 | scale = this.scale(zoom); 10 | 11 | return this.transformation._transform(projectedPoint, scale); 12 | }, 13 | 14 | // converts pixel coords to geo coords 15 | pointToLatLng: function (point, zoom) { 16 | var scale = this.scale(zoom), 17 | untransformedPoint = this.transformation.untransform(point, scale); 18 | 19 | return this.projection.unproject(untransformedPoint); 20 | }, 21 | 22 | // converts geo coords to projection-specific coords (e.g. in meters) 23 | project: function (latlng) { 24 | return this.projection.project(latlng); 25 | }, 26 | 27 | // converts projected coords to geo coords 28 | unproject: function (point) { 29 | return this.projection.unproject(point); 30 | }, 31 | 32 | // defines how the world scales with zoom 33 | scale: function (zoom) { 34 | return 256 * Math.pow(2, zoom); 35 | }, 36 | 37 | // returns the bounds of the world in projected coords if applicable 38 | getProjectedBounds: function (zoom) { 39 | if (this.infinite) { return null; } 40 | 41 | var b = this.projection.bounds, 42 | s = this.scale(zoom), 43 | min = this.transformation.transform(b.min, s), 44 | max = this.transformation.transform(b.max, s); 45 | 46 | return L.bounds(min, max); 47 | }, 48 | 49 | // whether a coordinate axis wraps in a given range (e.g. longitude from -180 to 180); depends on CRS 50 | // wrapLng: [min, max], 51 | // wrapLat: [min, max], 52 | 53 | // if true, the coordinate space will be unbounded (infinite in all directions) 54 | // infinite: false, 55 | 56 | // wraps geo coords in certain ranges if applicable 57 | wrapLatLng: function (latlng) { 58 | var lng = this.wrapLng ? L.Util.wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, 59 | lat = this.wrapLat ? L.Util.wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat; 60 | 61 | return L.latLng(lat, lng); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /spec/suites/control/Control.LayersSpec.js: -------------------------------------------------------------------------------- 1 | describe("Control.Layers", function () { 2 | var map; 3 | 4 | beforeEach(function () { 5 | map = L.map(document.createElement('div')); 6 | }); 7 | 8 | describe("baselayerchange event", function () { 9 | it("is fired on input that changes the base layer", function () { 10 | var baseLayers = {"Layer 1": L.tileLayer(), "Layer 2": L.tileLayer()}, 11 | layers = L.control.layers(baseLayers).addTo(map), 12 | spy = sinon.spy(); 13 | 14 | map.on('baselayerchange', spy) 15 | .whenReady(function () { 16 | happen.click(layers._baseLayersList.getElementsByTagName("input")[0]); 17 | 18 | expect(spy.called).to.be.ok(); 19 | expect(spy.mostRecentCall.args[0].layer).to.be(baseLayers["Layer 1"]); 20 | }); 21 | }); 22 | 23 | it("is not fired on input that doesn't change the base layer", function () { 24 | var overlays = {"Marker 1": L.marker([0, 0]), "Marker 2": L.marker([0, 0])}, 25 | layers = L.control.layers({}, overlays).addTo(map), 26 | spy = sinon.spy(); 27 | 28 | map.on('baselayerchange', spy); 29 | happen.click(layers._overlaysList.getElementsByTagName("input")[0]); 30 | 31 | expect(spy.called).to.not.be.ok(); 32 | }); 33 | }); 34 | 35 | describe("updates", function () { 36 | beforeEach(function () { 37 | map.setView([0, 0], 14); 38 | }); 39 | 40 | it("when an included layer is addded or removed", function () { 41 | var baseLayer = L.tileLayer(), 42 | overlay = L.marker([0, 0]), 43 | layers = L.control.layers({"Base": baseLayer}, {"Overlay": overlay}).addTo(map); 44 | 45 | var spy = sinon.spy(layers, '_update'); 46 | 47 | map.addLayer(overlay); 48 | map.removeLayer(overlay); 49 | 50 | expect(spy.called).to.be.ok(); 51 | expect(spy.callCount).to.eql(2); 52 | }); 53 | 54 | it("not when a non-included layer is added or removed", function () { 55 | var baseLayer = L.tileLayer(), 56 | overlay = L.marker([0, 0]), 57 | layers = L.control.layers({"Base": baseLayer}).addTo(map); 58 | 59 | var spy = sinon.spy(layers, '_update'); 60 | 61 | map.addLayer(overlay); 62 | map.removeLayer(overlay); 63 | 64 | expect(spy.called).to.not.be.ok(); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/layer/vector/Circle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Circle is a circle overlay (with a certain radius in meters). 3 | * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion) 4 | */ 5 | 6 | L.Circle = L.CircleMarker.extend({ 7 | 8 | initialize: function (latlng, radius, options) { 9 | L.setOptions(this, options); 10 | this._latlng = L.latLng(latlng); 11 | this._mRadius = radius; 12 | }, 13 | 14 | setRadius: function (radius) { 15 | this._mRadius = radius; 16 | return this.redraw(); 17 | }, 18 | 19 | getRadius: function () { 20 | return this._mRadius; 21 | }, 22 | 23 | getBounds: function () { 24 | var half = [this._radius, this._radiusY]; 25 | 26 | return new L.LatLngBounds( 27 | this._map.layerPointToLatLng(this._point.subtract(half)), 28 | this._map.layerPointToLatLng(this._point.add(half))); 29 | }, 30 | 31 | setStyle: L.Path.prototype.setStyle, 32 | 33 | _project: function () { 34 | 35 | var lng = this._latlng.lng, 36 | lat = this._latlng.lat, 37 | map = this._map, 38 | crs = map.options.crs; 39 | 40 | if (crs.distance === L.CRS.Earth.distance) { 41 | var d = Math.PI / 180, 42 | latR = (this._mRadius / L.CRS.Earth.R) / d, 43 | top = map.project([lat + latR, lng]), 44 | bottom = map.project([lat - latR, lng]), 45 | p = top.add(bottom).divideBy(2), 46 | lat2 = map.unproject(p).lat, 47 | lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) / 48 | (Math.cos(lat * d) * Math.cos(lat2 * d))) / d; 49 | 50 | this._point = p.subtract(map.getPixelOrigin()); 51 | this._radius = isNaN(lngR) ? 0 : Math.max(Math.round(p.x - map.project([lat2, lng - lngR]).x), 1); 52 | this._radiusY = Math.max(Math.round(p.y - top.y), 1); 53 | 54 | } else { 55 | var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0])); 56 | 57 | this._point = map.latLngToLayerPoint(this._latlng); 58 | this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x; 59 | } 60 | 61 | this._updateBounds(); 62 | } 63 | }); 64 | 65 | L.circle = function (latlng, radius, options) { 66 | return new L.Circle(latlng, radius, options); 67 | }; 68 | -------------------------------------------------------------------------------- /src/dom/DomEvent.DoubleTap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extends the event handling code with double tap support for mobile browsers. 3 | */ 4 | 5 | L.extend(L.DomEvent, { 6 | 7 | _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart', 8 | _touchend: L.Browser.msPointer ? 'MSPointerUp' : L.Browser.pointer ? 'pointerup' : 'touchend', 9 | 10 | // inspired by Zepto touch code by Thomas Fuchs 11 | addDoubleTapListener: function (obj, handler, id) { 12 | var last, touch, 13 | doubleTap = false, 14 | delay = 250; 15 | 16 | function onTouchStart(e) { 17 | var count; 18 | 19 | if (L.Browser.pointer) { 20 | count = L.DomEvent._pointersCount; 21 | } else { 22 | count = e.touches.length; 23 | } 24 | 25 | if (count > 1) { return; } 26 | 27 | var now = Date.now(), 28 | delta = now - (last || now); 29 | 30 | touch = e.touches ? e.touches[0] : e; 31 | doubleTap = (delta > 0 && delta <= delay); 32 | last = now; 33 | } 34 | 35 | function onTouchEnd() { 36 | if (doubleTap) { 37 | if (L.Browser.pointer) { 38 | // work around .type being readonly with MSPointer* events 39 | var newTouch = {}, 40 | prop, i; 41 | 42 | for (i in touch) { 43 | prop = touch[i]; 44 | newTouch[i] = prop && prop.bind ? prop.bind(touch) : prop; 45 | } 46 | touch = newTouch; 47 | } 48 | touch.type = 'dblclick'; 49 | handler(touch); 50 | last = null; 51 | } 52 | } 53 | 54 | var pre = '_leaflet_', 55 | touchstart = this._touchstart, 56 | touchend = this._touchend; 57 | 58 | obj[pre + touchstart + id] = onTouchStart; 59 | obj[pre + touchend + id] = onTouchEnd; 60 | 61 | obj.addEventListener(touchstart, onTouchStart, false); 62 | obj.addEventListener(touchend, onTouchEnd, false); 63 | return this; 64 | }, 65 | 66 | removeDoubleTapListener: function (obj, id) { 67 | var pre = '_leaflet_', 68 | touchend = obj[pre + this._touchend + id]; 69 | 70 | obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false); 71 | obj.removeEventListener(this._touchend, touchend, false); 72 | 73 | return this; 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /debug/map/zoompan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 28 | 29 | 30 | 31 |
32 | 33 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/layer/vector/Renderer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Renderer is a base class for renderer implementations (SVG, Canvas); 3 | * handles renderer container, bounds and zoom animation. 4 | */ 5 | 6 | L.Renderer = L.Layer.extend({ 7 | 8 | options: { 9 | // how much to extend the clip area around the map view (relative to its size) 10 | // e.g. 0.1 would be 10% of map view in each direction; defaults to clip with the map view 11 | padding: 0 12 | }, 13 | 14 | initialize: function (options) { 15 | L.setOptions(this, options); 16 | L.stamp(this); 17 | }, 18 | 19 | onAdd: function () { 20 | if (!this._container) { 21 | this._initContainer(); // defined by renderer implementations 22 | 23 | if (this._zoomAnimated) { 24 | L.DomUtil.addClass(this._container, 'leaflet-zoom-animated'); 25 | } 26 | } 27 | 28 | this.getPane().appendChild(this._container); 29 | this._update(); 30 | }, 31 | 32 | onRemove: function () { 33 | L.DomUtil.remove(this._container); 34 | }, 35 | 36 | getEvents: function () { 37 | var events = { 38 | moveend: this._update 39 | }; 40 | if (this._zoomAnimated) { 41 | events.zoomanim = this._animateZoom; 42 | } 43 | return events; 44 | }, 45 | 46 | _animateZoom: function (e) { 47 | var origin = e.origin.subtract(this._map._getCenterLayerPoint()), 48 | offset = this._bounds.min.add(origin.multiplyBy(1 - e.scale)).add(e.offset).round(); 49 | 50 | L.DomUtil.setTransform(this._container, offset, e.scale); 51 | }, 52 | 53 | _update: function () { 54 | // update pixel bounds of renderer container (for positioning/sizing/clipping later) 55 | var p = this.options.padding, 56 | size = this._map.getSize(), 57 | min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round(); 58 | 59 | this._bounds = new L.Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round()); 60 | } 61 | }); 62 | 63 | 64 | L.Map.include({ 65 | // used by each vector layer to decide which renderer to use 66 | getRenderer: function (layer) { 67 | var renderer = layer.options.renderer || this.options.renderer || this._renderer; 68 | 69 | if (!renderer) { 70 | renderer = this._renderer = (L.SVG && L.svg()) || (L.Canvas && L.canvas()); 71 | } 72 | 73 | if (!this.hasLayer(renderer)) { 74 | this.addLayer(renderer); 75 | } 76 | return renderer; 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /spec/suites/control/Control.AttributionSpec.js: -------------------------------------------------------------------------------- 1 | describe("Control.Attribution", function () { 2 | 3 | var map, control, container; 4 | 5 | beforeEach(function () { 6 | map = L.map(document.createElement('div')); 7 | control = new L.Control.Attribution({ 8 | prefix: 'prefix' 9 | }).addTo(map); 10 | container = control.getContainer(); 11 | }); 12 | 13 | it("contains just prefix if no attributions added", function () { 14 | expect(container.innerHTML).to.eql('prefix'); 15 | }); 16 | 17 | describe('#addAttribution', function () { 18 | it('adds one attribution correctly', function () { 19 | control.addAttribution('foo'); 20 | expect(container.innerHTML).to.eql('prefix | foo'); 21 | }); 22 | 23 | it('adds no duplicate attributions', function () { 24 | control.addAttribution('foo'); 25 | control.addAttribution('foo'); 26 | expect(container.innerHTML).to.eql('prefix | foo'); 27 | }); 28 | 29 | it('adds several attributions listed with comma', function () { 30 | control.addAttribution('foo'); 31 | control.addAttribution('bar'); 32 | expect(container.innerHTML).to.eql('prefix | foo, bar'); 33 | }); 34 | }); 35 | 36 | describe('#removeAttribution', function () { 37 | it('removes attribution correctly', function () { 38 | control.addAttribution('foo'); 39 | control.addAttribution('bar'); 40 | control.removeAttribution('foo'); 41 | expect(container.innerHTML).to.eql('prefix | bar'); 42 | }); 43 | it('does nothing if removing attribution that was not present', function () { 44 | control.addAttribution('foo'); 45 | control.addAttribution('baz'); 46 | control.removeAttribution('bar'); 47 | control.removeAttribution('baz'); 48 | control.removeAttribution('baz'); 49 | control.removeAttribution(''); 50 | expect(container.innerHTML).to.eql('prefix | foo'); 51 | }); 52 | }); 53 | 54 | describe('#setPrefix', function () { 55 | it('changes prefix', function () { 56 | control.setPrefix('bla'); 57 | expect(container.innerHTML).to.eql('bla'); 58 | }); 59 | }); 60 | 61 | describe('control.attribution factory', function () { 62 | it('creates Control.Attribution instance', function () { 63 | var options = {prefix: 'prefix'}; 64 | expect(L.control.attribution(options)).to.eql(new L.Control.Attribution(options)); 65 | }); 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /src/geo/LatLng.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.LatLng represents a geographical point with latitude and longitude coordinates. 3 | */ 4 | 5 | L.LatLng = function (lat, lng, alt) { 6 | if (isNaN(lat) || isNaN(lng)) { 7 | throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); 8 | } 9 | 10 | this.lat = +lat; 11 | this.lng = +lng; 12 | 13 | if (alt !== undefined) { 14 | this.alt = +alt; 15 | } 16 | }; 17 | 18 | L.LatLng.prototype = { 19 | equals: function (obj, maxMargin) { 20 | if (!obj) { return false; } 21 | 22 | obj = L.latLng(obj); 23 | 24 | var margin = Math.max( 25 | Math.abs(this.lat - obj.lat), 26 | Math.abs(this.lng - obj.lng)); 27 | 28 | return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); 29 | }, 30 | 31 | toString: function (precision) { 32 | return 'LatLng(' + 33 | L.Util.formatNum(this.lat, precision) + ', ' + 34 | L.Util.formatNum(this.lng, precision) + ')'; 35 | }, 36 | 37 | distanceTo: function (other) { 38 | return L.CRS.Earth.distance(this, L.latLng(other)); 39 | }, 40 | 41 | wrap: function () { 42 | return L.CRS.Earth.wrapLatLng(this); 43 | }, 44 | 45 | toBounds: function (sizeInMeters) { 46 | var latAccuracy = 180 * sizeInMeters / 40075017, 47 | lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); 48 | 49 | return L.latLngBounds( 50 | [this.lat - latAccuracy, this.lng - lngAccuracy], 51 | [this.lat + latAccuracy, this.lng + lngAccuracy]); 52 | } 53 | }; 54 | 55 | 56 | // constructs LatLng with different signatures 57 | // (LatLng) or ([Number, Number]) or (Number, Number) or (Object) 58 | 59 | L.latLng = function (a, b, c) { 60 | if (a instanceof L.LatLng) { 61 | return a; 62 | } 63 | if (L.Util.isArray(a) && typeof a[0] !== 'object') { 64 | if (a.length === 3) { 65 | return new L.LatLng(a[0], a[1], a[2]); 66 | } 67 | if (a.length === 2) { 68 | return new L.LatLng(a[0], a[1]); 69 | } 70 | return null; 71 | } 72 | if (a === undefined || a === null) { 73 | return a; 74 | } 75 | if (typeof a === 'object' && 'lat' in a) { 76 | return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); 77 | } 78 | if (b === undefined) { 79 | return null; 80 | } 81 | return new L.LatLng(a, b, c); 82 | }; 83 | -------------------------------------------------------------------------------- /src/layer/tile/TileLayer.WMS.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.TileLayer.WMS is used for WMS tile layers. 3 | */ 4 | 5 | L.TileLayer.WMS = L.TileLayer.extend({ 6 | 7 | defaultWmsParams: { 8 | service: 'WMS', 9 | request: 'GetMap', 10 | version: '1.1.1', 11 | layers: '', 12 | styles: '', 13 | format: 'image/jpeg', 14 | transparent: false 15 | }, 16 | 17 | options: { 18 | crs: null, 19 | uppercase: false 20 | }, 21 | 22 | initialize: function (url, options) { 23 | 24 | this._url = url; 25 | 26 | var wmsParams = L.extend({}, this.defaultWmsParams); 27 | 28 | // all keys that are not TileLayer options go to WMS params 29 | for (var i in options) { 30 | if (!(i in this.options)) { 31 | wmsParams[i] = options[i]; 32 | } 33 | } 34 | 35 | options = L.setOptions(this, options); 36 | 37 | wmsParams.width = wmsParams.height = options.tileSize * (options.detectRetina && L.Browser.retina ? 2 : 1); 38 | 39 | this.wmsParams = wmsParams; 40 | }, 41 | 42 | onAdd: function (map) { 43 | 44 | this._crs = this.options.crs || map.options.crs; 45 | this._wmsVersion = parseFloat(this.wmsParams.version); 46 | 47 | var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs'; 48 | this.wmsParams[projectionKey] = this._crs.code; 49 | 50 | L.TileLayer.prototype.onAdd.call(this, map); 51 | }, 52 | 53 | getTileUrl: function (coords) { 54 | 55 | var tileBounds = this._tileCoordsToBounds(coords), 56 | nw = this._crs.project(tileBounds.getNorthWest()), 57 | se = this._crs.project(tileBounds.getSouthEast()), 58 | 59 | bbox = (this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ? 60 | [se.y, nw.x, nw.y, se.x] : 61 | [nw.x, se.y, se.x, nw.y]).join(','), 62 | 63 | url = L.TileLayer.prototype.getTileUrl.call(this, coords); 64 | 65 | return url + 66 | L.Util.getParamString(this.wmsParams, url, this.options.uppercase) + 67 | (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox; 68 | }, 69 | 70 | setParams: function (params, noRedraw) { 71 | 72 | L.extend(this.wmsParams, params); 73 | 74 | if (!noRedraw) { 75 | this.redraw(); 76 | } 77 | 78 | return this; 79 | } 80 | }); 81 | 82 | L.tileLayer.wms = function (url, options) { 83 | return new L.TileLayer.WMS(url, options); 84 | }; 85 | -------------------------------------------------------------------------------- /spec/suites/layer/FeatureGroupSpec.js: -------------------------------------------------------------------------------- 1 | describe('FeatureGroup', function () { 2 | var map; 3 | beforeEach(function () { 4 | map = L.map(document.createElement('div')); 5 | map.setView([0, 0], 1); 6 | }); 7 | describe("#_propagateEvent", function () { 8 | var marker; 9 | beforeEach(function () { 10 | marker = L.marker([0, 0]); 11 | }); 12 | describe("when a Marker is added to multiple FeatureGroups ", function () { 13 | it("e.layer should be the Marker", function () { 14 | var fg1 = L.featureGroup(), 15 | fg2 = L.featureGroup(); 16 | 17 | fg1.addLayer(marker); 18 | fg2.addLayer(marker); 19 | 20 | var wasClicked1, 21 | wasClicked2; 22 | 23 | fg2.on('click', function (e) { 24 | expect(e.layer).to.be(marker); 25 | expect(e.target).to.be(fg2); 26 | wasClicked2 = true; 27 | }); 28 | 29 | fg1.on('click', function (e) { 30 | expect(e.layer).to.be(marker); 31 | expect(e.target).to.be(fg1); 32 | wasClicked1 = true; 33 | }); 34 | 35 | marker.fire('click', {type: 'click'}, true); 36 | 37 | expect(wasClicked1).to.be(true); 38 | expect(wasClicked2).to.be(true); 39 | }); 40 | }); 41 | }); 42 | describe('addLayer', function () { 43 | it('adds the layer', function () { 44 | var fg = L.featureGroup(), 45 | marker = L.marker([0, 0]); 46 | 47 | expect(fg.hasLayer(marker)).to.be(false); 48 | 49 | fg.addLayer(marker); 50 | 51 | expect(fg.hasLayer(marker)).to.be(true); 52 | }); 53 | it('supports non-evented layers', function () { 54 | var fg = L.featureGroup(), 55 | g = L.layerGroup(); 56 | 57 | expect(fg.hasLayer(g)).to.be(false); 58 | 59 | fg.addLayer(g); 60 | 61 | expect(fg.hasLayer(g)).to.be(true); 62 | }); 63 | }); 64 | describe('removeLayer', function () { 65 | it('removes the layer passed to it', function () { 66 | var fg = L.featureGroup(), 67 | marker = L.marker([0, 0]); 68 | 69 | fg.addLayer(marker); 70 | expect(fg.hasLayer(marker)).to.be(true); 71 | 72 | fg.removeLayer(marker); 73 | expect(fg.hasLayer(marker)).to.be(false); 74 | }); 75 | it('removes the layer passed to it by id', function () { 76 | var fg = L.featureGroup(), 77 | marker = L.marker([0, 0]); 78 | 79 | fg.addLayer(marker); 80 | expect(fg.hasLayer(marker)).to.be(true); 81 | 82 | fg.removeLayer(L.stamp(marker)); 83 | expect(fg.hasLayer(marker)).to.be(false); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/layer/marker/Icon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Icon is an image-based icon class that you can use with L.Marker for custom markers. 3 | */ 4 | 5 | L.Icon = L.Class.extend({ 6 | /* 7 | options: { 8 | iconUrl: (String) (required) 9 | iconRetinaUrl: (String) (optional, used for retina devices if detected) 10 | iconSize: (Point) (can be set through CSS) 11 | iconAnchor: (Point) (centered by default, can be set in CSS with negative margins) 12 | popupAnchor: (Point) (if not specified, popup opens in the anchor point) 13 | shadowUrl: (String) (no shadow by default) 14 | shadowRetinaUrl: (String) (optional, used for retina devices if detected) 15 | shadowSize: (Point) 16 | shadowAnchor: (Point) 17 | className: (String) 18 | }, 19 | */ 20 | 21 | initialize: function (options) { 22 | L.setOptions(this, options); 23 | }, 24 | 25 | createIcon: function (oldIcon) { 26 | return this._createIcon('icon', oldIcon); 27 | }, 28 | 29 | createShadow: function (oldIcon) { 30 | return this._createIcon('shadow', oldIcon); 31 | }, 32 | 33 | _createIcon: function (name, oldIcon) { 34 | var src = this._getIconUrl(name); 35 | 36 | if (!src) { 37 | if (name === 'icon') { 38 | throw new Error('iconUrl not set in Icon options (see the docs).'); 39 | } 40 | return null; 41 | } 42 | 43 | var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null); 44 | this._setIconStyles(img, name); 45 | 46 | return img; 47 | }, 48 | 49 | _setIconStyles: function (img, name) { 50 | var options = this.options, 51 | size = L.point(options[name + 'Size']), 52 | anchor = L.point(name === 'shadow' && options.shadowAnchor || options.iconAnchor || 53 | size && size.divideBy(2, true)); 54 | 55 | img.className = 'leaflet-marker-' + name + ' ' + (options.className || ''); 56 | 57 | if (anchor) { 58 | img.style.marginLeft = (-anchor.x) + 'px'; 59 | img.style.marginTop = (-anchor.y) + 'px'; 60 | } 61 | 62 | if (size) { 63 | img.style.width = size.x + 'px'; 64 | img.style.height = size.y + 'px'; 65 | } 66 | }, 67 | 68 | _createImg: function (src, el) { 69 | el = el || document.createElement('img'); 70 | el.src = src; 71 | return el; 72 | }, 73 | 74 | _getIconUrl: function (name) { 75 | return L.Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url']; 76 | } 77 | }); 78 | 79 | L.icon = function (options) { 80 | return new L.Icon(options); 81 | }; 82 | -------------------------------------------------------------------------------- /debug/vector/vector-bounds.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/control/Control.Attribution.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Control.Attribution is used for displaying attribution on the map (added by default). 3 | */ 4 | 5 | L.Control.Attribution = L.Control.extend({ 6 | options: { 7 | position: 'bottomright', 8 | prefix: 'Leaflet' 9 | }, 10 | 11 | initialize: function (options) { 12 | L.setOptions(this, options); 13 | 14 | this._attributions = {}; 15 | }, 16 | 17 | onAdd: function (map) { 18 | this._container = L.DomUtil.create('div', 'leaflet-control-attribution'); 19 | if (L.DomEvent) { 20 | L.DomEvent.disableClickPropagation(this._container); 21 | } 22 | 23 | // TODO ugly, refactor 24 | for (var i in map._layers) { 25 | if (map._layers[i].getAttribution) { 26 | this.addAttribution(map._layers[i].getAttribution()); 27 | } 28 | } 29 | 30 | this._update(); 31 | 32 | return this._container; 33 | }, 34 | 35 | setPrefix: function (prefix) { 36 | this.options.prefix = prefix; 37 | this._update(); 38 | return this; 39 | }, 40 | 41 | addAttribution: function (text) { 42 | if (!text) { return this; } 43 | 44 | if (!this._attributions[text]) { 45 | this._attributions[text] = 0; 46 | } 47 | this._attributions[text]++; 48 | 49 | this._update(); 50 | 51 | return this; 52 | }, 53 | 54 | removeAttribution: function (text) { 55 | if (!text) { return this; } 56 | 57 | if (this._attributions[text]) { 58 | this._attributions[text]--; 59 | this._update(); 60 | } 61 | 62 | return this; 63 | }, 64 | 65 | _update: function () { 66 | if (!this._map) { return; } 67 | 68 | var attribs = []; 69 | 70 | for (var i in this._attributions) { 71 | if (this._attributions[i]) { 72 | attribs.push(i); 73 | } 74 | } 75 | 76 | var prefixAndAttribs = []; 77 | 78 | if (this.options.prefix) { 79 | prefixAndAttribs.push(this.options.prefix); 80 | } 81 | if (attribs.length) { 82 | prefixAndAttribs.push(attribs.join(', ')); 83 | } 84 | 85 | this._container.innerHTML = prefixAndAttribs.join(' | '); 86 | } 87 | }); 88 | 89 | L.Map.mergeOptions({ 90 | attributionControl: true 91 | }); 92 | 93 | L.Map.addInitHook(function () { 94 | if (this.options.attributionControl) { 95 | this.attributionControl = (new L.Control.Attribution()).addTo(this); 96 | } 97 | }); 98 | 99 | L.control.attribution = function (options) { 100 | return new L.Control.Attribution(options); 101 | }; 102 | -------------------------------------------------------------------------------- /src/map/ext/Map.Geolocation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Provides L.Map with convenient shortcuts for using browser geolocation features. 3 | */ 4 | 5 | L.Map.include({ 6 | _defaultLocateOptions: { 7 | timeout: 10000, 8 | watch: false 9 | // setView: false 10 | // maxZoom: 11 | // maximumAge: 0 12 | // enableHighAccuracy: false 13 | }, 14 | 15 | locate: function (/*Object*/ options) { 16 | 17 | options = this._locateOptions = L.extend({}, this._defaultLocateOptions, options); 18 | 19 | if (!navigator.geolocation) { 20 | this._handleGeolocationError({ 21 | code: 0, 22 | message: 'Geolocation not supported.' 23 | }); 24 | return this; 25 | } 26 | 27 | var onResponse = L.bind(this._handleGeolocationResponse, this), 28 | onError = L.bind(this._handleGeolocationError, this); 29 | 30 | if (options.watch) { 31 | this._locationWatchId = 32 | navigator.geolocation.watchPosition(onResponse, onError, options); 33 | } else { 34 | navigator.geolocation.getCurrentPosition(onResponse, onError, options); 35 | } 36 | return this; 37 | }, 38 | 39 | stopLocate: function () { 40 | if (navigator.geolocation) { 41 | navigator.geolocation.clearWatch(this._locationWatchId); 42 | } 43 | if (this._locateOptions) { 44 | this._locateOptions.setView = false; 45 | } 46 | return this; 47 | }, 48 | 49 | _handleGeolocationError: function (error) { 50 | var c = error.code, 51 | message = error.message || 52 | (c === 1 ? 'permission denied' : 53 | (c === 2 ? 'position unavailable' : 'timeout')); 54 | 55 | if (this._locateOptions.setView && !this._loaded) { 56 | this.fitWorld(); 57 | } 58 | 59 | this.fire('locationerror', { 60 | code: c, 61 | message: 'Geolocation error: ' + message + '.' 62 | }); 63 | }, 64 | 65 | _handleGeolocationResponse: function (pos) { 66 | var lat = pos.coords.latitude, 67 | lng = pos.coords.longitude, 68 | latlng = new L.LatLng(lat, lng), 69 | bounds = latlng.toBounds(pos.coords.accuracy), 70 | options = this._locateOptions; 71 | 72 | if (options.setView) { 73 | var zoom = this.getBoundsZoom(bounds); 74 | this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); 75 | } 76 | 77 | var data = { 78 | latlng: latlng, 79 | bounds: bounds, 80 | timestamp: pos.timestamp 81 | }; 82 | 83 | for (var i in pos.coords) { 84 | if (typeof pos.coords[i] === 'number') { 85 | data[i] = pos.coords[i]; 86 | } 87 | } 88 | 89 | this.fire('locationfound', data); 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /src/geometry/Bounds.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Bounds represents a rectangular area on the screen in pixel coordinates. 3 | */ 4 | 5 | L.Bounds = function (a, b) { //(Point, Point) or Point[] 6 | if (!a) { return; } 7 | 8 | var points = b ? [a, b] : a; 9 | 10 | for (var i = 0, len = points.length; i < len; i++) { 11 | this.extend(points[i]); 12 | } 13 | }; 14 | 15 | L.Bounds.prototype = { 16 | // extend the bounds to contain the given point 17 | extend: function (point) { // (Point) 18 | point = L.point(point); 19 | 20 | if (!this.min && !this.max) { 21 | this.min = point.clone(); 22 | this.max = point.clone(); 23 | } else { 24 | this.min.x = Math.min(point.x, this.min.x); 25 | this.max.x = Math.max(point.x, this.max.x); 26 | this.min.y = Math.min(point.y, this.min.y); 27 | this.max.y = Math.max(point.y, this.max.y); 28 | } 29 | return this; 30 | }, 31 | 32 | getCenter: function (round) { // (Boolean) -> Point 33 | return new L.Point( 34 | (this.min.x + this.max.x) / 2, 35 | (this.min.y + this.max.y) / 2, round); 36 | }, 37 | 38 | getBottomLeft: function () { // -> Point 39 | return new L.Point(this.min.x, this.max.y); 40 | }, 41 | 42 | getTopRight: function () { // -> Point 43 | return new L.Point(this.max.x, this.min.y); 44 | }, 45 | 46 | getSize: function () { 47 | return this.max.subtract(this.min); 48 | }, 49 | 50 | contains: function (obj) { // (Bounds) or (Point) -> Boolean 51 | var min, max; 52 | 53 | if (typeof obj[0] === 'number' || obj instanceof L.Point) { 54 | obj = L.point(obj); 55 | } else { 56 | obj = L.bounds(obj); 57 | } 58 | 59 | if (obj instanceof L.Bounds) { 60 | min = obj.min; 61 | max = obj.max; 62 | } else { 63 | min = max = obj; 64 | } 65 | 66 | return (min.x >= this.min.x) && 67 | (max.x <= this.max.x) && 68 | (min.y >= this.min.y) && 69 | (max.y <= this.max.y); 70 | }, 71 | 72 | intersects: function (bounds) { // (Bounds) -> Boolean 73 | bounds = L.bounds(bounds); 74 | 75 | var min = this.min, 76 | max = this.max, 77 | min2 = bounds.min, 78 | max2 = bounds.max, 79 | xIntersects = (max2.x >= min.x) && (min2.x <= max.x), 80 | yIntersects = (max2.y >= min.y) && (min2.y <= max.y); 81 | 82 | return xIntersects && yIntersects; 83 | }, 84 | 85 | isValid: function () { 86 | return !!(this.min && this.max); 87 | } 88 | }; 89 | 90 | L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[]) 91 | if (!a || a instanceof L.Bounds) { 92 | return a; 93 | } 94 | return new L.Bounds(a, b); 95 | }; 96 | -------------------------------------------------------------------------------- /spec/suites/geometry/LineUtilSpec.js: -------------------------------------------------------------------------------- 1 | describe('LineUtil', function () { 2 | 3 | describe('#clipSegment', function () { 4 | 5 | var bounds; 6 | 7 | beforeEach(function () { 8 | bounds = L.bounds([5, 0], [15, 10]); 9 | }); 10 | 11 | it('clips a segment by bounds', function () { 12 | var a = new L.Point(0, 0); 13 | var b = new L.Point(15, 15); 14 | 15 | var segment = L.LineUtil.clipSegment(a, b, bounds); 16 | 17 | expect(segment[0]).to.eql(new L.Point(5, 5)); 18 | expect(segment[1]).to.eql(new L.Point(10, 10)); 19 | 20 | var c = new L.Point(5, -5); 21 | var d = new L.Point(20, 10); 22 | 23 | var segment2 = L.LineUtil.clipSegment(c, d, bounds); 24 | 25 | expect(segment2[0]).to.eql(new L.Point(10, 0)); 26 | expect(segment2[1]).to.eql(new L.Point(15, 5)); 27 | }); 28 | 29 | it('uses last bit code and reject segments out of bounds', function () { 30 | var a = new L.Point(15, 15); 31 | var b = new L.Point(25, 20); 32 | var segment = L.LineUtil.clipSegment(a, b, bounds, true); 33 | 34 | expect(segment).to.be(false); 35 | }); 36 | 37 | it('can round numbers in clipped bounds', function () { 38 | var a = new L.Point(4, 5); 39 | var b = new L.Point(8, 6); 40 | 41 | var segment1 = L.LineUtil.clipSegment(a, b, bounds); 42 | 43 | expect(segment1[0]).to.eql(new L.Point(5, 5.25)); 44 | expect(segment1[1]).to.eql(b); 45 | 46 | var segment2 = L.LineUtil.clipSegment(a, b, bounds, false, true); 47 | 48 | expect(segment2[0]).to.eql(new L.Point(5, 5)); 49 | expect(segment2[1]).to.eql(b); 50 | }); 51 | }); 52 | 53 | describe('#pointToSegmentDistance & #closestPointOnSegment', function () { 54 | 55 | var p1 = new L.Point(0, 10); 56 | var p2 = new L.Point(10, 0); 57 | var p = new L.Point(0, 0); 58 | 59 | it('calculates distance from point to segment', function () { 60 | expect(L.LineUtil.pointToSegmentDistance(p, p1, p2)).to.eql(Math.sqrt(200) / 2); 61 | }); 62 | 63 | it('calculates point closest to segment', function () { 64 | expect(L.LineUtil.closestPointOnSegment(p, p1, p2)).to.eql(new L.Point(5, 5)); 65 | }); 66 | }); 67 | 68 | describe('#simplify', function () { 69 | it('simplifies polylines according to tolerance', function () { 70 | var points = [ 71 | new L.Point(0, 0), 72 | new L.Point(0.01, 0), 73 | new L.Point(0.5, 0.01), 74 | new L.Point(0.7, 0), 75 | new L.Point(1, 0), 76 | new L.Point(1.999, 0.999), 77 | new L.Point(2, 1) 78 | ]; 79 | 80 | var simplified = L.LineUtil.simplify(points, 0.1); 81 | 82 | expect(simplified).to.eql([ 83 | new L.Point(0, 0), 84 | new L.Point(1, 0), 85 | new L.Point(2, 1) 86 | ]); 87 | }); 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /src/core/Class.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Class powers the OOP facilities of the library. 3 | * Thanks to John Resig and Dean Edwards for inspiration! 4 | */ 5 | 6 | L.Class = function () {}; 7 | 8 | L.Class.extend = function (props) { 9 | 10 | // extended class with the new prototype 11 | var NewClass = function () { 12 | 13 | // call the constructor 14 | if (this.initialize) { 15 | this.initialize.apply(this, arguments); 16 | } 17 | 18 | // call all constructor hooks 19 | this.callInitHooks(); 20 | }; 21 | 22 | var parentProto = NewClass.__super__ = this.prototype; 23 | 24 | var proto = L.Util.create(parentProto); 25 | proto.constructor = NewClass; 26 | 27 | NewClass.prototype = proto; 28 | 29 | //inherit parent's statics 30 | for (var i in this) { 31 | if (this.hasOwnProperty(i) && i !== 'prototype') { 32 | NewClass[i] = this[i]; 33 | } 34 | } 35 | 36 | // mix static properties into the class 37 | if (props.statics) { 38 | L.extend(NewClass, props.statics); 39 | delete props.statics; 40 | } 41 | 42 | // mix includes into the prototype 43 | if (props.includes) { 44 | L.Util.extend.apply(null, [proto].concat(props.includes)); 45 | delete props.includes; 46 | } 47 | 48 | // merge options 49 | if (proto.options) { 50 | props.options = L.Util.extend(L.Util.create(proto.options), props.options); 51 | } 52 | 53 | // mix given properties into the prototype 54 | L.extend(proto, props); 55 | 56 | proto._initHooks = []; 57 | 58 | // add method for calling all hooks 59 | proto.callInitHooks = function () { 60 | 61 | if (this._initHooksCalled) { return; } 62 | 63 | if (parentProto.callInitHooks) { 64 | parentProto.callInitHooks.call(this); 65 | } 66 | 67 | this._initHooksCalled = true; 68 | 69 | for (var i = 0, len = proto._initHooks.length; i < len; i++) { 70 | proto._initHooks[i].call(this); 71 | } 72 | }; 73 | 74 | return NewClass; 75 | }; 76 | 77 | 78 | // method for adding properties to prototype 79 | L.Class.include = function (props) { 80 | L.extend(this.prototype, props); 81 | }; 82 | 83 | // merge new default options to the Class 84 | L.Class.mergeOptions = function (options) { 85 | L.extend(this.prototype.options, options); 86 | }; 87 | 88 | // add a constructor hook 89 | L.Class.addInitHook = function (fn) { // (Function) || (String, args...) 90 | var args = Array.prototype.slice.call(arguments, 1); 91 | 92 | var init = typeof fn === 'function' ? fn : function () { 93 | this[fn].apply(this, args); 94 | }; 95 | 96 | this.prototype._initHooks = this.prototype._initHooks || []; 97 | this.prototype._initHooks.push(init); 98 | }; 99 | -------------------------------------------------------------------------------- /spec/suites/layer/vector/PolygonSpec.js: -------------------------------------------------------------------------------- 1 | describe('Polygon', function () { 2 | 3 | var c = document.createElement('div'); 4 | c.style.width = '400px'; 5 | c.style.height = '400px'; 6 | var map = new L.Map(c); 7 | map.setView(new L.LatLng(55.8, 37.6), 6); 8 | 9 | describe("#initialize", function () { 10 | it("doesn't overwrite the given latlng array", function () { 11 | var originalLatLngs = [ 12 | [1, 2], 13 | [3, 4] 14 | ]; 15 | var sourceLatLngs = originalLatLngs.slice(); 16 | 17 | var polygon = new L.Polygon(sourceLatLngs); 18 | 19 | expect(sourceLatLngs).to.eql(originalLatLngs); 20 | expect(polygon._latlngs).to.not.eql(sourceLatLngs); 21 | }); 22 | 23 | it("can be called with an empty array", function () { 24 | var polygon = new L.Polygon([]); 25 | expect(polygon.getLatLngs()).to.eql([]); 26 | }); 27 | 28 | it("can be initialized with holes", function () { 29 | var originalLatLngs = [ 30 | [ //external rink 31 | [0, 10], [10, 10], [10, 0] 32 | ], [ //hole 33 | [2, 3], [2, 4], [3, 4] 34 | ] 35 | ]; 36 | 37 | var polygon = new L.Polygon(originalLatLngs); 38 | 39 | // getLatLngs() returns both rings 40 | expect(polygon.getLatLngs()).to.eql([ 41 | [L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])], 42 | [L.latLng([2, 3]), L.latLng([2, 4]), L.latLng([3, 4])] 43 | ]); 44 | }); 45 | }); 46 | 47 | describe("#setLatLngs", function () { 48 | it("doesn't overwrite the given latlng array", function () { 49 | var originalLatLngs = [ 50 | [1, 2], 51 | [3, 4] 52 | ]; 53 | var sourceLatLngs = originalLatLngs.slice(); 54 | 55 | var polygon = new L.Polygon(sourceLatLngs); 56 | 57 | polygon.setLatLngs(sourceLatLngs); 58 | 59 | expect(sourceLatLngs).to.eql(originalLatLngs); 60 | }); 61 | 62 | it("can be set external ring and holes", function () { 63 | var latLngs = [ 64 | [ //external rink 65 | [0, 10], [10, 10], [10, 0] 66 | ], [ //hole 67 | [2, 3], [2, 4], [3, 4] 68 | ] 69 | ]; 70 | 71 | var polygon = new L.Polygon([]); 72 | polygon.setLatLngs(latLngs); 73 | 74 | expect(polygon.getLatLngs()).to.eql([ 75 | [L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])], 76 | [L.latLng([2, 3]), L.latLng([2, 4]), L.latLng([3, 4])] 77 | ]); 78 | }); 79 | }); 80 | 81 | describe("#spliceLatLngs", function () { 82 | it("splices the internal latLngs", function () { 83 | var latLngs = [ 84 | [1, 2], 85 | [3, 4], 86 | [5, 6] 87 | ]; 88 | 89 | var polygon = new L.Polygon(latLngs); 90 | 91 | polygon.spliceLatLngs(1, 1, [7, 8]); 92 | 93 | expect(polygon._latlngs).to.eql([L.latLng([1, 2]), L.latLng([7, 8]), L.latLng([5, 6])]); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/layer/LayerGroup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.LayerGroup is a class to combine several layers into one so that 3 | * you can manipulate the group (e.g. add/remove it) as one layer. 4 | */ 5 | 6 | L.LayerGroup = L.Layer.extend({ 7 | 8 | initialize: function (layers) { 9 | this._layers = {}; 10 | 11 | var i, len; 12 | 13 | if (layers) { 14 | for (i = 0, len = layers.length; i < len; i++) { 15 | this.addLayer(layers[i]); 16 | } 17 | } 18 | }, 19 | 20 | addLayer: function (layer) { 21 | var id = this.getLayerId(layer); 22 | 23 | this._layers[id] = layer; 24 | 25 | if (this._map) { 26 | this._map.addLayer(layer); 27 | } 28 | 29 | return this; 30 | }, 31 | 32 | removeLayer: function (layer) { 33 | var id = layer in this._layers ? layer : this.getLayerId(layer); 34 | 35 | if (this._map && this._layers[id]) { 36 | this._map.removeLayer(this._layers[id]); 37 | } 38 | 39 | delete this._layers[id]; 40 | 41 | return this; 42 | }, 43 | 44 | hasLayer: function (layer) { 45 | return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers); 46 | }, 47 | 48 | clearLayers: function () { 49 | for (var i in this._layers) { 50 | this.removeLayer(this._layers[i]); 51 | } 52 | return this; 53 | }, 54 | 55 | invoke: function (methodName) { 56 | var args = Array.prototype.slice.call(arguments, 1), 57 | i, layer; 58 | 59 | for (i in this._layers) { 60 | layer = this._layers[i]; 61 | 62 | if (layer[methodName]) { 63 | layer[methodName].apply(layer, args); 64 | } 65 | } 66 | 67 | return this; 68 | }, 69 | 70 | onAdd: function (map) { 71 | for (var i in this._layers) { 72 | map.addLayer(this._layers[i]); 73 | } 74 | }, 75 | 76 | onRemove: function (map) { 77 | for (var i in this._layers) { 78 | map.removeLayer(this._layers[i]); 79 | } 80 | }, 81 | 82 | eachLayer: function (method, context) { 83 | for (var i in this._layers) { 84 | method.call(context, this._layers[i]); 85 | } 86 | return this; 87 | }, 88 | 89 | getLayer: function (id) { 90 | return this._layers[id]; 91 | }, 92 | 93 | getLayers: function () { 94 | var layers = []; 95 | 96 | for (var i in this._layers) { 97 | layers.push(this._layers[i]); 98 | } 99 | return layers; 100 | }, 101 | 102 | setZIndex: function (zIndex) { 103 | return this.invoke('setZIndex', zIndex); 104 | }, 105 | 106 | getLayerId: function (layer) { 107 | return L.stamp(layer); 108 | } 109 | }); 110 | 111 | L.layerGroup = function (layers) { 112 | return new L.LayerGroup(layers); 113 | }; 114 | -------------------------------------------------------------------------------- /spec/suites/geometry/BoundsSpec.js: -------------------------------------------------------------------------------- 1 | describe('Bounds', function () { 2 | var a, b, c; 3 | 4 | beforeEach(function () { 5 | a = new L.Bounds( 6 | new L.Point(14, 12), 7 | new L.Point(30, 40)); 8 | b = new L.Bounds([ 9 | new L.Point(20, 12), 10 | new L.Point(14, 20), 11 | new L.Point(30, 40) 12 | ]); 13 | c = new L.Bounds(); 14 | }); 15 | 16 | describe('constructor', function () { 17 | it('creates bounds with proper min & max on (Point, Point)', function () { 18 | expect(a.min).to.eql(new L.Point(14, 12)); 19 | expect(a.max).to.eql(new L.Point(30, 40)); 20 | }); 21 | it('creates bounds with proper min & max on (Point[])', function () { 22 | expect(b.min).to.eql(new L.Point(14, 12)); 23 | expect(b.max).to.eql(new L.Point(30, 40)); 24 | }); 25 | }); 26 | 27 | describe('#extend', function () { 28 | it('extends the bounds to contain the given point', function () { 29 | a.extend(new L.Point(50, 20)); 30 | expect(a.min).to.eql(new L.Point(14, 12)); 31 | expect(a.max).to.eql(new L.Point(50, 40)); 32 | 33 | b.extend(new L.Point(25, 50)); 34 | expect(b.min).to.eql(new L.Point(14, 12)); 35 | expect(b.max).to.eql(new L.Point(30, 50)); 36 | }); 37 | }); 38 | 39 | describe('#getCenter', function () { 40 | it('returns the center point', function () { 41 | expect(a.getCenter()).to.eql(new L.Point(22, 26)); 42 | }); 43 | }); 44 | 45 | describe('#contains', function () { 46 | it('contains other bounds or point', function () { 47 | a.extend(new L.Point(50, 10)); 48 | expect(a.contains(b)).to.be.ok(); 49 | expect(b.contains(a)).to.not.be.ok(); 50 | expect(a.contains(new L.Point(24, 25))).to.be.ok(); 51 | expect(a.contains(new L.Point(54, 65))).to.not.be.ok(); 52 | }); 53 | }); 54 | 55 | describe('#isValid', function () { 56 | it('returns true if properly set up', function () { 57 | expect(a.isValid()).to.be.ok(); 58 | }); 59 | it('returns false if is invalid', function () { 60 | expect(c.isValid()).to.not.be.ok(); 61 | }); 62 | it('returns true if extended', function () { 63 | c.extend([0, 0]); 64 | expect(c.isValid()).to.be.ok(); 65 | }); 66 | }); 67 | 68 | describe('#getSize', function () { 69 | it('returns the size of the bounds as point', function () { 70 | expect(a.getSize()).to.eql(new L.Point(16, 28)); 71 | }); 72 | }); 73 | 74 | describe('#intersects', function () { 75 | it('returns true if bounds intersect', function () { 76 | expect(a.intersects(b)).to.be(true); 77 | expect(a.intersects(new L.Bounds(new L.Point(100, 100), new L.Point(120, 120)))).to.eql(false); 78 | }); 79 | }); 80 | 81 | describe('L.bounds factory', function () { 82 | it('creates bounds from array of number arrays', function () { 83 | var bounds = L.bounds([[14, 12], [30, 40]]); 84 | expect(bounds).to.eql(a); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /debug/tests/add_remove_layers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet debug page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 77 | 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /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.remove(); 41 | this._map = map; 42 | 43 | var container = this._container = this.onAdd(map), 44 | pos = this.getPosition(), 45 | corner = map._controlCorners[pos]; 46 | 47 | L.DomUtil.addClass(container, 'leaflet-control'); 48 | 49 | if (pos.indexOf('bottom') !== -1) { 50 | corner.insertBefore(container, corner.firstChild); 51 | } else { 52 | corner.appendChild(container); 53 | } 54 | 55 | return this; 56 | }, 57 | 58 | remove: function () { 59 | if (!this._map) { 60 | return this; 61 | } 62 | 63 | L.DomUtil.remove(this._container); 64 | 65 | if (this.onRemove) { 66 | this.onRemove(this._map); 67 | } 68 | 69 | this._map = null; 70 | 71 | return this; 72 | }, 73 | 74 | _refocusOnMap: function () { 75 | if (this._map) { 76 | this._map.getContainer().focus(); 77 | } 78 | } 79 | }); 80 | 81 | L.control = function (options) { 82 | return new L.Control(options); 83 | }; 84 | 85 | 86 | // adds control-related methods to L.Map 87 | 88 | L.Map.include({ 89 | addControl: function (control) { 90 | control.addTo(this); 91 | return this; 92 | }, 93 | 94 | removeControl: function (control) { 95 | control.remove(); 96 | return this; 97 | }, 98 | 99 | _initControlPos: function () { 100 | var corners = this._controlCorners = {}, 101 | l = 'leaflet-', 102 | container = this._controlContainer = 103 | L.DomUtil.create('div', l + 'control-container', this._container); 104 | 105 | function createCorner(vSide, hSide) { 106 | var className = l + vSide + ' ' + l + hSide; 107 | 108 | corners[vSide + hSide] = L.DomUtil.create('div', className, container); 109 | } 110 | 111 | createCorner('top', 'left'); 112 | createCorner('top', 'right'); 113 | createCorner('bottom', 'left'); 114 | createCorner('bottom', 'right'); 115 | }, 116 | 117 | _clearControlPos: function () { 118 | L.DomUtil.remove(this._controlContainer); 119 | } 120 | }); 121 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /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 | var className = 'leaflet-control-scale', 16 | container = L.DomUtil.create('div', className), 17 | options = this.options; 18 | 19 | this._addScales(options, className + '-line', container); 20 | 21 | map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this); 22 | map.whenReady(this._update, this); 23 | 24 | return container; 25 | }, 26 | 27 | onRemove: function (map) { 28 | map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this); 29 | }, 30 | 31 | _addScales: function (options, className, container) { 32 | if (options.metric) { 33 | this._mScale = L.DomUtil.create('div', className, container); 34 | } 35 | if (options.imperial) { 36 | this._iScale = L.DomUtil.create('div', className, container); 37 | } 38 | }, 39 | 40 | _update: function () { 41 | var map = this._map, 42 | y = map.getSize().y / 2; 43 | 44 | var maxMeters = L.CRS.Earth.distance( 45 | map.containerPointToLatLng([0, y]), 46 | map.containerPointToLatLng([this.options.maxWidth, y])); 47 | 48 | this._updateScales(maxMeters); 49 | }, 50 | 51 | _updateScales: function (maxMeters) { 52 | if (this.options.metric && maxMeters) { 53 | this._updateMetric(maxMeters); 54 | } 55 | if (this.options.imperial && maxMeters) { 56 | this._updateImperial(maxMeters); 57 | } 58 | }, 59 | 60 | _updateMetric: function (maxMeters) { 61 | var meters = this._getRoundNum(maxMeters), 62 | label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; 63 | 64 | this._updateScale(this._mScale, label, meters / maxMeters); 65 | }, 66 | 67 | _updateImperial: function (maxMeters) { 68 | var maxFeet = maxMeters * 3.2808399, 69 | maxMiles, miles, feet; 70 | 71 | if (maxFeet > 5280) { 72 | maxMiles = maxFeet / 5280; 73 | miles = this._getRoundNum(maxMiles); 74 | this._updateScale(this._iScale, miles + ' mi', miles / maxMiles); 75 | 76 | } else { 77 | feet = this._getRoundNum(maxFeet); 78 | this._updateScale(this._iScale, feet + ' ft', feet / maxFeet); 79 | } 80 | }, 81 | 82 | _updateScale: function (scale, text, ratio) { 83 | scale.style.width = Math.round(this.options.maxWidth * ratio) + 'px'; 84 | scale.innerHTML = text; 85 | }, 86 | 87 | _getRoundNum: function (num) { 88 | var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1), 89 | d = num / pow10; 90 | 91 | d = d >= 10 ? 10 : 92 | d >= 5 ? 5 : 93 | d >= 3 ? 3 : 94 | d >= 2 ? 2 : 1; 95 | 96 | return pow10 * d; 97 | } 98 | }); 99 | 100 | L.control.scale = function (options) { 101 | return new L.Control.Scale(options); 102 | }; 103 | -------------------------------------------------------------------------------- /src/map/handler/Map.BoxZoom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Handler.ShiftDragZoom is used to add shift-drag zoom interaction to the map 3 | * (zoom to a selected bounding box), enabled by default. 4 | */ 5 | 6 | L.Map.mergeOptions({ 7 | boxZoom: true 8 | }); 9 | 10 | L.Map.BoxZoom = L.Handler.extend({ 11 | initialize: function (map) { 12 | this._map = map; 13 | this._container = map._container; 14 | this._pane = map._panes.overlayPane; 15 | }, 16 | 17 | addHooks: function () { 18 | L.DomEvent.on(this._container, 'mousedown', this._onMouseDown, this); 19 | }, 20 | 21 | removeHooks: function () { 22 | L.DomEvent.off(this._container, 'mousedown', this._onMouseDown, this); 23 | }, 24 | 25 | moved: function () { 26 | return this._moved; 27 | }, 28 | 29 | _onMouseDown: function (e) { 30 | if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } 31 | 32 | this._moved = false; 33 | 34 | L.DomUtil.disableTextSelection(); 35 | L.DomUtil.disableImageDrag(); 36 | 37 | this._startPoint = this._map.mouseEventToContainerPoint(e); 38 | 39 | L.DomEvent.on(document, { 40 | contextmenu: L.DomEvent.stop, 41 | mousemove: this._onMouseMove, 42 | mouseup: this._onMouseUp, 43 | keydown: this._onKeyDown 44 | }, this); 45 | }, 46 | 47 | _onMouseMove: function (e) { 48 | if (!this._moved) { 49 | this._moved = true; 50 | 51 | this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._container); 52 | L.DomUtil.addClass(this._container, 'leaflet-crosshair'); 53 | 54 | this._map.fire('boxzoomstart'); 55 | } 56 | 57 | this._point = this._map.mouseEventToContainerPoint(e); 58 | 59 | var bounds = new L.Bounds(this._point, this._startPoint), 60 | size = bounds.getSize(); 61 | 62 | L.DomUtil.setPosition(this._box, bounds.min); 63 | 64 | this._box.style.width = size.x + 'px'; 65 | this._box.style.height = size.y + 'px'; 66 | }, 67 | 68 | _finish: function () { 69 | if (this._moved) { 70 | L.DomUtil.remove(this._box); 71 | L.DomUtil.removeClass(this._container, 'leaflet-crosshair'); 72 | } 73 | 74 | L.DomUtil.enableTextSelection(); 75 | L.DomUtil.enableImageDrag(); 76 | 77 | L.DomEvent.off(document, { 78 | contextmenu: L.DomEvent.stop, 79 | mousemove: this._onMouseMove, 80 | mouseup: this._onMouseUp, 81 | keydown: this._onKeyDown 82 | }, this); 83 | }, 84 | 85 | _onMouseUp: function (e) { 86 | if ((e.which !== 1) && (e.button !== 1)) { return; } 87 | 88 | this._finish(); 89 | 90 | if (!this._moved) { return; } 91 | 92 | var bounds = new L.LatLngBounds( 93 | this._map.containerPointToLatLng(this._startPoint), 94 | this._map.containerPointToLatLng(this._point)); 95 | 96 | this._map 97 | .fitBounds(bounds) 98 | .fire('boxzoomend', {boxZoomBounds: bounds}); 99 | }, 100 | 101 | _onKeyDown: function (e) { 102 | if (e.keyCode === 27) { 103 | this._finish(); 104 | } 105 | } 106 | }); 107 | 108 | L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom); 109 | -------------------------------------------------------------------------------- /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/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 | options = this.options; 18 | 19 | this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle, 20 | zoomName + '-in', container, this._zoomIn); 21 | this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle, 22 | zoomName + '-out', container, this._zoomOut); 23 | 24 | this._updateDisabled(); 25 | map.on('zoomend zoomlevelschange', this._updateDisabled, this); 26 | 27 | return container; 28 | }, 29 | 30 | onRemove: function (map) { 31 | map.off('zoomend zoomlevelschange', this._updateDisabled, this); 32 | }, 33 | 34 | disable: function () { 35 | this._disabled = true; 36 | this._updateDisabled(); 37 | return this; 38 | }, 39 | 40 | enable: function () { 41 | this._disabled = false; 42 | this._updateDisabled(); 43 | return this; 44 | }, 45 | 46 | _zoomIn: function (e) { 47 | if (!this._disabled) { 48 | this._map.zoomIn(e.shiftKey ? 3 : 1); 49 | } 50 | }, 51 | 52 | _zoomOut: function (e) { 53 | if (!this._disabled) { 54 | this._map.zoomOut(e.shiftKey ? 3 : 1); 55 | } 56 | }, 57 | 58 | _createButton: function (html, title, className, container, fn) { 59 | var link = L.DomUtil.create('a', className, container); 60 | link.innerHTML = html; 61 | link.href = '#'; 62 | link.title = title; 63 | 64 | L.DomEvent 65 | .on(link, 'mousedown dblclick', L.DomEvent.stopPropagation) 66 | .on(link, 'click', L.DomEvent.stop) 67 | .on(link, 'click', fn, this) 68 | .on(link, 'click', this._refocusOnMap, this); 69 | 70 | return link; 71 | }, 72 | 73 | _updateDisabled: function () { 74 | var map = this._map, 75 | className = 'leaflet-disabled'; 76 | 77 | L.DomUtil.removeClass(this._zoomInButton, className); 78 | L.DomUtil.removeClass(this._zoomOutButton, className); 79 | 80 | if (this._disabled || map._zoom === map.getMinZoom()) { 81 | L.DomUtil.addClass(this._zoomOutButton, className); 82 | } 83 | if (this._disabled || map._zoom === map.getMaxZoom()) { 84 | L.DomUtil.addClass(this._zoomInButton, className); 85 | } 86 | } 87 | }); 88 | 89 | L.Map.mergeOptions({ 90 | zoomControl: true 91 | }); 92 | 93 | L.Map.addInitHook(function () { 94 | if (this.options.zoomControl) { 95 | this.zoomControl = new L.Control.Zoom(); 96 | this.addControl(this.zoomControl); 97 | } 98 | }); 99 | 100 | L.control.zoom = function (options) { 101 | return new L.Control.Zoom(options); 102 | }; 103 | -------------------------------------------------------------------------------- /src/geometry/Point.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Point represents a point with x and y coordinates. 3 | */ 4 | 5 | L.Point = function (x, y, 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 | ceil: function () { 79 | return this.clone()._ceil(); 80 | }, 81 | 82 | _ceil: function () { 83 | this.x = Math.ceil(this.x); 84 | this.y = Math.ceil(this.y); 85 | return this; 86 | }, 87 | 88 | distanceTo: function (point) { 89 | point = L.point(point); 90 | 91 | var x = point.x - this.x, 92 | y = point.y - this.y; 93 | 94 | return Math.sqrt(x * x + y * y); 95 | }, 96 | 97 | equals: function (point) { 98 | point = L.point(point); 99 | 100 | return point.x === this.x && 101 | point.y === this.y; 102 | }, 103 | 104 | contains: function (point) { 105 | point = L.point(point); 106 | 107 | return Math.abs(point.x) <= Math.abs(this.x) && 108 | Math.abs(point.y) <= Math.abs(this.y); 109 | }, 110 | 111 | toString: function () { 112 | return 'Point(' + 113 | L.Util.formatNum(this.x) + ', ' + 114 | L.Util.formatNum(this.y) + ')'; 115 | } 116 | }; 117 | 118 | L.point = function (x, y, round) { 119 | if (x instanceof L.Point) { 120 | return x; 121 | } 122 | if (L.Util.isArray(x)) { 123 | return new L.Point(x[0], x[1]); 124 | } 125 | if (x === undefined || x === null) { 126 | return x; 127 | } 128 | return new L.Point(x, y, round); 129 | }; 130 | -------------------------------------------------------------------------------- /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/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 | this._simulateEvent('mousedown', first); 53 | 54 | L.DomEvent.on(document, { 55 | touchmove: this._onMove, 56 | touchend: this._onUp 57 | }, this); 58 | }, 59 | 60 | _onUp: function (e) { 61 | clearTimeout(this._holdTimeout); 62 | 63 | L.DomEvent.off(document, { 64 | touchmove: this._onMove, 65 | touchend: this._onUp 66 | }, this); 67 | 68 | if (this._fireClick && e && e.changedTouches) { 69 | 70 | var first = e.changedTouches[0], 71 | el = first.target; 72 | 73 | if (el && el.tagName && el.tagName.toLowerCase() === 'a') { 74 | L.DomUtil.removeClass(el, 'leaflet-active'); 75 | } 76 | 77 | this._simulateEvent('mouseup', first); 78 | 79 | // simulate click if the touch didn't move too much 80 | if (this._isTapValid()) { 81 | this._simulateEvent('click', first); 82 | } 83 | } 84 | }, 85 | 86 | _isTapValid: function () { 87 | return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance; 88 | }, 89 | 90 | _onMove: function (e) { 91 | var first = e.touches[0]; 92 | this._newPos = new L.Point(first.clientX, first.clientY); 93 | }, 94 | 95 | _simulateEvent: function (type, e) { 96 | var simulatedEvent = document.createEvent('MouseEvents'); 97 | 98 | simulatedEvent._simulated = true; 99 | e.target._simulatedClick = true; 100 | 101 | simulatedEvent.initMouseEvent( 102 | type, true, true, window, 1, 103 | e.screenX, e.screenY, 104 | e.clientX, e.clientY, 105 | false, false, false, false, 0, null); 106 | 107 | e.target.dispatchEvent(simulatedEvent); 108 | } 109 | }); 110 | 111 | if (L.Browser.touch && !L.Browser.pointer) { 112 | L.Map.addInitHook('addHandler', 'tap', L.Map.Tap); 113 | } 114 | -------------------------------------------------------------------------------- /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 | this.stop(); 14 | 15 | if (this._loaded && !options.reset && options !== true) { 16 | 17 | if (options.animate !== undefined) { 18 | options.zoom = L.extend({animate: options.animate}, options.zoom); 19 | options.pan = L.extend({animate: options.animate}, options.pan); 20 | } 21 | 22 | // try animating pan or zoom 23 | var animated = (this._zoom !== zoom) ? 24 | this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : 25 | this._tryAnimatedPan(center, options.pan); 26 | 27 | if (animated) { 28 | // prevent resize handler call, the view will refresh after animation anyway 29 | clearTimeout(this._sizeTimer); 30 | return this; 31 | } 32 | } 33 | 34 | // animation didn't start, just reset the map view 35 | this._resetView(center, zoom); 36 | 37 | return this; 38 | }, 39 | 40 | panBy: function (offset, options) { 41 | offset = L.point(offset).round(); 42 | options = options || {}; 43 | 44 | if (!offset.x && !offset.y) { 45 | return this; 46 | } 47 | //If we pan too far then chrome gets issues with tiles 48 | // and makes them disappear or appear in the wrong place (slightly offset) #2602 49 | if (options.animate !== true && !this.getSize().contains(offset)) { 50 | return this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom()); 51 | } 52 | 53 | if (!this._panAnim) { 54 | this._panAnim = new L.PosAnimation(); 55 | 56 | this._panAnim.on({ 57 | 'step': this._onPanTransitionStep, 58 | 'end': this._onPanTransitionEnd 59 | }, this); 60 | } 61 | 62 | // don't fire movestart if animating inertia 63 | if (!options.noMoveStart) { 64 | this.fire('movestart'); 65 | } 66 | 67 | // animate pan unless animate: false specified 68 | if (options.animate !== false) { 69 | L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim'); 70 | 71 | var newPos = this._getMapPanePos().subtract(offset); 72 | this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity); 73 | } else { 74 | this._rawPanBy(offset); 75 | this.fire('move').fire('moveend'); 76 | } 77 | 78 | return this; 79 | }, 80 | 81 | _onPanTransitionStep: function () { 82 | this.fire('move'); 83 | }, 84 | 85 | _onPanTransitionEnd: function () { 86 | L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim'); 87 | this.fire('moveend'); 88 | }, 89 | 90 | _tryAnimatedPan: function (center, options) { 91 | // difference between the new and current centers in pixels 92 | var offset = this._getCenterOffset(center)._floor(); 93 | 94 | // don't animate too far unless animate: true specified in options 95 | if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; } 96 | 97 | this.panBy(offset, options); 98 | 99 | return (options && options.animate) !== false; 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /spec/suites/geometry/PointSpec.js: -------------------------------------------------------------------------------- 1 | describe("Point", function () { 2 | 3 | describe('constructor', function () { 4 | 5 | it("creates a point with the given x and y", function () { 6 | var p = new L.Point(1.5, 2.5); 7 | expect(p.x).to.eql(1.5); 8 | expect(p.y).to.eql(2.5); 9 | }); 10 | 11 | it("rounds the given x and y if the third argument is true", function () { 12 | var p = new L.Point(1.3, 2.7, true); 13 | expect(p.x).to.eql(1); 14 | expect(p.y).to.eql(3); 15 | }); 16 | }); 17 | 18 | describe('#subtract', function () { 19 | it('subtracts the given point from this one', function () { 20 | var a = new L.Point(50, 30), 21 | b = new L.Point(20, 10); 22 | expect(a.subtract(b)).to.eql(new L.Point(30, 20)); 23 | }); 24 | }); 25 | 26 | describe('#add', function () { 27 | it('adds given point to this one', function () { 28 | expect(new L.Point(50, 30).add(new L.Point(20, 10))).to.eql(new L.Point(70, 40)); 29 | }); 30 | }); 31 | 32 | describe('#divideBy', function () { 33 | it('divides this point by the given amount', function () { 34 | expect(new L.Point(50, 30).divideBy(5)).to.eql(new L.Point(10, 6)); 35 | }); 36 | }); 37 | 38 | describe('#multiplyBy', function () { 39 | it('multiplies this point by the given amount', function () { 40 | expect(new L.Point(50, 30).multiplyBy(2)).to.eql(new L.Point(100, 60)); 41 | }); 42 | }); 43 | 44 | describe('#floor', function () { 45 | it('returns a new point with floored coordinates', function () { 46 | expect(new L.Point(50.56, 30.123).floor()).to.eql(new L.Point(50, 30)); 47 | }); 48 | }); 49 | 50 | describe('#distanceTo', function () { 51 | it('calculates distance between two points', function () { 52 | var p1 = new L.Point(0, 30); 53 | var p2 = new L.Point(40, 0); 54 | expect(p1.distanceTo(p2)).to.eql(50.0); 55 | }); 56 | }); 57 | 58 | describe('#equals', function () { 59 | it('returns true if points are equal', function () { 60 | var p1 = new L.Point(20.4, 50.12); 61 | var p2 = new L.Point(20.4, 50.12); 62 | var p3 = new L.Point(20.5, 50.13); 63 | 64 | expect(p1.equals(p2)).to.be(true); 65 | expect(p1.equals(p3)).to.be(false); 66 | }); 67 | }); 68 | 69 | describe('#contains', function () { 70 | it('returns true if the point is bigger in absolute dimensions than the passed one', function () { 71 | var p1 = new L.Point(50, 30), 72 | p2 = new L.Point(-40, 20), 73 | p3 = new L.Point(60, -20), 74 | p4 = new L.Point(-40, -40); 75 | 76 | expect(p1.contains(p2)).to.be(true); 77 | expect(p1.contains(p3)).to.be(false); 78 | expect(p1.contains(p4)).to.be(false); 79 | }); 80 | }); 81 | 82 | describe('#toString', function () { 83 | it('formats a string out of point coordinates', function () { 84 | expect(new L.Point(50, 30) + '').to.eql('Point(50, 30)'); 85 | }); 86 | }); 87 | 88 | describe('L.point factory', function () { 89 | it('leaves L.Point instances as is', function () { 90 | var p = new L.Point(50, 30); 91 | expect(L.point(p)).to.be(p); 92 | }); 93 | it('creates a point out of three arguments', function () { 94 | expect(L.point(50.1, 30.1, true)).to.eql(new L.Point(50, 30)); 95 | }); 96 | it('creates a point from an array of coordinates', function () { 97 | expect(L.point([50, 30])).to.eql(new L.Point(50, 30)); 98 | }); 99 | it('does not fail on invalid arguments', function () { 100 | expect(L.point(undefined)).to.be(undefined); 101 | expect(L.point(null)).to.be(null); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/map/handler/Map.TouchZoom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers. 3 | */ 4 | 5 | L.Map.mergeOptions({ 6 | touchZoom: L.Browser.touch && !L.Browser.android23, 7 | bounceAtZoomLimits: true 8 | }); 9 | 10 | L.Map.TouchZoom = L.Handler.extend({ 11 | addHooks: function () { 12 | L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this); 13 | }, 14 | 15 | removeHooks: function () { 16 | L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this); 17 | }, 18 | 19 | _onTouchStart: function (e) { 20 | var map = this._map; 21 | 22 | if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; } 23 | 24 | var p1 = map.mouseEventToLayerPoint(e.touches[0]), 25 | p2 = map.mouseEventToLayerPoint(e.touches[1]), 26 | viewCenter = map._getCenterLayerPoint(); 27 | 28 | this._startCenter = p1.add(p2)._divideBy(2); 29 | this._startDist = p1.distanceTo(p2); 30 | 31 | this._moved = false; 32 | this._zooming = true; 33 | 34 | this._centerOffset = viewCenter.subtract(this._startCenter); 35 | 36 | map.stop(); 37 | 38 | L.DomEvent 39 | .on(document, 'touchmove', this._onTouchMove, this) 40 | .on(document, 'touchend', this._onTouchEnd, this); 41 | 42 | L.DomEvent.preventDefault(e); 43 | }, 44 | 45 | _onTouchMove: function (e) { 46 | if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; } 47 | 48 | var map = this._map, 49 | p1 = map.mouseEventToLayerPoint(e.touches[0]), 50 | p2 = map.mouseEventToLayerPoint(e.touches[1]); 51 | 52 | this._scale = p1.distanceTo(p2) / this._startDist; 53 | this._delta = p1._add(p2)._divideBy(2)._subtract(this._startCenter); 54 | 55 | if (!map.options.bounceAtZoomLimits) { 56 | var currentZoom = map.getScaleZoom(this._scale); 57 | if ((currentZoom <= map.getMinZoom() && this._scale < 1) || 58 | (currentZoom >= map.getMaxZoom() && this._scale > 1)) { return; } 59 | } 60 | 61 | if (!this._moved) { 62 | map 63 | .fire('movestart') 64 | .fire('zoomstart'); 65 | 66 | this._moved = true; 67 | } 68 | 69 | L.Util.cancelAnimFrame(this._animRequest); 70 | this._animRequest = L.Util.requestAnimFrame(this._updateOnMove, this, true, this._map._container); 71 | 72 | L.DomEvent.preventDefault(e); 73 | }, 74 | 75 | _updateOnMove: function () { 76 | var map = this._map; 77 | 78 | if (map.options.touchZoom === 'center') { 79 | this._center = map.getCenter(); 80 | } else { 81 | this._center = map.layerPointToLatLng(this._getTargetCenter()); 82 | } 83 | this._zoom = map.getScaleZoom(this._scale); 84 | 85 | map._animateZoom(this._center, this._zoom); 86 | }, 87 | 88 | _onTouchEnd: function () { 89 | if (!this._moved || !this._zooming) { 90 | this._zooming = false; 91 | return; 92 | } 93 | 94 | this._zooming = false; 95 | L.Util.cancelAnimFrame(this._animRequest); 96 | 97 | L.DomEvent 98 | .off(document, 'touchmove', this._onTouchMove) 99 | .off(document, 'touchend', this._onTouchEnd); 100 | 101 | var map = this._map, 102 | oldZoom = map.getZoom(), 103 | zoomDelta = this._zoom - oldZoom, 104 | finalZoom = map._limitZoom(zoomDelta > 0 ? Math.ceil(this._zoom) : Math.floor(this._zoom)); 105 | 106 | map._animateZoom(this._center, finalZoom, true); 107 | }, 108 | 109 | _getTargetCenter: function () { 110 | var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale); 111 | return this._startCenter.add(centerOffset); 112 | } 113 | }); 114 | 115 | L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom); 116 | -------------------------------------------------------------------------------- /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(85.0840591556, 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(-85.0840591556, -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 | describe("Projection.SphericalMercator", function () { 50 | var p = L.Projection.SphericalMercator; 51 | 52 | describe("#project", function () { 53 | it("projects a center point", function () { 54 | //edge cases 55 | expect(p.project(new L.LatLng(0, 0))).near(new L.Point(0, 0)); 56 | }); 57 | 58 | it("projects the northeast corner of the world", function () { 59 | expect(p.project(new L.LatLng(85.0511287798, 180))).near(new L.Point(20037508, 20037508)); 60 | }); 61 | 62 | it("projects the southwest corner of the world", function () { 63 | expect(p.project(new L.LatLng(-85.0511287798, -180))).near(new L.Point(-20037508, -20037508)); 64 | }); 65 | 66 | it("projects other points", function () { 67 | expect(p.project(new L.LatLng(50, 30))).near(new L.Point(3339584, 6446275)); 68 | 69 | // from https://github.com/Leaflet/Leaflet/issues/1578 70 | expect(p.project(new L.LatLng(51.9371170300465, 80.11230468750001))) 71 | .near(new L.Point(8918060.96409, 6788763.38325)); 72 | }); 73 | }); 74 | 75 | describe("#unproject", function () { 76 | function pr(point) { 77 | return p.project(p.unproject(point)); 78 | } 79 | 80 | it("unprojects a center point", function () { 81 | expect(pr(new L.Point(0, 0))).near(new L.Point(0, 0)); 82 | }); 83 | 84 | it("unprojects pi points", function () { 85 | expect(pr(new L.Point(-Math.PI, Math.PI))).near(new L.Point(-Math.PI, Math.PI)); 86 | expect(pr(new L.Point(-Math.PI, -Math.PI))).near(new L.Point(-Math.PI, -Math.PI)); 87 | 88 | expect(pr(new L.Point(0.523598775598, 1.010683188683))).near(new L.Point(0.523598775598, 1.010683188683)); 89 | }); 90 | 91 | it('unprojects other points', function () { 92 | // from https://github.com/Leaflet/Leaflet/issues/1578 93 | expect(pr(new L.Point(8918060.964088084, 6755099.410887127))); 94 | }); 95 | }); 96 | }); 97 | --------------------------------------------------------------------------------