diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1f98e6..35e95f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,149 @@ Leaflet Changelog ## 0.7-dev (master) -An in-progress version being developed on the `master` branch. Includes all fixes from the `stable` branch. +An in-progress version being developed on the `master` branch. + +### Improvements + +#### Usability improvements + +* Added **support for IE11 touch devices** (by [@danzel](https://github.com/danzel), [@DanielX2](https://github.com/DanielX2) and [@fnicollet](https://github.com/fnicollet)). [#2039](https://github.com/Leaflet/Leaflet/pull/2039) [#2066](https://github.com/Leaflet/Leaflet/pull/2066) [#2037](https://github.com/Leaflet/Leaflet/issues/2037) [#2102](https://github.com/Leaflet/Leaflet/issues/2102) +* Added shift-double-click to zoom out shortcut. [#2185](https://github.com/Leaflet/Leaflet/issues/2185) +* Significantly improved **controls design on mobile** devices. [#1868](https://github.com/Leaflet/Leaflet/issues/1868) [#2012](https://github.com/Leaflet/Leaflet/issues/2012) +* Fixed and improved IE7-8 control and popup styles. +* Made subtle improvements to control styles on desktop browsers. +* Improved keyboard nav support so that map doesn't loose focus when you click on a control (by [@jacobtoye](https://github.com/jacobtoye)). [#2150](https://github.com/Leaflet/Leaflet/issues/2150) [#2148](https://github.com/Leaflet/Leaflet/issues/2148) +* Improved `maxBounds` behavior: now it doesn't force higher minimal zoom, and anchors to max bounds edges properly when zooming (by [@kapouer](https://github.com/kapouer) and [@mourner](https://github.com/mourner)). [#2187](https://github.com/Leaflet/Leaflet/pull/2187) [#1946](https://github.com/Leaflet/Leaflet/pull/1946) [#2081](https://github.com/Leaflet/Leaflet/issues/2081) [#2168](https://github.com/Leaflet/Leaflet/issues/2168) [#1908](https://github.com/Leaflet/Leaflet/issues/1908) + +#### Map API improvements + +* Made `Map` `setView` `zoom` argument optional. [#2056](https://github.com/Leaflet/Leaflet/issues/2056) +* Added `maxZoom` option to `Map` `fitBounds`. [#2101](https://github.com/Leaflet/Leaflet/issues/2101) +* Added `Map` `bounceAtZoomLimits` option that makes the map bounce when you pinch-zoom past limits (it worked like this before, but now you can disable this) (by [@trevorpowell](https://github.com/trevorpowell)). [#1864](https://github.com/Leaflet/Leaflet/issues/1864) [#2072](https://github.com/Leaflet/Leaflet/pull/2072) +* Added `distance` property to `Map` and `Marker` `dragend` events. [#2158](https://github.com/Leaflet/Leaflet/issues/2158) [#872](https://github.com/Leaflet/Leaflet/issues/872) +* Added optional support for center-oriented scroll and double-click zoom (by [@jfirebaugh](https://github.com/jfirebaugh)). [#1939](https://github.com/Leaflet/Leaflet/issues/1939) +* Added `timestamp` to `Map` `locationfound` event. [#584](https://github.com/Leaflet/Leaflet/pull/584) +* Improved `Map` `invalidateSize` to call `moveend` immediately unless given `debounceMoveend: true` option (by [@jfirebaugh](https://github.com/jfirebaugh)). [#2181](https://github.com/Leaflet/Leaflet/issues/2181) + +#### TileLayer API improvements * Added `TileLayer` `maxNativeZoom` option that allows displaying tile layers on zoom levels above their maximum by **upscaling tiles**. [#1802](https://github.com/Leaflet/Leaflet/issues/1802) [#1798](https://github.com/Leaflet/Leaflet/issues/1798) +* Added `TileLayer` `tileloadstart` event (by [@tmcw](https://github.com/tmcw)). [#2142](https://github.com/Leaflet/Leaflet/pull/2142) [#2140](https://github.com/Leaflet/Leaflet/issues/2140) +* Improved `TileLayer` world size (used for wrapping and limiting tiles) to be derived from CRS instead of hardcoded, making it easier to use with custom projections (by [@perliedman](https://github.com/perliedman)). [#2160](https://github.com/Leaflet/Leaflet/pull/2160) + +#### Marker API improvements + +* Added CSS classes to draggable markers for easier customization (by [@snkashis](https://github.com/snkashis)). [#1902](https://github.com/Leaflet/Leaflet/issues/1902) [#1916](https://github.com/Leaflet/Leaflet/issues/1916) +* Added `Marker` `add` event (by [@tohaocean](https://github.com/tohaocean)). [#1942](https://github.com/Leaflet/Leaflet/issues/1942) +* Added `Marker` `getPopup` method (by [@scottharvey](https://github.com/scottharvey)). [#618](https://github.com/Leaflet/Leaflet/issues/618) [#1197](https://github.com/Leaflet/Leaflet/pull/1197) +* Added `Marker` `alt` option for adding `alt` text to markers (by [@jimmytidey](https://github.com/jimmytidey)). [#2112](https://github.com/Leaflet/Leaflet/pull/2112) + +#### Vector layers API improvements + +* Added `Path` `className` option for adding custom class names to vector layers. +* Added `Path` `lineCap` and `lineJoin` options (by [@palewire](https://github.com/palewire)). [#1843](https://github.com/Leaflet/Leaflet/issues/1843) [#1863](https://github.com/Leaflet/Leaflet/issues/1863) [#1881](https://github.com/Leaflet/Leaflet/issues/1881) +* Added ability to pass vector options to `GeoJSON` (by [@kapouer](https://github.com/kapouer)). [#2075](https://github.com/Leaflet/Leaflet/pull/2075) +* Improved `Polygon` `setLatLngs` to also accept holes (by [@aparshin](https://github.com/aparshin)). [#2095](https://github.com/Leaflet/Leaflet/pull/2095) [#1518](https://github.com/Leaflet/Leaflet/issues/1518) +* Added `GeoJSON` 3D format support and optional `altitude` argument to `LatLng` constructor (by [@Starefossen](https://github.com/Starefossen)). [#1822](https://github.com/Leaflet/Leaflet/pull/1822) +* Added `MultiPolygon` and `MultiPolyline` `openPopup` method. [#2046](https://github.com/Leaflet/Leaflet/issues/2046) + +#### Popup API improvements + +* Added `Popup` `update` method. [#1959](https://github.com/Leaflet/Leaflet/issues/1959) +* Added `Popup` `autoPanPaddingTopLeft` and `autoPanPaddingBottomRight` options (by [@albburtsev](https://github.com/albburtsev)). [#1972](https://github.com/Leaflet/Leaflet/issues/1972) [#1588](https://github.com/Leaflet/Leaflet/issues/1588) +* Added `Popup` `getContent` method. [#2100](https://github.com/Leaflet/Leaflet/issues/2100) +* Added `Popup` `getLatLng` method (by [@AndreyGeonya](https://github.com/AndreyGeonya)). [#2097](https://github.com/Leaflet/Leaflet/pull/2097) + +#### Misc API improvements + +* Added `ImageOverlay` `setUrl` and `getAttribution` methods and `attribution` option (by [@stsydow](https://github.com/stsydow)). [#1957](https://github.com/Leaflet/Leaflet/issues/1957) [#1958](https://github.com/Leaflet/Leaflet/issues/1958) +* Added localization support for the zoom control (by [@Danielku15](https://github.com/Danielku15)). [#1953](https://github.com/Leaflet/Leaflet/issues/1953) [#1643](https://github.com/Leaflet/Leaflet/issues/1643) [#1953](https://github.com/Leaflet/Leaflet/pull/1953) +* Significantly improved `L.Util.template` performance (affects `L.TileLayer`) by introducing cached template compilation (by [@calvinmetcalf](https://github.com/calvinmetcalf)). [#1969](https://github.com/Leaflet/Leaflet/issues/1969) [#1968](https://github.com/Leaflet/Leaflet/issues/1968) [#1554](https://github.com/Leaflet/Leaflet/issues/1554) +* Added `CRS` `getSize` for getting the world size in pixels (by [@perliedman](https://github.com/perliedman)). [#2160](https://github.com/Leaflet/Leaflet/pull/2160) +* Added `leaflet-drag-target` CSS class to an element under cursor when dragging for more flexible customization. [#2164](https://github.com/Leaflet/Leaflet/issues/2164) [#1902](https://github.com/Leaflet/Leaflet/issues/1902) +* Improved `L.DomUtil` `addClass`, `removeClass`, `hasClass` methods performance and fixed it to work with SVG elements. [#2164](https://github.com/Leaflet/Leaflet/issues/2164) + +#### Dev workflow improvements + +* Added an [official FAQ](https://github.com/Leaflet/Leaflet/blob/master/FAQ.md). +* Cleaned up and moved old IE styles to `leaflet.css` and removed `leaflet.ie.css`, so **no need for IE conditional comment** when including Leaflet now. [#2159](https://github.com/Leaflet/Leaflet/issues/2159) +* Added `leaflet-oldie` CSS class to map container in IE7-8 for easier styling. [#2159](https://github.com/Leaflet/Leaflet/issues/2159) +* Officially **dropped support for IE6**. Nobody cares anyway, and Leaflet should still be accessible on it. [#2159](https://github.com/Leaflet/Leaflet/issues/2159) +* Improved the build system to check JS errors in spec files. [#2151](https://github.com/Leaflet/Leaflet/issues/2151) +* Fixed `jake` command to run tests before building the source. [#2151](https://github.com/Leaflet/Leaflet/issues/2151) +* Switched the main file in `package.json` to unminified version for NPM/Browserify (by [@icetan](https://github.com/icetan)). [#2109](https://github.com/Leaflet/Leaflet/pull/2109) + +### Bugfixes + +#### 0.6 regression fixes + +* Fixed a **memory leak in iOS7** that could crash Safari when handling lots of objects (e.g. 1000 markers) (by [@danzel](https://github.com/danzel)). [#2149](https://github.com/Leaflet/Leaflet/pull/2149) [#2122](https://github.com/Leaflet/Leaflet/issues/2122) +* Fixed a bug that caused lag at the beginning of panning in Chrome (by [@jfirebaugh](https://github.com/jfirebaugh)). [#2163](https://github.com/Leaflet/Leaflet/issues/2163) +* Fixed a regression that made the layers control unscrollable in Firefox. [#2029](https://github.com/Leaflet/Leaflet/issues/2029) +* Fixed a regression that broke `worldCopyJump: true` option (by [@fastrde](https://github.com/fastrde)). [#1904](https://github.com/Leaflet/Leaflet/issues/1904) [#1831](https://github.com/Leaflet/Leaflet/issues/1831) [#1982](https://github.com/Leaflet/Leaflet/issues/1982) +* Fixed a regression where a first map click after popup close button click was ignored (by [@fastrde](https://github.com/fastrde)). [#1537](https://github.com/Leaflet/Leaflet/issues/1537) [#1963](https://github.com/Leaflet/Leaflet/issues/1963) [#1925](https://github.com/Leaflet/Leaflet/issues/1925) +* Fixed a regression where `L.DomUtil.getMousePosition` would throw an error if container argument not provided (by [@scooterw](https://github.com/scooterw)). [#1826](https://github.com/Leaflet/Leaflet/issues/1826) [#1928](https://github.com/Leaflet/Leaflet/issues/1928) [#1926](https://github.com/Leaflet/Leaflet/issues/1926) +* Fixed a regression with vector layers positioning when zooming on IE10+ touch devices (by [@danzel](https://github.com/danzel)). [#2002](https://github.com/Leaflet/Leaflet/issues/2002) [#2000](https://github.com/Leaflet/Leaflet/issues/2000) +* Fixed a regression with `maxBounds` behaving weirdly on panning inertia out of bounds. [#2168](https://github.com/Leaflet/Leaflet/issues/2168) + +#### General bugfixes + +* Fixed a bug where the map could freeze if centered and immediately recentered on initialization. [#2071](https://github.com/Leaflet/Leaflet/issues/2071) +* Fixed a bug where all maps except the first one on a page didn't track window resize. [#1980](https://github.com/Leaflet/Leaflet/issues/1980) +* Fixed a bug where tiles in `EPSG:3395` projection were shifted (by [@aparshin](https://github.com/aparshin)). [#2020](https://github.com/Leaflet/Leaflet/issues/2020) +* Fixed a bug where scale control displayed wrong scale when on pages with `box-sizing: border-box`. +* Fixed a bug where zoom control button didn't appear as disabled if map was initialized at the zoom limit. [#2083](https://github.com/Leaflet/Leaflet/issues/2083) +* Fixed a bug where box zoom also triggered a map click event (by [@fastrde](https://github.com/fastrde)). [#1951](https://github.com/Leaflet/Leaflet/issues/1951) [#1884](https://github.com/Leaflet/Leaflet/issues/1884) +* Fixed a bug where shift-clicking on a map immediately after a drag didn't trigger a click event (by [@fastrde](https://github.com/fastrde)). [#1952](https://github.com/Leaflet/Leaflet/issues/1952) [#1950](https://github.com/Leaflet/Leaflet/issues/1950) +* Fixed a bug where content was updated twice when opening a popup. [#2137](https://github.com/Leaflet/Leaflet/issues/2137) +* Fixed a bug that could sometimes cause infinite panning loop when using `maxBounds` (by [@kapouer](https://github.com/kapouer) and [@mourner](https://github.com/mourner)). [#2187](https://github.com/Leaflet/Leaflet/pull/2187) [#1946](https://github.com/Leaflet/Leaflet/pull/1946) [#2081](https://github.com/Leaflet/Leaflet/issues/2081) [#2168](https://github.com/Leaflet/Leaflet/issues/2168) [#1908](https://github.com/Leaflet/Leaflet/issues/1908) + +#### Browser bugfixes + +* Fixed a bug where keyboard `+` no longer zoomed the map on FF 22+ (by [@fastrde](https://github.com/fastrde)). [#1943](https://github.com/Leaflet/Leaflet/issues/1943) [#1981](https://github.com/Leaflet/Leaflet/issues/1981) +* Fixed a bug where calling `Map` `remove` throwed an error in IE6-8. [#2015](https://github.com/Leaflet/Leaflet/issues/2015) +* Fixed a bug where `isArray` didn't work in rare cases in IE9. [#2077](https://github.com/Leaflet/Leaflet/issues/2077) +* Fixed a bug where FF sometimes produced console warnings when animating markers. [#2090](https://github.com/Leaflet/Leaflet/issues/2090) +* Fixed a bug where mouse wasn't handled correctly on RTL pages in some cases (by [@danzel](https://github.com/danzel)). [#1986](https://github.com/Leaflet/Leaflet/issues/1986) [#2136](https://github.com/Leaflet/Leaflet/pull/2136) + +#### Mobile bugfixes + +* Fixed a bug where tiles could **disappear after zooming on Chrome 30+ for Android** (by [@danzel](https://github.com/danzel)). [#2152](https://github.com/Leaflet/Leaflet/pull/2152) [#2078](https://github.com/Leaflet/Leaflet/issues/2078) +* Fixed a bug on IE10+ touch where pinch-zoom also caused click (by [@danzel](https://github.com/danzel)). [#2117](https://github.com/Leaflet/Leaflet/pull/2117) [#2094](https://github.com/Leaflet/Leaflet/issues/2094) +* Fixed a bug on IE10+ touch where controls didn't loose the pressed state after tapping (by [@danzel](https://github.com/danzel)). [#2111](https://github.com/Leaflet/Leaflet/pull/2111) [#2103](https://github.com/Leaflet/Leaflet/issues/2103) +* Fixed a bug where clicking on layers control labels on iOS throwed an error (by [@olemarkus](https://github.com/olemarkus) and [@dagjomar](https://github.com/dagjomar)). [#1984](https://github.com/Leaflet/Leaflet/issues/1984) [#1989](https://github.com/Leaflet/Leaflet/issues/1989) + +#### Map API bugfixes + +* Fixed a bug where `Map` `getCenter` returned old result after map container size changed (by [@jfirebaugh](https://github.com/jfirebaugh)). [#1940](https://github.com/Leaflet/Leaflet/issues/1940) [#1919](https://github.com/Leaflet/Leaflet/issues/1919) +* Fixed `Map` `invalidateSize` rounding issues when changing map size by an odd pixel amount (by [@russelldavis](https://github.com/russelldavis)). [#1931](https://github.com/Leaflet/Leaflet/issues/1931) +* Fixed a bug where `Map` `removeLayer` didn't return `this` in a corner case (by [@jfirebaugh](https://github.com/jfirebaugh)). +* Fixed a bug where calling `Map` `setZoom` before `setView` would throw an error. [#1449](https://github.com/Leaflet/Leaflet/issues/1449) + +#### Layers API bugfixes + +* Fixed a bug where `Popup` `setLatLng` unnecessarily reset content and updated layout; works much faster now. [#2167](https://github.com/Leaflet/Leaflet/issues/2167) +* Fixed a bug where `toGeoJSON` of layers originated from GeoJSON GeometryCollection and MultiPoint didn't work properly (wasn't round-tripped). [#1956](https://github.com/Leaflet/Leaflet/issues/1956) +* Fixed `GeoJSON` dependencies in build configuration that could lead to a broken custom build in some situations (by [@alubchuk](https://github.com/alubchuk)). [#1909](https://github.com/Leaflet/Leaflet/issues/1909) +* Fixed a bug where `CircleMarker` popup placement wasn't updated after calling `setLatLng` (by [@snkashis](https://github.com/snkashis)). [#1921](https://github.com/Leaflet/Leaflet/issues/1921) [#1927](https://github.com/Leaflet/Leaflet/issues/1927) +* Fixed a bug where popup anchor wasn't updated on `Marker` `setIcon` (by [@snkashis](https://github.com/snkashis)). [#1874](https://github.com/Leaflet/Leaflet/issues/1874) [#1891](https://github.com/Leaflet/Leaflet/issues/1891) +* Fixed a bug with GeoJSON not throwing a descriptive error if a polygon has zero length inner ring (by [@snkashis](https://github.com/snkashis)). [#1917](https://github.com/Leaflet/Leaflet/issues/1917) [#1918](https://github.com/Leaflet/Leaflet/issues/1918) +* Fixed a bug where `FeatureGroup` would break when using non-evented children layers (by [@tmcw](https://github.com/tmcw)). [#2032](https://github.com/Leaflet/Leaflet/pull/2032) [#1962](https://github.com/Leaflet/Leaflet/issues/1962) +* Fixed a bug where `CircleMarker` `getRadius` would always return `null`. [#2016](https://github.com/Leaflet/Leaflet/issues/2016) [#2017](https://github.com/Leaflet/Leaflet/pull/2017) +* Fixed a bug where `TileLayer.WMS` didn't work with WMS 1.3 & EPSG4326 projection (by [@Bobboya](https://github.com/Bobboya)). [#1897](https://github.com/Leaflet/Leaflet/pull/1897) +* Fixed a bug where `FeatureGroup` events `e.layer` was sometimes empty in old IE. [#1938](https://github.com/Leaflet/Leaflet/issues/1938) + +#### Misc API bugfixes + +* Fixed a bug where `L.latLngBounds` didn't accept simple object `LatLng` form (by [@Gnurfos](https://github.com/Gnurfos)). [#2025](https://github.com/Leaflet/Leaflet/issues/2025) [#1915](https://github.com/Leaflet/Leaflet/issues/1915) +* Fixed a bug where `L.Util.tempalate` wouldn't work with double quotes in the string (by [@jieter](https://github.com/jieter)). [#1968](https://github.com/Leaflet/Leaflet/issues/1968) [#2121](https://github.com/Leaflet/Leaflet/pull/2121) [#2120](https://github.com/Leaflet/Leaflet/issues/2120) +* Fixed a bug where attribution control that was added to a map after attributed layers didn't have the corresponding attributions (by [@snkashis](https://github.com/snkashis)). [#2177](https://github.com/Leaflet/Leaflet/issues/2177) [#2178](https://github.com/Leaflet/Leaflet/pull/2178) + + +## 0.6.4 (July 25, 2013) + +* Fixed a regression where `fitBounds` and `setMaxBounds` could freeze the browser in some situations. [#1895](https://github.com/Leaflet/Leaflet/issues/1895) [1866](https://github.com/Leaflet/Leaflet/issues/1866) +* Fixed a bug where click on a map on a page with horizontal scroll caused the page to scroll right (by [@mstrelan](https://github.com/mstrelan)). [#1901](https://github.com/Leaflet/Leaflet/issues/1901) ## 0.6.3 (July 17, 2013) diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 00000000..79d2ae31 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,138 @@ +# Leaflet FAQ + +This is a collection of answers to the most frequently asked questions about Leaflet. + + 1. [Data Providers](#data-providers) + 2. [Commercial Use and Licensing](#commercial-use-and-licensing) + 3. [Features](#features) + 4. [Performance](#performance) + 5. [Misc](#misc) + +## Data Providers + +#### The map is wrong in my neighborhood, could you fix it? + +Nope, but you can. +The map you see on Leaflet examples is based on [OpenStreetMap](http://openstreetmap.org), +a free editable map of the world. +Signing up and editing the map there is easy, +and the changes will be reflected on the map in a few minutes. + +#### What map tiles can I use with Leaflet? Is it limited to OpenStreetMap? + +Leaflet is provider-agnostic, meaning you can use any map provider as long as you conform to its terms of use. +You can roll your own tiles as well. +[OpenStreetMap](http://openstreetmap.org) is the most popular data source among different tile providers, +but there are providers that use other sources. + +Check out [this example](http://leaflet-extras.github.io/leaflet-providers/preview/) +with half a hundred different layers to choose from. +Popular commercial options, free up to a particular number of requests, include +[MapBox](http://mapbox.com), +[CloudMade](http://cloudmade.com), +[Bing Maps](http://www.microsoft.com/maps/choose-your-binge's-maps-API.aspx) (using a [plugin](https://github.com/shramov/leaflet-plugins)), +[Esri ArcGIS](http://www.arcgis.com/features/maps/imagery.html) ([official plugin](https://github.com/Esri/esri-leaflet)) +and [Nokia Here](http://developer.here.com/web-experiences). +A notable exception is [MapQuest Open](http://developer.mapquest.com/web/products/open/map), which is free for any number of requests. + +Always be sure to **read the terms of use** of a chosen tile provider, **know its limitations**, and **attribute it properly** in your app. + +#### I'm looking for satellite imagery to use with my Leaflet map, any options? + +[MapBox](http://mapbox.com), +[Bing Maps](http://www.microsoft.com/maps/choose-your-bing-maps-API.aspx), +[ArcGIS](http://www.arcgis.com/features/maps/imagery.html) +and [MapQuest Open](http://developer.mapquest.com/web/products/open/map) provide satellite imagery among others. + +#### I want to use Google Maps API tiles with Leaflet, can I do that? + +The problem with Google is that its [Terms of Use](https://developers.google.com/maps/terms?hl=ru) forbid any means of tile access other than through the Google Maps API. + +You can add the Google Maps API as a Leaflet layer with a [plugin](https://github.com/shramov/leaflet-plugins). But note that the map experience will not be perfect, because Leaflet will just act as a proxy to the Google Maps JS engine, so you won't get all the performance and usability benefits of using Leaflet when the Google layer is on. + +#### I want to roll my own OSM tile server for Leaflet, where do I start? + +Check out [this excellent guide](http://switch2osm.org/serving-tiles/). + +#### I want to create tiles from my own data for use with Leaflet, what are the options? + +There's a number of services that allow you to do this easily, +notably [MapBox](https://www.mapbox.com/), [CartoDB](http://cartodb.com/) and [GIS Cloud](http://www.giscloud.com/). +If you want to make tiles on your own, probably the easiest way is using [TileMill](https://www.mapbox.com/tilemill/). +TileMill can export your map as a single [.mbtiles](https://www.mapbox.com/developers/mbtiles/) file, which can be copied to a webserver and accessed by Leaflet with [a small PHP script](https://github.com/infostreams/mbtiles-php). +Alternatively, you can [extract](https://github.com/mapbox/mbutil) the tiled images from the .mbtiles database and place them directly on your webserver with absolutely no server-side dependencies. + +## Commercial Use and Licensing + +#### I have an app that gets lots of hits a day, and I want to switch from Google/Bing/whatever to Leaflet. Is there a fee for using it? + +Leaflet, unlike Google Maps and other all-in-one solutions, is just a JavaScript library. +It's free to use, but doesn't provide map imagery on its own — +you have to choose a tile service to combine with it. + +There are [plenty of options](#what-map-tiles-can-i-use-with-leaflet-is-it-limited-to-openstreetmap) for a tile service, +each with their own terms of use, prices (some of them free), features, limitations, etc. +Choice is yours. + +#### I'm building a commercial app that I plan to sell. Can I use Leaflet in it? + +You're welcome, as the code is published under the very permissive [2-clause BSD License](https://github.com/Leaflet/Leaflet/blob/master/LICENSE). +Just make sure to attribute the use of the library somewhere in the app UI or the distribution +(e.g. keep the Leaflet link on the map, or mention the use on the About page or a Readme file, etc.) and you'll be fine. + +That only applies to the code though. +Make sure you conform to the terms of use of the tile images provider(s) that you choose as well. + + +## Features + +#### Why is there still no feature X in Leaflet? + +First of all, did you check out the [Leaflet plugins page](http://leafletjs.com/plugins.html)? +It lists about a hundred plugins doing all kinds of crazy stuff, +and there's a high possibility that it has what you're looking for. + +Generally, we do our best to keep the Leaflet core small, lightweight and simple, +focusing on _quality_ instead of _quantity_, and leaving all the rest to plugin authors. + +Check out [this video](http://www.youtube.com/watch?v=_P2SaCPbJ4w) of a talk by the Leaflet creator for more background on the story and philosophy behind Leaflet. +Another essential read is [Advocating Simplicity in Open Source](http://blog.universalmind.com/advocating-simplicity-in-open-source/) by the same guy. + + +## Performance + +#### I have thousands of markers on my map. How do I make it faster and more usable? + +Check out the [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster) plugin. It's amazing. + +#### I have vector data with many thousands of points on my map. Any performance tips? + +Leaflet generally does a pretty good job of handling heavy vector data +with its real-time clipping and simplification algorithms, +but browser technology still has its limits. +Try [switching from SVG to Canvas as the default rendering back-end](http://leafletjs.com/reference.html#global), +it may help considerably (depends on the app and the data). + +If you still have too much data to render, you'll have to use some help of a server-side service +like [MapBox](https://www.mapbox.com/), +[CartoDB](http://cartodb.com/) +and [GIS Cloud](http://www.giscloud.com/) +(they all work great with Leaflet). +What they do under the hood is serving rendered data as image tiles, +along with additional data to enable interactivity like hovering shapes +(e.g. done using [UTFGrid](https://www.mapbox.com/developers/utfgrid/) — +Leaflet [has a nice plugin](https://github.com/danzel/Leaflet.utfgrid) for it). + + +## Misc + +#### I downloaded the Leaflet source but didn't find `leaflet.js` there. Why is that? + +You can download the built versions using links from the [download page](http://leafletjs.com/download.html). +It even includes the latest build of the development version (`master` branch), +updated automatically on each commit to the repo. + +We removed the built versions from the repository because it's a chore to build and commit them manually on each change, +and it often complicates merging branches and managing contributions. + +There's a common complaint that Leaflet can't be used with [Bower](http://bower.io/) because of that, but we'll resolve the issue soon. diff --git a/Jakefile.js b/Jakefile.js index a4a64eac..f7ed1453 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -14,13 +14,33 @@ For a custom build, open build/build.html in the browser and follow the instruct var build = require('./build/build.js'); +function hint(msg, paths) { + return function () { + console.log(msg); + jake.exec('node node_modules/jshint/bin/jshint -c ' + paths, + {printStdout: true}, function () { + console.log('\tCheck passed.\n'); + complete(); + }); + } +} + desc('Check Leaflet source for errors with JSHint'); -task('lint', build.lint); +task('lint', {async: true}, hint('Checking for JS errors...', 'build/hintrc.js src')); + +desc('Check Leaflet specs source for errors with JSHint'); +task('lintspec', {async: true}, hint('Checking for specs JS errors...', 'spec/spec.hintrc.js spec/suites')); desc('Combine and compress Leaflet source files'); -task('build', ['lint'], build.build); +task('build', build.build); desc('Run PhantomJS tests'); -task('test', ['lint'], build.test); +task('test', ['lint', 'lintspec'], {async: true}, function () { + build.test(complete); +}); -task('default', ['build']); +task('default', ['test', 'build']); + +jake.addListener('complete', function () { + process.exit(); +}); diff --git a/README.md b/README.md index e40d3b5b..220b74ec 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,23 @@ Leaflet -Leaflet is a modern open-source JavaScript library for **mobile-friendly interactive maps**. -It is developed by [Vladimir Agafonkin][] with a team of dedicated [contributors][]. +Leaflet is an open source JavaScript library for **mobile-friendly interactive maps**. +It is developed by [Vladimir Agafonkin][] of [MapBox][] with a team of dedicated [contributors][]. Weighing just about 30 KB of gzipped JS code, it has all the [features][] most developers ever need for online maps. Leaflet is designed with *simplicity*, *performance* and *usability* in mind. It works efficiently across all major desktop and mobile platforms out of the box, taking advantage of HTML5 and CSS3 on modern browsers while being accessible on older ones too. -It can also be extended with many [plugins][], +It can be extended with a huge amount of [plugins][], has a beautiful, easy to use and [well-documented][] API and a simple, readable [source code][] that is a joy to [contribute][] to. -For more information, check out the [official website][]. +For more info, docs and tutorials, check out the [official website][].
+For **Leaflet downloads** (including the built master version), check out the [download page][]. We're happy to meet new contributors. If you want to **get involved** with Leaflet development, check out the [contribution guide][contribute]. -Let's make the best open-source library for maps that can possibly exist! - -P.S. If you're looking for **Leaflet downloads** (including the built master version), -check out the [Leaflet Download Page][]. +Let's make the best mapping library that will ever exist, +and push the limits of what's possible with online maps! [![Build Status](https://travis-ci.org/Leaflet/Leaflet.png?branch=master)](https://travis-ci.org/Leaflet/Leaflet) @@ -31,4 +30,5 @@ check out the [Leaflet Download Page][]. [hosted on GitHub]: http://github.com/Leaflet/Leaflet [contribute]: https://github.com/Leaflet/Leaflet/blob/master/CONTRIBUTING.md "A guide to contributing to Leaflet" [official website]: http://leafletjs.com - [Leaflet Download Page]: http://leafletjs.com/download.html + [download page]: http://leafletjs.com/download.html + [MapBox]: https://mapbox.com diff --git a/build/build.html b/build/build.html index 9e5a2333..f876582c 100644 --- a/build/build.html +++ b/build/build.html @@ -86,21 +86,14 @@
  • Download and install Node
  • Run this in the command line:
    npm install -g jake
    -npm install jshint
    -npm install uglify-js
  • +npm install
  • Run this command inside the Leaflet directory:
    -

    Building using Closure Compiler

    -
      -
    1. Download Closure Compiler, extract it into closure-compiler directory
    2. -
    3. Run this command in the root Leaflet directory:
    4. -
    @@ -18,18 +17,18 @@ - \ No newline at end of file + diff --git a/debug/map/controls.html b/debug/map/controls.html index 9204def6..b84adb62 100644 --- a/debug/map/controls.html +++ b/debug/map/controls.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/map/geolocation.html b/debug/map/geolocation.html index 86a23b9c..7cc7d93f 100644 --- a/debug/map/geolocation.html +++ b/debug/map/geolocation.html @@ -4,7 +4,6 @@ Leaflet geolocation debug page - diff --git a/debug/map/image-overlay.html b/debug/map/image-overlay.html index 62afa0bb..43790175 100644 --- a/debug/map/image-overlay.html +++ b/debug/map/image-overlay.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/map/map-mobile.html b/debug/map/map-mobile.html index 4c16de38..e9829c1c 100644 --- a/debug/map/map-mobile.html +++ b/debug/map/map-mobile.html @@ -6,7 +6,6 @@ - diff --git a/debug/map/map.html b/debug/map/map.html index 6246a4e8..6941ffd2 100644 --- a/debug/map/map.html +++ b/debug/map/map.html @@ -4,7 +4,6 @@ Leaflet debug page - @@ -22,7 +21,7 @@ var cloudmade = L.tileLayer('http://{s}.tile.cloudmade.com/{key}/997/256/{z}/{x}/{y}.png', { maxZoom: 18, - attribution: 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade', + attribution: 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade', key: 'd4fc77ea4a63471cab2423e66626cbb6' }); diff --git a/debug/map/max-bounds.html b/debug/map/max-bounds.html index 728f7d48..7de4eac7 100644 --- a/debug/map/max-bounds.html +++ b/debug/map/max-bounds.html @@ -6,7 +6,6 @@ - @@ -31,6 +30,8 @@ maxBounds: bounds }); + var latlngs = L.rectangle(bounds).getLatLngs(); + L.polyline(latlngs.concat([latlngs[0]])).addTo(map); diff --git a/debug/map/opacity.html b/debug/map/opacity.html index 732c8ff9..64dd5ec8 100644 --- a/debug/map/opacity.html +++ b/debug/map/opacity.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/map/scroll.html b/debug/map/scroll.html index 2da0045e..5fcb7e2a 100644 --- a/debug/map/scroll.html +++ b/debug/map/scroll.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/map/simple-proj.html b/debug/map/simple-proj.html index 7fe86064..e7e532ae 100644 --- a/debug/map/simple-proj.html +++ b/debug/map/simple-proj.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/map/wms-marble.html b/debug/map/wms-marble.html index 8e459779..41703bc2 100644 --- a/debug/map/wms-marble.html +++ b/debug/map/wms-marble.html @@ -4,10 +4,9 @@ Leaflet debug page - - + - + @@ -17,7 +16,7 @@ - \ No newline at end of file + diff --git a/debug/map/wms.html b/debug/map/wms.html index 222b7120..278a2d1f 100644 --- a/debug/map/wms.html +++ b/debug/map/wms.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/map/zoomlevels.html b/debug/map/zoomlevels.html index ab202ade..87203bfb 100644 --- a/debug/map/zoomlevels.html +++ b/debug/map/zoomlevels.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/tests/add_remove_layers.html b/debug/tests/add_remove_layers.html index 24c07aeb..f89e5f9e 100644 --- a/debug/tests/add_remove_layers.html +++ b/debug/tests/add_remove_layers.html @@ -6,7 +6,6 @@ - @@ -32,7 +31,7 @@ map.addLayer(osm); map.fitBounds(new L.LatLngBounds([51,7],[51,7])); drawTestLine(); - + }; @@ -46,19 +45,19 @@ function (e) { popup = new L.Popup(); popup.setLatLng(this.getLatLng()); - + var popuptxt = "Hello!"; alert("I am the click function"); popup.setContent(popuptxt); map.openPopup(popup); - - + + }); myLayerGroup.addLayer(myCircle); lat = lat + 0.0001; long = long + 0.0001; - + } map.addLayer(myLayerGroup); diff --git a/debug/tests/bringtoback.html b/debug/tests/bringtoback.html index 0efc770f..1cca85c0 100644 --- a/debug/tests/bringtoback.html +++ b/debug/tests/bringtoback.html @@ -6,7 +6,6 @@ - diff --git a/debug/tests/canvasloop.html b/debug/tests/canvasloop.html index fa825aed..d8a2ed41 100644 --- a/debug/tests/canvasloop.html +++ b/debug/tests/canvasloop.html @@ -1,7 +1,6 @@ - @@ -45,4 +44,4 @@ - \ No newline at end of file + diff --git a/debug/tests/click_on_canvas.html b/debug/tests/click_on_canvas.html index ae75ce96..db7c0375 100644 --- a/debug/tests/click_on_canvas.html +++ b/debug/tests/click_on_canvas.html @@ -6,7 +6,6 @@ - diff --git a/debug/tests/dragging_and_copyworldjump.html b/debug/tests/dragging_and_copyworldjump.html new file mode 100644 index 00000000..d7ede358 --- /dev/null +++ b/debug/tests/dragging_and_copyworldjump.html @@ -0,0 +1,61 @@ + + + + + Leaflet debug page + + + + + + + + + + + +

    + On the left Map dragging and worldCopyJump are enabled during initialisation.
    + On the right Map worldCopyJump is enabled. Dragging is enabled by clicking the button. +

    +
    +
    +
    +
    + + + + + diff --git a/debug/tests/opacity.html b/debug/tests/opacity.html index 8a2a9965..940a6f37 100644 --- a/debug/tests/opacity.html +++ b/debug/tests/opacity.html @@ -6,7 +6,6 @@ - + + + +

    Click the map to place a popup at the mouse location

    +
    + + + + diff --git a/debug/tests/rtl2.html b/debug/tests/rtl2.html new file mode 100644 index 00000000..eaa4f635 --- /dev/null +++ b/debug/tests/rtl2.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + +
    + + + diff --git a/debug/tests/set_icon_reuse_dom.html b/debug/tests/set_icon_reuse_dom.html index 1e9d68ac..e8ee1cfb 100644 --- a/debug/tests/set_icon_reuse_dom.html +++ b/debug/tests/set_icon_reuse_dom.html @@ -2,18 +2,17 @@ Test for preservation of Icon DOM element - - + - +
    - + @@ -33,7 +32,7 @@ var osm = new L.TileLayer(osmUrl, { minZoom: 1, maxZoom: 17 }); map.addLayer(osm); map.fitBounds(new L.LatLngBounds([51,7],[51,7])); - + var route = L.polyline([ [51, 7.000], [51.002, 7.004], @@ -45,8 +44,8 @@ [51.002, 7.004] ], { clickable:false,color:'#f00' } - ).addTo(map); - + ).addTo(map); + // when the mouse hovers over the red route2, you cannot click through the blue route1 beneath }; diff --git a/debug/vector/bounds-extend.html b/debug/vector/bounds-extend.html index 960d95ae..c0c2d105 100644 --- a/debug/vector/bounds-extend.html +++ b/debug/vector/bounds-extend.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/vector/feature-group-bounds.html b/debug/vector/feature-group-bounds.html index 384eff44..173e9c8f 100644 --- a/debug/vector/feature-group-bounds.html +++ b/debug/vector/feature-group-bounds.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/vector/geojson.html b/debug/vector/geojson.html index b5988081..c3f72e36 100644 --- a/debug/vector/geojson.html +++ b/debug/vector/geojson.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/vector/rectangle.html b/debug/vector/rectangle.html index 5db4eba4..c9179eb9 100644 --- a/debug/vector/rectangle.html +++ b/debug/vector/rectangle.html @@ -6,7 +6,6 @@ - diff --git a/debug/vector/touchzoomemu.html b/debug/vector/touchzoomemu.html index 47c3a9a5..5030f811 100644 --- a/debug/vector/touchzoomemu.html +++ b/debug/vector/touchzoomemu.html @@ -4,7 +4,6 @@ Leaflet debug page - @@ -24,7 +23,7 @@ //console.log = function (text) { // buffer += text + "\r\n"; //} - + var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/d4fc77ea4a63471cab2423e66626cbb6/997/256/{z}/{x}/{y}.png', cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18}), map = new L.Map('map', {layers: [cloudmade], center: new L.LatLng(51.505, -0.04), zoom: 13}); @@ -157,7 +156,7 @@ } _timerAt++; } - + function Hack4() { _timerAt = 0; clearInterval(_timer); diff --git a/debug/vector/vector-bounds.html b/debug/vector/vector-bounds.html index 5c63eb23..ba0839ee 100644 --- a/debug/vector/vector-bounds.html +++ b/debug/vector/vector-bounds.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/vector/vector-canvas.html b/debug/vector/vector-canvas.html index 99c0d130..932912a0 100644 --- a/debug/vector/vector-canvas.html +++ b/debug/vector/vector-canvas.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/debug/vector/vector-mobile.html b/debug/vector/vector-mobile.html index 59157cd5..db531407 100644 --- a/debug/vector/vector-mobile.html +++ b/debug/vector/vector-mobile.html @@ -6,7 +6,6 @@ - diff --git a/debug/vector/vector-simple.html b/debug/vector/vector-simple.html index 832dc44d..527831f5 100644 --- a/debug/vector/vector-simple.html +++ b/debug/vector/vector-simple.html @@ -6,7 +6,6 @@ - diff --git a/debug/vector/vector.html b/debug/vector/vector.html index 3eea849b..f39b830f 100644 --- a/debug/vector/vector.html +++ b/debug/vector/vector.html @@ -4,7 +4,6 @@ Leaflet debug page - diff --git a/dist/leaflet.css b/dist/leaflet.css index 7dc11b12..ac0cd174 100644 --- a/dist/leaflet.css +++ b/dist/leaflet.css @@ -65,6 +65,16 @@ .leaflet-marker-pane { z-index: 6; } .leaflet-popup-pane { z-index: 7; } +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + /* control positioning */ @@ -160,9 +170,8 @@ .leaflet-control { cursor: auto; } -.leaflet-dragging, -.leaflet-dragging .leaflet-clickable, -.leaflet-dragging .leaflet-container { +.leaflet-dragging .leaflet-container, +.leaflet-dragging .leaflet-clickable { cursor: move; cursor: -webkit-grabbing; cursor: -moz-grabbing; @@ -182,9 +191,8 @@ outline: 2px solid orange; } .leaflet-zoom-box { - border: 2px dotted #05f; - background: white; - opacity: 0.5; + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); } @@ -197,11 +205,11 @@ /* general toolbar styles */ .leaflet-bar { - box-shadow: 0 1px 7px rgba(0,0,0,0.65); - -webkit-border-radius: 4px; - border-radius: 4px; + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; } -.leaflet-bar a, .leaflet-bar a:hover { +.leaflet-bar a, +.leaflet-bar a:hover { background-color: #fff; border-bottom: 1px solid #ccc; width: 26px; @@ -222,16 +230,12 @@ background-color: #f4f4f4; } .leaflet-bar a:first-child { - -webkit-border-top-left-radius: 4px; - border-top-left-radius: 4px; - -webkit-border-top-right-radius: 4px; - border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; } .leaflet-bar a:last-child { - -webkit-border-bottom-left-radius: 4px; - border-bottom-left-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; border-bottom: none; } .leaflet-bar a.leaflet-disabled { @@ -240,55 +244,38 @@ color: #bbb; } -.leaflet-touch .leaflet-bar { - -webkit-border-radius: 10px; - border-radius: 10px; - } .leaflet-touch .leaflet-bar a { width: 30px; height: 30px; - } -.leaflet-touch .leaflet-bar a:first-child { - -webkit-border-top-left-radius: 7px; - border-top-left-radius: 7px; - -webkit-border-top-right-radius: 7px; - border-top-right-radius: 7px; - } -.leaflet-touch .leaflet-bar a:last-child { - -webkit-border-bottom-left-radius: 7px; - border-bottom-left-radius: 7px; - -webkit-border-bottom-right-radius: 7px; - border-bottom-right-radius: 7px; - border-bottom: none; + line-height: 30px; } /* zoom control */ -.leaflet-control-zoom-in { +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; } .leaflet-control-zoom-out { - font: bold 22px 'Lucida Console', Monaco, monospace; + font-size: 20px; } .leaflet-touch .leaflet-control-zoom-in { font-size: 22px; - line-height: 30px; } .leaflet-touch .leaflet-control-zoom-out { - font-size: 28px; - line-height: 30px; + font-size: 24px; } /* layers control */ .leaflet-control-layers { - box-shadow: 0 1px 7px rgba(0,0,0,0.4); - background: #f8f8f9; - -webkit-border-radius: 5px; - border-radius: 5px; + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; } .leaflet-control-layers-toggle { background-image: url(images/layers.png); @@ -334,8 +321,8 @@ /* attribution and scale controls */ .leaflet-container .leaflet-control-attribution { - background-color: rgba(255, 255, 255, 0.7); - box-shadow: 0 0 5px #bbb; + background: #fff; + background: rgba(255, 255, 255, 0.7); margin: 0; } .leaflet-control-attribution, @@ -343,6 +330,12 @@ padding: 0 5px; color: #333; } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover { + text-decoration: underline; + } .leaflet-container .leaflet-control-attribution, .leaflet-container .leaflet-control-scale { font-size: 11px; @@ -356,21 +349,21 @@ .leaflet-control-scale-line { border: 2px solid #777; border-top: none; - color: black; line-height: 1.1; padding: 2px 5px 1px; font-size: 11px; - text-shadow: 1px 1px 1px #fff; - background-color: rgba(255, 255, 255, 0.5); - box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.2); white-space: nowrap; overflow: hidden; + -moz-box-sizing: content-box; + box-sizing: content-box; + + background: #fff; + background: rgba(255, 255, 255, 0.5); } .leaflet-control-scale-line:not(:first-child) { border-top: 2px solid #777; border-bottom: none; margin-top: -2px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .leaflet-control-scale-line:not(:first-child):not(:last-child) { border-bottom: 2px solid #777; @@ -383,7 +376,8 @@ } .leaflet-touch .leaflet-control-layers, .leaflet-touch .leaflet-bar { - border: 4px solid rgba(0,0,0,0.3); + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; } @@ -396,8 +390,7 @@ .leaflet-popup-content-wrapper { padding: 1px; text-align: left; - -webkit-border-radius: 12px; - border-radius: 12px; + border-radius: 12px; } .leaflet-popup-content { margin: 13px 19px; @@ -426,7 +419,8 @@ -o-transform: rotate(45deg); transform: rotate(45deg); } -.leaflet-popup-content-wrapper, .leaflet-popup-tip { +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { background: white; box-shadow: 0 3px 14px rgba(0,0,0,0.4); @@ -454,6 +448,27 @@ border-top: 1px solid #ddd; } +.leaflet-oldie .leaflet-popup-content-wrapper { + zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } +.leaflet-oldie .leaflet-popup-tip-container { + margin-top: -1px; + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + /* div icon */ @@ -461,7 +476,3 @@ background: #fff; border: 1px solid #666; } -.leaflet-editing-icon { - -webkit-border-radius: 2px; - border-radius: 2px; - } diff --git a/dist/leaflet.ie.css b/dist/leaflet.ie.css deleted file mode 100644 index 14b84b69..00000000 --- a/dist/leaflet.ie.css +++ /dev/null @@ -1,51 +0,0 @@ -.leaflet-vml-shape { - width: 1px; - height: 1px; - } -.lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; - } - -.leaflet-control { - display: inline; - } - -.leaflet-popup-tip { - width: 21px; - _width: 27px; - margin: 0 auto; - _margin-top: -3px; - - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - } -.leaflet-popup-tip-container { - margin-top: -1px; - } -.leaflet-popup-content-wrapper, .leaflet-popup-tip { - border: 1px solid #999; - } -.leaflet-popup-content-wrapper { - zoom: 1; - } - -.leaflet-control-zoom, -.leaflet-control-layers { - border: 3px solid #999; - } -.leaflet-control-layers-toggle { - } -.leaflet-control-attribution, -.leaflet-control-layers, -.leaflet-control-scale-line { - background: white; - } -.leaflet-zoom-box { - filter: alpha(opacity=50); - } -.leaflet-control-attribution { - border-top: 1px solid #bbb; - border-left: 1px solid #bbb; - } diff --git a/package.json b/package.json index 688dfc63..efb0cb2b 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,27 @@ { - "name": "leaflet", - "version": "0.6.4", - "description": "JavaScript library for mobile-friendly interactive maps", - "devDependencies": { - "jshint": "~2.1.4", - "mocha": "~1.10.0", - "happen": "~0.1.2", - "karma": "~0.8.6", - "uglify-js": "~2.3.6", - "jake": "~0.5.16" - }, - "main": "dist/leaflet.js", - "scripts": { - "test": "jake test", - "prepublish": "jake" - }, - "repository": { - "type": "git", - "url": "git://github.com/Leaflet/Leaflet.git" - }, - "keywords": ["gis", "map"] + "name": "leaflet", + "version": "0.7.0", + "description": "JavaScript library for mobile-friendly interactive maps", + "devDependencies": { + "jake": "~0.7.4", + "jshint": "~2.3.0", + "uglify-js": "~2.4.3", + "mocha": "~1.14.0", + "happen": "~0.1.3", + "karma": "~0.10.4", + "karma-mocha": "~0.1.0" + }, + "main": "dist/leaflet-src.js", + "scripts": { + "test": "jake test", + "prepublish": "jake build" + }, + "repository": { + "type": "git", + "url": "git://github.com/Leaflet/Leaflet.git" + }, + "keywords": [ + "gis", + "map" + ] } diff --git a/spec/after.js b/spec/after.js index 7dcd1d9b..94d1dfee 100644 --- a/spec/after.js +++ b/spec/after.js @@ -1,2 +1,2 @@ // put after Leaflet files as imagePath can't be detected in a PhantomJS env -L.Icon.Default.imagePath = "../dist/images"; +L.Icon.Default.imagePath = "/base/dist/images"; diff --git a/spec/index.html b/spec/index.html index 6454bc2f..f1201a8f 100644 --- a/spec/index.html +++ b/spec/index.html @@ -1,15 +1,15 @@ - - Spec Runner + + Leaflet Spec Runner -
    - - - +
    + + + @@ -18,10 +18,12 @@ - + @@ -74,8 +76,11 @@ - + + + + diff --git a/spec/karma.conf.js b/spec/karma.conf.js index f36142f0..76f717b5 100644 --- a/spec/karma.conf.js +++ b/spec/karma.conf.js @@ -1,64 +1,65 @@ // Karma configuration -var libSources = require(__dirname+'/../build/build.js').getFiles(); +module.exports = function (config) { -// base path, that will be used to resolve files and exclude -basePath = ''; + var libSources = require(__dirname+'/../build/build.js').getFiles(); -for (var i=0; i < libSources.length; i++) { - libSources[i] = "../" + libSources[i]; -} + var files = [ + "spec/before.js", + "spec/sinon.js", + "spec/expect.js" + ].concat(libSources, [ + "spec/after.js", + "node_modules/happen/happen.js", + "spec/suites/SpecHelper.js", + "spec/suites/**/*.js", + {pattern: "dist/images/*.png", included: false} + ]); -// list of files / patterns to load in the browser -files = [].concat([ - "../node_modules/mocha/mocha.js", - MOCHA_ADAPTER, - "before.js", - "sinon.js", - "expect.js" -], libSources, [ - "after.js", - "../node_modules/happen/src/happen.js", - "suites/SpecHelper.js", - "suites/**/*.js" -]); + config.set({ + // base path, that will be used to resolve files and exclude + basePath: '../', -// list of files to exclude -exclude = [ -]; + plugins: ['karma-mocha', 'karma-phantomjs-launcher', 'karma-chrome-launcher'], -// test results reporter to use -// possible values: 'dots', 'progress', 'junit' -reporters = ['dots']; + // frameworks to use + frameworks: ['mocha'], -// web server port -port = 8080; + // list of files / patterns to load in the browser + files: files, + exclude: [], -// cli runner port -runnerPort = 9100; + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['dots'], -// enable / disable colors in the output (reporters and logs) -colors = true; + // web server port + port: 9876, -// level of logging -// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG -logLevel = LOG_WARN; + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_WARN, -// enable / disable watching file and executing tests whenever any file changes -autoWatch = false; + // enable / disable colors in the output (reporters and logs) + colors: true, -// Start these browsers, currently available: -// - Chrome -// - ChromeCanary -// - Firefox -// - Opera -// - Safari (only Mac) -// - PhantomJS -// - IE (only Windows) -browsers = ['PhantomJS']; + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, -// If browser does not capture in given timeout [ms], kill it -captureTimeout = 5000; + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: ['PhantomJS'], -// Continuous Integration mode -// if true, it capture browsers, run tests and exit -singleRun = true; + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 5000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: true + }); +}; diff --git a/spec/spec.hintrc.js b/spec/spec.hintrc.js new file mode 100644 index 00000000..3dc6f044 --- /dev/null +++ b/spec/spec.hintrc.js @@ -0,0 +1,25 @@ +{ + "browser": true, + "node": true, + "predef": ["define", "L", "expect", "describe", "it", "sinon", "happen", "beforeEach", "afterEach"], + "strict": false, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "forin": false, + "immed": true, + "latedef": true, + "newcap": true, + "noarg": true, + "noempty": true, + "nonew": true, + "undef": true, + // "unused": true, + // "quotmark": "single", + "indent": 4, + "trailing": true, + "white": true, + "smarttabs": true + // "maxlen": 120 +} diff --git a/spec/suites/LeafletSpec.js b/spec/suites/LeafletSpec.js index 38765e90..3fe80572 100644 --- a/spec/suites/LeafletSpec.js +++ b/spec/suites/LeafletSpec.js @@ -1,5 +1,5 @@ -describe('L#noConflict', function() { - it('restores the previous L value and returns Leaflet namespace', function(){ +describe('L#noConflict', function () { + it('restores the previous L value and returns Leaflet namespace', function () { expect(L.version).to.be.ok(); diff --git a/spec/suites/SpecHelper.js b/spec/suites/SpecHelper.js index 353359fc..cabff4b2 100644 --- a/spec/suites/SpecHelper.js +++ b/spec/suites/SpecHelper.js @@ -1,26 +1,42 @@ -function noSpecs() { - xit('has no specs'); +if (!Array.prototype.map) { + Array.prototype.map = function (fun /*, thisp */) { + "use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + // jshint bitwise: false + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + res[i] = fun.call(thisp, t[i], i, t); + } + } + + return res; + }; } -if (!Array.prototype.map) { - Array.prototype.map = function(fun /*, thisp */) { - "use strict"; +expect.Assertion.prototype.near = function (expected, delta) { + delta = delta || 1; + expect(this.obj.x).to + .be.within(expected.x - delta, expected.x + delta); + expect(this.obj.y).to + .be.within(expected.y - delta, expected.y + delta); +}; - if (this === void 0 || this === null) - throw new TypeError(); - - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") - throw new TypeError(); - - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) - res[i] = fun.call(thisp, t[i], i, t); - } - - return res; - }; -} \ No newline at end of file +expect.Assertion.prototype.nearLatLng = function (expected, delta) { + delta = delta || 1e-4; + expect(this.obj.lat).to + .be.within(expected.lat - delta, expected.lat + delta); + expect(this.obj.lng).to + .be.within(expected.lng - delta, expected.lng + delta); +}; diff --git a/spec/suites/control/Control.LayersSpec.js b/spec/suites/control/Control.LayersSpec.js index b4387349..8a7e1fe0 100644 --- a/spec/suites/control/Control.LayersSpec.js +++ b/spec/suites/control/Control.LayersSpec.js @@ -12,7 +12,7 @@ describe("Control.Layers", function () { spy = sinon.spy(); map.on('baselayerchange', spy) - .whenReady(function() { + .whenReady(function () { happen.click(layers._baseLayersList.getElementsByTagName("input")[0]); expect(spy.called).to.be.ok(); diff --git a/spec/suites/core/ClassSpec.js b/spec/suites/core/ClassSpec.js index 59c34e51..81673e3f 100644 --- a/spec/suites/core/ClassSpec.js +++ b/spec/suites/core/ClassSpec.js @@ -1,11 +1,11 @@ -describe("Class", function() { +describe("Class", function () { - describe("#extend", function() { + describe("#extend", function () { var Klass, constructor, method; - beforeEach(function() { + beforeEach(function () { constructor = sinon.spy(); method = sinon.spy(); @@ -19,7 +19,7 @@ describe("Class", function() { }); }); - it("creates a class with the given constructor & properties", function() { + it("creates a class with the given constructor & properties", function () { var a = new Klass(); expect(constructor.called).to.be.ok(); @@ -30,7 +30,7 @@ describe("Class", function() { expect(method.called).to.be.ok(); }); - it("inherits parent classes' constructor & properties", function() { + it("inherits parent classes' constructor & properties", function () { var Klass2 = Klass.extend({baz: 2}); var b = new Klass2(); @@ -46,28 +46,28 @@ describe("Class", function() { expect(method.called).to.be.ok(); }); - it("supports static properties", function() { + it("supports static properties", function () { expect(Klass.bla).to.eql(1); }); - it("inherits parent static properties", function() { + it("inherits parent static properties", function () { var Klass2 = Klass.extend({}); expect(Klass2.bla).to.eql(1); }); - it("overrides parent static properties", function() { + it("overrides parent static properties", function () { var Klass2 = Klass.extend({statics: {bla: 2}}); expect(Klass2.bla).to.eql(2); }); - it("includes the given mixin", function() { + it("includes the given mixin", function () { var a = new Klass(); expect(a.mixin).to.be.ok(); }); - it("includes multiple mixins", function() { + it("includes multiple mixins", function () { var Klass2 = L.Class.extend({ includes: [{mixin: true}, {mixin2: true}] }); @@ -77,14 +77,14 @@ describe("Class", function() { expect(a.mixin2).to.be.ok(); }); - it("grants the ability to include the given mixin", function() { + it("grants the ability to include the given mixin", function () { Klass.include({mixin2: true}); var a = new Klass(); expect(a.mixin2).to.be.ok(); }); - it("merges options instead of replacing them", function() { + it("merges options instead of replacing them", function () { var KlassWithOptions1 = L.Class.extend({ options: { foo1: 1, diff --git a/spec/suites/core/EventsSpec.js b/spec/suites/core/EventsSpec.js index 400f4242..69b130e0 100644 --- a/spec/suites/core/EventsSpec.js +++ b/spec/suites/core/EventsSpec.js @@ -1,21 +1,21 @@ -describe('Events', function() { +describe('Events', function () { var Klass; - beforeEach(function() { + beforeEach(function () { Klass = L.Class.extend({ includes: L.Mixin.Events }); }); - describe('#fireEvent', function() { + describe('#fireEvent', function () { - it('fires all listeners added through #addEventListener', function() { + it('fires all listeners added through #addEventListener', function () { var obj = new Klass(), spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy(), spy4 = sinon.spy(), - spy5 = sinon.spy(); + spy5 = sinon.spy(), spy6 = sinon.spy(); obj.addEventListener('test', spy1); @@ -42,7 +42,7 @@ describe('Events', function() { expect(spy6.callCount).to.be(1); }); - it('provides event object to listeners and executes them in the right context', function() { + it('provides event object to listeners and executes them in the right context', function () { var obj = new Klass(), obj2 = new Klass(), obj3 = new Klass(), @@ -88,7 +88,7 @@ describe('Events', function() { obj4.fireEvent('test', {baz: 4}); }); - it('calls no listeners removed through #removeEventListener', function() { + it('calls no listeners removed through #removeEventListener', function () { var obj = new Klass(), spy = sinon.spy(), spy2 = sinon.spy(), @@ -171,7 +171,7 @@ describe('Events', function() { expect(spy2.called).to.be(true); }); - it('removes listeners with a stamp originally added without one', function() { + it('removes listeners with a stamp originally added without one', function () { var obj = new Klass(), spy1 = sinon.spy(), spy2 = sinon.spy(), @@ -190,6 +190,29 @@ describe('Events', function() { expect(spy2.called).to.be(false); }); + it('removes listeners with context == this and a stamp originally added without one', function () { + var obj = new Klass(), + obj2 = new Klass(), + spy1 = sinon.spy(), + spy2 = sinon.spy(), + spy3 = sinon.spy(); + + obj.addEventListener('test', spy1, obj); + L.Util.stamp(obj); + obj.addEventListener('test', spy2, obj); + obj.addEventListener('test', spy3, obj2); // So that there is a contextId based listener, otherwise removeEventListener will do correct behaviour anyway + + obj.removeEventListener('test', spy1, obj); + obj.removeEventListener('test', spy2, obj); + obj.removeEventListener('test', spy3, obj2); + + obj.fireEvent('test'); + + expect(spy1.called).to.be(false); + expect(spy2.called).to.be(false); + expect(spy3.called).to.be(false); + }); + it('doesnt lose track of listeners when removing non existent ones', function () { var obj = new Klass(), spy = sinon.spy(), @@ -212,20 +235,42 @@ describe('Events', function() { expect(spy.called).to.be(false); }); - it('makes sure an event is not triggered if a listener is removed during dispatch',function() { + it('correctly removes all listeners if given no fn', function () { + var obj = new Klass(), + spy = sinon.spy(), + foo = {}, + foo2 = {}, + foo3 = {}; + + obj.addEventListener('test', spy, foo2); + obj.addEventListener('test', spy, foo3); + + obj.removeEventListener('test'); // Removes both of the above listeners + + expect(obj.hasEventListeners('test')).to.be(false); + + //Add and remove a listener + obj.addEventListener('test', spy, foo2); + obj.removeEventListener('test', spy, foo2); + + expect(obj.hasEventListeners('test')).to.be(false); + }); + + it('makes sure an event is not triggered if a listener is removed during dispatch', function () { var obj = new Klass(), spy = sinon.spy(); - obj.addEventListener('test', function() { obj.removeEventListener('test',spy); }); - obj.addEventListener('test', spy); - obj.fireEvent('test'); + + obj.addEventListener('test', function () { obj.removeEventListener('test', spy); }); + obj.addEventListener('test', spy); + obj.fireEvent('test'); expect(spy.called).to.be(false); }); }); - describe('#on, #off & #fire', function() { + describe('#on, #off & #fire', function () { - it('works like #addEventListener && #removeEventListener', function() { + it('works like #addEventListener && #removeEventListener', function () { var obj = new Klass(), spy = sinon.spy(); @@ -240,7 +285,7 @@ describe('Events', function() { expect(spy.callCount).to.be.lessThan(2); }); - it('does not override existing methods with the same name', function() { + it('does not override existing methods with the same name', function () { var spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy(); @@ -265,10 +310,10 @@ describe('Events', function() { }); }); - describe("#clearEventListeners", function() { - it("clears all registered listeners on an object", function() { + describe("#clearEventListeners", function () { + it("clears all registered listeners on an object", function () { var spy = sinon.spy(), - obj = new Klass() + obj = new Klass(), otherObj = new Klass(); obj.on('test', spy, obj); @@ -282,8 +327,8 @@ describe('Events', function() { }); }); - describe('#once', function() { - it('removes event listeners after first trigger', function() { + describe('#once', function () { + it('removes event listeners after first trigger', function () { var obj = new Klass(), spy = sinon.spy(); @@ -297,7 +342,7 @@ describe('Events', function() { expect(spy.callCount).to.be.lessThan(2); }); - it('works with an object hash', function() { + it('works with an object hash', function () { var obj = new Klass(), spy = sinon.spy(), otherSpy = sinon.spy(); @@ -332,7 +377,7 @@ describe('Events', function() { expect(spy.called).to.be(false); }); - it('works if called from a context that doesnt implement #Events', function() { + it('works if called from a context that doesnt implement #Events', function () { var obj = new Klass(), spy = sinon.spy(), foo = {}; diff --git a/spec/suites/core/UtilSpec.js b/spec/suites/core/UtilSpec.js index 62d487d9..f4b0b534 100644 --- a/spec/suites/core/UtilSpec.js +++ b/spec/suites/core/UtilSpec.js @@ -1,16 +1,16 @@ -describe('Util', function() { +describe('Util', function () { - describe('#extend', function() { + describe('#extend', function () { var a; - beforeEach(function() { + beforeEach(function () { a = { foo: 5, bar: 'asd' }; }); - it('extends the first argument with the properties of the second', function() { + it('extends the first argument with the properties of the second', function () { L.Util.extend(a, { bar: 7, baz: 3 @@ -23,7 +23,7 @@ describe('Util', function() { }); }); - it('accepts more than 2 arguments', function() { + it('accepts more than 2 arguments', function () { L.Util.extend(a, {bar: 7}, {baz: 3}); expect(a).to.eql({ @@ -34,9 +34,9 @@ describe('Util', function() { }); }); - describe('#bind', function() { - it('returns the given function with the given context', function() { - var fn = function() { + describe('#bind', function () { + it('returns the given function with the given context', function () { + var fn = function () { return this; }; @@ -59,8 +59,8 @@ describe('Util', function() { }); }); - describe('#stamp', function() { - it('sets a unique id on the given object and returns it', function() { + describe('#stamp', function () { + it('sets a unique id on the given object and returns it', function () { var a = {}, id = L.Util.stamp(a); @@ -115,38 +115,37 @@ describe('Util', function() { }); - describe('#getParamString', function() { - it('creates a valid query string for appending depending on url input', function() { + describe('#getParamString', function () { + it('creates a valid query string for appending depending on url input', function () { var a = { - url: "http://example.com/get", + url: 'http://example.com/get', obj: {bar: 7, baz: 3}, - result: "?bar=7&baz=3" + result: '?bar=7&baz=3' }; - expect(L.Util.getParamString(a.obj,a.url)).to.eql(a.result); + expect(L.Util.getParamString(a.obj, a.url)).to.eql(a.result); var b = { - url: "http://example.com/get?justone=qs", + url: 'http://example.com/get?justone=qs', obj: {bar: 7, baz: 3}, - result: "&bar=7&baz=3" + result: '&bar=7&baz=3' }; - expect(L.Util.getParamString(b.obj,b.url)).to.eql(b.result); + expect(L.Util.getParamString(b.obj, b.url)).to.eql(b.result); var c = { url: undefined, obj: {bar: 7, baz: 3}, - result: "?bar=7&baz=3" + result: '?bar=7&baz=3' }; - expect(L.Util.getParamString(c.obj,c.url)).to.eql(c.result); + expect(L.Util.getParamString(c.obj, c.url)).to.eql(c.result); }); }); describe('#requestAnimFrame', function () { it('calles a function on next frame, unless canceled', function (done) { var spy = sinon.spy(), - spy2 = sinon.spy(), foo = {}; L.Util.requestAnimFrame(spy); @@ -160,7 +159,7 @@ describe('Util', function() { }); }); - describe('#limitExecByInterval', function() { + describe('#limitExecByInterval', function () { it('limits execution to not more often than specified time interval', function (done) { var spy = sinon.spy(); @@ -189,24 +188,69 @@ describe('Util', function() { describe('#template', function () { it('evaluates templates with a given data object', function () { - var tpl = 'Hello {foo} and {bar}!'; + var tpl = 'Hello {foo} and {baz }!'; var str = L.Util.template(tpl, { foo: 'Vlad', - bar: 'Dave' + bar: 'Dave', + baz: function (o) { + return o.bar; + } }); expect(str).to.eql('Hello Vlad and Dave!'); }); + it('check the cache', function () { + var tpl = 'Hello {foo} and {baz }!'; + + var str = L.Util._templateCache[tpl]({ + foo: 'ladies', + baz: function () { + return 'gentlemen'; + } + }); + + expect(str).to.eql('Hello ladies and gentlemen!'); + }); + + it('evaluates templates with a function', function () { + var tpl = L.Util.compileTemplate('Hello { foo } and { bar}!', {}); + + var str1 = tpl({ + foo: 'Vlad', + bar: 'Dave' + }); + var str2 = tpl({ + foo: '{Calvin}', + bar: '{Simon}' + }); + + expect(str1).to.eql('Hello Vlad and Dave!'); + expect(str2).to.eql('Hello {Calvin} and {Simon}!'); + }); + it('does not modify text without a token variable', function () { expect(L.Util.template('foo', {})).to.eql('foo'); }); + it('supports templates with double quotes', function () { + expect(L.Util.template('He said: "{foo}"!', { + foo: 'Hello' + })).to.eql('He said: "Hello"!'); + }); + it('throws when a template token is not given', function () { expect(function () { - L.Util.template(tpl, {foo: 'bar'}); + L.Util.template(undefined, {foo: 'bar'}); }).to.throwError(); }); }); + + describe('#isArray', function () { + expect(L.Util.isArray([1, 2, 3])).to.be(true); + expect(L.Util.isArray(new Array(1, 2, 3))).to.be(true); + expect(L.Util.isArray('blabla')).to.be(false); + expect(L.Util.isArray({0: 1, 1: 2})).to.be(false); + }); }); diff --git a/spec/suites/dom/DomEventSpec.js b/spec/suites/dom/DomEventSpec.js index 120cdf09..97d586fe 100644 --- a/spec/suites/dom/DomEventSpec.js +++ b/spec/suites/dom/DomEventSpec.js @@ -1,10 +1,10 @@ -describe('DomEvent', function() { +describe('DomEvent', function () { var el; function simulateClick(el) { if (document.createEvent) { var e = document.createEvent('MouseEvents'); - e.initMouseEvent('click', true, true, window, + e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); return el.dispatchEvent(e); } else if (el.fireEvent) { @@ -12,19 +12,19 @@ describe('DomEvent', function() { } } - beforeEach(function() { + beforeEach(function () { el = document.createElement('div'); el.style.position = 'absolute'; el.style.top = el.style.left = '-10000px'; document.body.appendChild(el); }); - afterEach(function() { + afterEach(function () { document.body.removeChild(el); }); - describe('#addListener', function() { - it('adds a listener and calls it on event', function() { + describe('#addListener', function () { + it('adds a listener and calls it on event', function () { var listener1 = sinon.spy(), listener2 = sinon.spy(); @@ -37,11 +37,11 @@ describe('DomEvent', function() { expect(listener2.called).to.be.ok(); }); - it('binds "this" to the given context', function() { + it('binds "this" to the given context', function () { var obj = {foo: 'bar'}, result; - L.DomEvent.addListener(el, 'click', function() { + L.DomEvent.addListener(el, 'click', function () { result = this; }, obj); @@ -50,10 +50,10 @@ describe('DomEvent', function() { expect(result).to.eql(obj); }); - it('passes an event object to the listener', function() { + it('passes an event object to the listener', function () { var type; - L.DomEvent.addListener(el, 'click', function(e) { + L.DomEvent.addListener(el, 'click', function (e) { type = e && e.type; }); simulateClick(el); @@ -62,8 +62,8 @@ describe('DomEvent', function() { }); }); - describe('#removeListener', function() { - it('removes a previously added listener', function() { + describe('#removeListener', function () { + it('removes a previously added listener', function () { var listener = sinon.spy(); L.DomEvent.addListener(el, 'click', listener); @@ -75,8 +75,8 @@ describe('DomEvent', function() { }); }); - describe('#stopPropagation', function() { - it('stops propagation of the given event', function() { + describe('#stopPropagation', function () { + it('stops propagation of the given event', function () { var child = document.createElement('div'), listener = sinon.spy(); @@ -92,8 +92,8 @@ describe('DomEvent', function() { el.removeChild(child); }); }); - describe('#preventDefault', function() { - it('prevents the default action of event', function() { + describe('#preventDefault', function () { + it('prevents the default action of event', function () { L.DomEvent.addListener(el, 'click', L.DomEvent.preventDefault); expect(simulateClick(el)).to.be(false); diff --git a/spec/suites/dom/DomUtilSpec.js b/spec/suites/dom/DomUtilSpec.js index 7b3d2003..c6ffc825 100644 --- a/spec/suites/dom/DomUtilSpec.js +++ b/spec/suites/dom/DomUtilSpec.js @@ -1,30 +1,30 @@ -describe('DomUtil', function() { +describe('DomUtil', function () { var el; - beforeEach(function() { + beforeEach(function () { el = document.createElement('div'); el.style.position = 'absolute'; el.style.top = el.style.left = '-10000px'; document.body.appendChild(el); }); - afterEach(function() { + afterEach(function () { document.body.removeChild(el); }); - describe('#get', function() { - it('gets element by id if the given argument is string', function() { + describe('#get', function () { + it('gets element by id if the given argument is string', function () { el.id = 'testId'; expect(L.DomUtil.get(el.id)).to.eql(el); }); - it('returns the element if it is given as an argument', function() { + it('returns the element if it is given as an argument', function () { expect(L.DomUtil.get(el)).to.eql(el); }); }); - describe('#addClass, #removeClass, #hasClass', function() { - it('has defined class for test element', function() { + describe('#addClass, #removeClass, #hasClass', function () { + it('has defined class for test element', function () { el.className = 'bar foo baz '; expect(L.DomUtil.hasClass(el, 'foo')).to.be.ok(); expect(L.DomUtil.hasClass(el, 'bar')).to.be.ok(); @@ -32,7 +32,7 @@ describe('DomUtil', function() { expect(L.DomUtil.hasClass(el, 'boo')).to.not.be.ok(); }); - it('adds or removes the class', function() { + it('adds or removes the class', function () { el.className = ''; L.DomUtil.addClass(el, 'foo'); @@ -53,13 +53,6 @@ describe('DomUtil', function() { }); }); - describe('#documentIsLtr', function () { - it('returns true if doc direction is ltr', function () { - expect(L.DomUtil.documentIsLtr()).to.eql(true); - expect(L.DomUtil.documentIsLtr()).to.eql(true); // cached - }); - }); - describe('#getViewportOffset', function () { it('calculates the viewport offset of an element', function () { var div = document.createElement('div'); diff --git a/spec/suites/dom/PosAnimationSpec.js b/spec/suites/dom/PosAnimationSpec.js index 6cc2c0e2..9b4ebfeb 100644 --- a/spec/suites/dom/PosAnimationSpec.js +++ b/spec/suites/dom/PosAnimationSpec.js @@ -1,23 +1,23 @@ -describe('PosAnimation', function() { +describe('PosAnimation', function () { var el; - beforeEach(function() { + beforeEach(function () { el = document.createElement('div'); this.subject = new L.PosAnimation(); this.subject._el = el; }); - describe('#_onStep', function() { - it("sets element position and fires step event if it is able to get current position", function() { + describe('#_onStep', function () { + it("sets element position and fires step event if it is able to get current position", function () { var point = new L.Point(5, 5, true); sinon.stub(this.subject, '_getPos').returns(point); this.subject.fire = sinon.stub(); this.subject._onStep(); expect(this.subject.fire.withArgs('step').calledOnce).to.be(true); - expect(this.subject._el._leaflet_pos).to.be(point); + expect(L.DomUtil.getPosition(this.subject._el)).to.be(point); }); - it('stops transition if a position returned', function() { + it('stops transition if a position returned', function () { sinon.stub(this.subject, '_onTransitionEnd'); sinon.stub(this.subject, '_getPos').returns(undefined); this.subject._onStep(); diff --git a/spec/suites/geo/CRSSpec.js b/spec/suites/geo/CRSSpec.js new file mode 100644 index 00000000..5a7aee3c --- /dev/null +++ b/spec/suites/geo/CRSSpec.js @@ -0,0 +1,47 @@ +describe("CRS.EPSG3395", function () { + var crs = L.CRS.EPSG3395; + + describe("#latLngToPoint", function () { + it("projects a center point", function () { + expect(crs.latLngToPoint(L.latLng(0, 0), 0)).near(new L.Point(128, 128), 0.01); + }); + + it("projects the northeast corner of the world", function () { + expect(crs.latLngToPoint(L.latLng(85.0840591556, 180), 0)).near(new L.Point(256, 0)); + }); + }); + + describe("#pointToLatLng", function () { + it("reprojects a center point", function () { + expect(crs.pointToLatLng(new L.Point(128, 128), 0)).nearLatLng(L.latLng(0, 0), 0.01); + }); + + it("reprojects the northeast corner of the world", function () { + expect(crs.pointToLatLng(new L.Point(256, 0), 0)).nearLatLng(L.latLng(85.0840591556, 180)); + }); + }); +}); + +describe("CRS.EPSG3857", function () { + var crs = L.CRS.EPSG3857; + + describe("#latLngToPoint", function () { + it("projects a center point", function () { + expect(crs.latLngToPoint(L.latLng(0, 0), 0)).near(new L.Point(128, 128), 0.01); + }); + + it("projects the northeast corner of the world", function () { + expect(crs.latLngToPoint(L.latLng(85.0511287798, 180), 0)).near(new L.Point(256, 0)); + }); + }); + + describe("#pointToLatLng", function () { + it("reprojects a center point", function () { + expect(crs.pointToLatLng(new L.Point(128, 128), 0)).nearLatLng(L.latLng(0, 0), 0.01); + }); + + it("reprojects the northeast corner of the world", function () { + expect(crs.pointToLatLng(new L.Point(256, 0), 0)).nearLatLng(L.latLng(85.0511287798, 180)); + }); + }); +}); diff --git a/spec/suites/geo/LatLngBoundsSpec.js b/spec/suites/geo/LatLngBoundsSpec.js index ebfceab3..ef01699d 100644 --- a/spec/suites/geo/LatLngBoundsSpec.js +++ b/spec/suites/geo/LatLngBoundsSpec.js @@ -1,7 +1,7 @@ -describe('LatLngBounds', function() { +describe('LatLngBounds', function () { var a, c; - beforeEach(function() { + beforeEach(function () { a = new L.LatLngBounds( new L.LatLng(14, 12), new L.LatLng(30, 40)); @@ -27,13 +27,17 @@ describe('LatLngBounds', function() { it('extends the bounds by given bounds', function () { a.extend([[20, 50], [8, 40]]); - expect(a.getSouthEast()).to.eql(new L.LatLng(8, 50)); }); it('extends the bounds by undefined', function () { expect(a.extend()).to.eql(a); }); + + it('extends the bounds by raw object', function () { + a.extend({lat: 20, lng: 50}); + expect(a.getNorthEast()).to.eql(new L.LatLng(30, 50)); + }); }); describe('#getCenter', function () { @@ -58,62 +62,62 @@ describe('LatLngBounds', function() { }); }); - describe('#isValid', function() { - it('returns true if properly set up', function() { + describe('#isValid', function () { + it('returns true if properly set up', function () { expect(a.isValid()).to.be.ok(); }); - it('returns false if is invalid', function() { + it('returns false if is invalid', function () { expect(c.isValid()).to.not.be.ok(); }); - it('returns true if extended', function() { + it('returns true if extended', function () { c.extend([0, 0]); expect(c.isValid()).to.be.ok(); }); }); describe('#getWest', function () { - it('returns a proper bbox west value', function() { + it('returns a proper bbox west value', function () { expect(a.getWest()).to.eql(12); }); }); describe('#getSouth', function () { - it('returns a proper bbox south value', function() { + it('returns a proper bbox south value', function () { expect(a.getSouth()).to.eql(14); }); }); describe('#getEast', function () { - it('returns a proper bbox east value', function() { + it('returns a proper bbox east value', function () { expect(a.getEast()).to.eql(40); }); }); describe('#getNorth', function () { - it('returns a proper bbox north value', function() { + it('returns a proper bbox north value', function () { expect(a.getNorth()).to.eql(30); }); }); describe('#toBBoxString', function () { - it('returns a proper left,bottom,right,top bbox', function() { + it('returns a proper left,bottom,right,top bbox', function () { expect(a.toBBoxString()).to.eql("12,14,40,30"); }); }); describe('#getNorthWest', function () { - it('returns a proper north-west LatLng', function() { + it('returns a proper north-west LatLng', function () { expect(a.getNorthWest()).to.eql(new L.LatLng(a.getNorth(), a.getWest())); }); }); describe('#getSouthEast', function () { - it('returns a proper south-east LatLng', function() { + it('returns a proper south-east LatLng', function () { expect(a.getSouthEast()).to.eql(new L.LatLng(a.getSouth(), a.getEast())); }); }); diff --git a/spec/suites/geo/LatLngSpec.js b/spec/suites/geo/LatLngSpec.js index aefea668..bc7c1a80 100644 --- a/spec/suites/geo/LatLngSpec.js +++ b/spec/suites/geo/LatLngSpec.js @@ -1,6 +1,6 @@ -describe('LatLng', function() { - describe('constructor', function() { - it("sets lat and lng", function() { +describe('LatLng', function () { + describe('constructor', function () { + it("sets lat and lng", function () { var a = new L.LatLng(25, 74); expect(a.lat).to.eql(25); expect(a.lng).to.eql(74); @@ -15,16 +15,30 @@ describe('LatLng', function() { var a = new L.LatLng(NaN, NaN); }).to.throwError(); }); + + it('does not set altitude if undefined', function () { + var a = new L.LatLng(25, 74); + expect(typeof a.alt).to.eql('undefined'); + }); + + it('sets altitude', function () { + var a = new L.LatLng(25, 74, 50); + expect(a.alt).to.eql(50); + + var b = new L.LatLng(-25, -74, -50); + expect(b.alt).to.eql(-50); + }); + }); - describe('#equals', function() { - it("returns true if compared objects are equal within a certain margin", function() { + describe('#equals', function () { + it("returns true if compared objects are equal within a certain margin", function () { var a = new L.LatLng(10, 20); var b = new L.LatLng(10 + 1.0E-10, 20 - 1.0E-10); expect(a.equals(b)).to.eql(true); }); - it("returns false if compared objects are not equal within a certain margin", function() { + it("returns false if compared objects are not equal within a certain margin", function () { var a = new L.LatLng(10, 20); var b = new L.LatLng(10, 23.3); expect(a.equals(b)).to.eql(false); @@ -37,7 +51,7 @@ describe('LatLng', function() { }); describe('#wrap', function () { - it("wraps longitude to lie between -180 and 180 by default", function() { + it("wraps longitude to lie between -180 and 180 by default", function () { var a = new L.LatLng(0, 190).wrap().lng; expect(a).to.eql(-170); @@ -63,7 +77,7 @@ describe('LatLng', function() { expect(h).to.eql(180); }); - it("wraps longitude within the given range", function() { + it("wraps longitude within the given range", function () { var a = new L.LatLng(0, 190).wrap(-100, 100).lng; expect(a).to.eql(-10); }); diff --git a/spec/suites/geo/ProjectionSpec.js b/spec/suites/geo/ProjectionSpec.js index 2414bfa5..41c469d7 100644 --- a/spec/suites/geo/ProjectionSpec.js +++ b/spec/suites/geo/ProjectionSpec.js @@ -1,47 +1,39 @@ -describe("Projection.Mercator", function() { +describe("Projection.Mercator", function () { var p = L.Projection.Mercator; - expect.Assertion.prototype.near = function(expected, delta) { - delta = delta || 1; - expect(this.obj.x).to - .be.within(expected.x - delta, expected.x + delta); - expect(this.obj.y).to - .be.within(expected.y - delta, expected.y + delta); - }; - - describe("#project", function() { - it("projects a center point", function() { + describe("#project", function () { + it("projects a center point", function () { //edge cases expect(p.project(new L.LatLng(0, 0))).near(new L.Point(0, 0)); }); - it("projects the northeast corner of the world", function() { + it("projects the northeast corner of the world", function () { expect(p.project(new L.LatLng(90, 180))).near(new L.Point(20037508, 20037508)); }); - it("projects the southwest corner of the world", function() { + it("projects the southwest corner of the world", function () { expect(p.project(new L.LatLng(-90, -180))).near(new L.Point(-20037508, -20037508)); }); - it("projects other points", function() { - expect(p.project(new L.LatLng(50, 30))).near(new L.Point(3339584, 6413524)); + it("projects other points", function () { + expect(p.project(new L.LatLng(50, 30))).near(new L.Point(3339584, 6413524)); - // from https://github.com/Leaflet/Leaflet/issues/1578 - expect(p.project(new L.LatLng(51.9371170300465, 80.11230468750001))) - .near(new L.Point(8918060.964088084, 6755099.410887127)); + // from https://github.com/Leaflet/Leaflet/issues/1578 + expect(p.project(new L.LatLng(51.9371170300465, 80.11230468750001))) + .near(new L.Point(8918060.964088084, 6755099.410887127)); }); }); - describe("#unproject", function() { + describe("#unproject", function () { function pr(point) { return p.project(p.unproject(point)); } - it("unprojects a center point", function() { + it("unprojects a center point", function () { expect(pr(new L.Point(0, 0))).near(new L.Point(0, 0)); }); - it("unprojects pi points", function() { + it("unprojects pi points", function () { expect(pr(new L.Point(-Math.PI, Math.PI))).near(new L.Point(-Math.PI, Math.PI)); expect(pr(new L.Point(-Math.PI, -Math.PI))).near(new L.Point(-Math.PI, -Math.PI)); diff --git a/spec/suites/geometry/BoundsSpec.js b/spec/suites/geometry/BoundsSpec.js index 38102a6b..7a048f3c 100644 --- a/spec/suites/geometry/BoundsSpec.js +++ b/spec/suites/geometry/BoundsSpec.js @@ -1,7 +1,7 @@ -describe('Bounds', function() { +describe('Bounds', function () { var a, b, c; - beforeEach(function() { + beforeEach(function () { a = new L.Bounds( new L.Point(14, 12), new L.Point(30, 40)); @@ -13,19 +13,19 @@ describe('Bounds', function() { c = new L.Bounds(); }); - describe('constructor', function() { - it('creates bounds with proper min & max on (Point, Point)', function() { + describe('constructor', function () { + it('creates bounds with proper min & max on (Point, Point)', function () { expect(a.min).to.eql(new L.Point(14, 12)); expect(a.max).to.eql(new L.Point(30, 40)); }); - it('creates bounds with proper min & max on (Point[])', function() { + it('creates bounds with proper min & max on (Point[])', function () { expect(b.min).to.eql(new L.Point(14, 12)); expect(b.max).to.eql(new L.Point(30, 40)); }); }); - describe('#extend', function() { - it('extends the bounds to contain the given point', function() { + describe('#extend', function () { + it('extends the bounds to contain the given point', function () { a.extend(new L.Point(50, 20)); expect(a.min).to.eql(new L.Point(14, 12)); expect(a.max).to.eql(new L.Point(50, 40)); @@ -36,14 +36,14 @@ describe('Bounds', function() { }); }); - describe('#getCenter', function() { - it('returns the center point', function() { + describe('#getCenter', function () { + it('returns the center point', function () { expect(a.getCenter()).to.eql(new L.Point(22, 26)); }); }); - describe('#contains', function() { - it('contains other bounds or point', function() { + describe('#contains', function () { + it('contains other bounds or point', function () { a.extend(new L.Point(50, 10)); expect(a.contains(b)).to.be.ok(); expect(b.contains(a)).to.not.be.ok(); @@ -52,14 +52,14 @@ describe('Bounds', function() { }); }); - describe('#isValid', function() { - it('returns true if properly set up', function() { + describe('#isValid', function () { + it('returns true if properly set up', function () { expect(a.isValid()).to.be.ok(); }); - it('returns false if is invalid', function() { + it('returns false if is invalid', function () { expect(c.isValid()).to.not.be.ok(); }); - it('returns true if extended', function() { + it('returns true if extended', function () { c.extend([0, 0]); expect(c.isValid()).to.be.ok(); }); diff --git a/spec/suites/geometry/PointSpec.js b/spec/suites/geometry/PointSpec.js index 5a903aeb..f11180b1 100644 --- a/spec/suites/geometry/PointSpec.js +++ b/spec/suites/geometry/PointSpec.js @@ -1,42 +1,42 @@ -describe("Point", function() { +describe("Point", function () { - describe('constructor', function() { + describe('constructor', function () { - it("creates a point with the given x and y", function() { + it("creates a point with the given x and y", function () { var p = new L.Point(1.5, 2.5); expect(p.x).to.eql(1.5); expect(p.y).to.eql(2.5); }); - it("rounds the given x and y if the third argument is true", function() { + it("rounds the given x and y if the third argument is true", function () { var p = new L.Point(1.3, 2.7, true); expect(p.x).to.eql(1); expect(p.y).to.eql(3); }); }); - describe('#subtract', function() { - it('subtracts the given point from this one', function() { + describe('#subtract', function () { + it('subtracts the given point from this one', function () { var a = new L.Point(50, 30), b = new L.Point(20, 10); expect(a.subtract(b)).to.eql(new L.Point(30, 20)); }); }); - describe('#add', function() { - it('adds given point to this one', function() { + describe('#add', function () { + it('adds given point to this one', function () { expect(new L.Point(50, 30).add(new L.Point(20, 10))).to.eql(new L.Point(70, 40)); }); }); - describe('#divideBy', function() { - it('divides this point by the given amount', function() { + describe('#divideBy', function () { + it('divides this point by the given amount', function () { expect(new L.Point(50, 30).divideBy(5)).to.eql(new L.Point(10, 6)); }); }); - describe('#multiplyBy', function() { - it('multiplies this point by the given amount', function() { + describe('#multiplyBy', function () { + it('multiplies this point by the given amount', function () { expect(new L.Point(50, 30).multiplyBy(2)).to.eql(new L.Point(100, 60)); }); }); diff --git a/spec/suites/geometry/TransformationSpec.js b/spec/suites/geometry/TransformationSpec.js index 46746ad7..c244459a 100644 --- a/spec/suites/geometry/TransformationSpec.js +++ b/spec/suites/geometry/TransformationSpec.js @@ -1,13 +1,13 @@ -describe("Transformation", function() { +describe("Transformation", function () { var t, p; - beforeEach(function() { + beforeEach(function () { t = new L.Transformation(1, 2, 3, 4); p = new L.Point(10, 20); }); describe('#transform', function () { - it("performs a transformation", function() { + it("performs a transformation", function () { var p2 = t.transform(p, 2); expect(p2).to.eql(new L.Point(24, 128)); }); @@ -18,7 +18,7 @@ describe("Transformation", function() { }); describe('#untransform', function () { - it("performs a reverse transformation", function() { + it("performs a reverse transformation", function () { var p2 = t.transform(p, 2); var p3 = t.untransform(p2, 2); expect(p3).to.eql(p); diff --git a/spec/suites/layer/FeatureGroupSpec.js b/spec/suites/layer/FeatureGroupSpec.js index fd875e8d..b6a735d8 100644 --- a/spec/suites/layer/FeatureGroupSpec.js +++ b/spec/suites/layer/FeatureGroupSpec.js @@ -17,22 +17,25 @@ fg1.addLayer(marker); fg2.addLayer(marker); - var wasClicked = 0; - fg2.on('click', function(e) { + var wasClicked1, + wasClicked2; + + fg2.on('click', function (e) { expect(e.layer).to.be(marker); expect(e.target).to.be(fg2); - wasClicked |= 1; + wasClicked2 = true; }); fg1.on('click', function (e) { expect(e.layer).to.be(marker); expect(e.target).to.be(fg1); - wasClicked |= 2; + wasClicked1 = true; }); marker.fire('click', { type: 'click' }); - expect(wasClicked).to.be(3); + expect(wasClicked1).to.be(true); + expect(wasClicked2).to.be(true); }); }); }); @@ -47,6 +50,16 @@ expect(fg.hasLayer(marker)).to.be(true); }); + it('supports non-evented layers', function () { + var fg = L.featureGroup(), + g = L.layerGroup(); + + expect(fg.hasLayer(g)).to.be(false); + + fg.addLayer(g); + + expect(fg.hasLayer(g)).to.be(true); + }); }); describe('removeLayer', function () { it('removes the layer passed to it', function () { @@ -66,7 +79,7 @@ fg.addLayer(marker); expect(fg.hasLayer(marker)).to.be(true); - fg.removeLayer(marker._leaflet_id); + fg.removeLayer(L.stamp(marker)); expect(fg.hasLayer(marker)).to.be(false); }); }); diff --git a/spec/suites/layer/GeoJSONSpec.js b/spec/suites/layer/GeoJSONSpec.js index 3ffc990b..0f326dd8 100644 --- a/spec/suites/layer/GeoJSONSpec.js +++ b/spec/suites/layer/GeoJSONSpec.js @@ -5,7 +5,7 @@ describe("L.GeoJSON", function () { properties: {}, geometry: { type: 'Point', - coordinates: [20, 10] + coordinates: [20, 10, 5] } }; @@ -24,47 +24,79 @@ describe("L.GeoJSON", function () { }); describe("L.Marker#toGeoJSON", function () { - it("returns a Point object", function () { + it("returns a 2D Point object", function () { var marker = new L.Marker([10, 20]); expect(marker.toGeoJSON().geometry).to.eql({ type: 'Point', coordinates: [20, 10] }); }); + + it("returns a 3D Point object", function () { + var marker = new L.Marker([10, 20, 30]); + expect(marker.toGeoJSON().geometry).to.eql({ + type: 'Point', + coordinates: [20, 10, 30] + }); + }); }); describe("L.Circle#toGeoJSON", function () { - it("returns a Point object", function () { + it("returns a 2D Point object", function () { var circle = new L.Circle([10, 20], 100); expect(circle.toGeoJSON().geometry).to.eql({ type: 'Point', coordinates: [20, 10] }); }); + + it("returns a 3D Point object", function () { + var circle = new L.Circle([10, 20, 30], 100); + expect(circle.toGeoJSON().geometry).to.eql({ + type: 'Point', + coordinates: [20, 10, 30] + }); + }); }); describe("L.CircleMarker#toGeoJSON", function () { - it("returns a Point object", function () { + it("returns a 2D Point object", function () { var marker = new L.CircleMarker([10, 20]); expect(marker.toGeoJSON().geometry).to.eql({ type: 'Point', coordinates: [20, 10] }); }); + + it("returns a 3D Point object", function () { + var marker = new L.CircleMarker([10, 20, 30]); + expect(marker.toGeoJSON().geometry).to.eql({ + type: 'Point', + coordinates: [20, 10, 30] + }); + }); }); describe("L.Polyline#toGeoJSON", function () { - it("returns a LineString object", function () { + it("returns a 2D LineString object", function () { var polyline = new L.Polyline([[10, 20], [2, 5]]); expect(polyline.toGeoJSON().geometry).to.eql({ type: 'LineString', coordinates: [[20, 10], [5, 2]] }); }); + + it("returns a 3D LineString object", function () { + var polyline = new L.Polyline([[10, 20, 30], [2, 5, 10]]); + expect(polyline.toGeoJSON().geometry).to.eql({ + type: 'LineString', + coordinates: [[20, 10, 30], [5, 2, 10]] + }); + }); }); describe("L.MultiPolyline#toGeoJSON", function () { - it("returns a MultiLineString object", function () { + it("returns a 2D MultiLineString object", function () { var multiPolyline = new L.MultiPolyline([[[10, 20], [2, 5]], [[1, 2], [3, 4]]]); expect(multiPolyline.toGeoJSON().geometry).to.eql({ type: 'MultiLineString', @@ -74,10 +106,21 @@ describe("L.MultiPolyline#toGeoJSON", function () { ] }); }); + + it("returns a 3D MultiLineString object", function () { + var multiPolyline = new L.MultiPolyline([[[10, 20, 30], [2, 5, 10]], [[1, 2, 3], [4, 5, 6]]]); + expect(multiPolyline.toGeoJSON().geometry).to.eql({ + type: 'MultiLineString', + coordinates: [ + [[20, 10, 30], [5, 2, 10]], + [[2, 1, 3], [5, 4, 6]] + ] + }); + }); }); describe("L.Polygon#toGeoJSON", function () { - it("returns a Polygon object (no holes)", function () { + it("returns a 2D Polygon object (no holes)", function () { var polygon = new L.Polygon([[1, 2], [3, 4], [5, 6]]); expect(polygon.toGeoJSON().geometry).to.eql({ type: 'Polygon', @@ -85,7 +128,15 @@ describe("L.Polygon#toGeoJSON", function () { }); }); - it("returns a Polygon object (with holes)", function () { + it("returns a 3D Polygon object (no holes)", function () { + var polygon = new L.Polygon([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); + expect(polygon.toGeoJSON().geometry).to.eql({ + type: 'Polygon', + coordinates: [[[2, 1, 3], [5, 4, 6], [8, 7, 9], [2, 1, 3]]] + }); + }); + + it("returns a 2D Polygon object (with holes)", function () { var polygon = new L.Polygon([[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10], [11, 12]]]); expect(polygon.toGeoJSON().geometry).to.eql({ type: 'Polygon', @@ -95,10 +146,21 @@ describe("L.Polygon#toGeoJSON", function () { ] }); }); + + it("returns a 3D Polygon object (with holes)", function () { + var polygon = new L.Polygon([[[1, 2, 3], [4, 5, 6], [7, 8, 9]], [[10, 11, 12], [13, 14, 15], [16, 17, 18]]]); + expect(polygon.toGeoJSON().geometry).to.eql({ + type: 'Polygon', + coordinates: [ + [[2, 1, 3], [5, 4, 6], [8, 7, 9], [2, 1, 3]], + [[11, 10, 12], [14, 13, 15], [17, 16, 18], [11, 10, 12]] + ] + }); + }); }); describe("L.MultiPolygon#toGeoJSON", function () { - it("returns a MultiPolygon object", function () { + it("returns a 2D MultiPolygon object", function () { var multiPolygon = new L.MultiPolygon([[[1, 2], [3, 4], [5, 6]]]); expect(multiPolygon.toGeoJSON().geometry).to.eql({ type: 'MultiPolygon', @@ -107,10 +169,20 @@ describe("L.MultiPolygon#toGeoJSON", function () { ] }); }); + + it("returns a 3D MultiPolygon object", function () { + var multiPolygon = new L.MultiPolygon([[[1, 2, 3], [4, 5, 6], [7, 8, 9]]]); + expect(multiPolygon.toGeoJSON().geometry).to.eql({ + type: 'MultiPolygon', + coordinates: [ + [[[2, 1, 3], [5, 4, 6], [8, 7, 9], [2, 1, 3]]] + ] + }); + }); }); describe("L.LayerGroup#toGeoJSON", function () { - it("returns a FeatureCollection object", function () { + it("returns a 2D FeatureCollection object", function () { var marker = new L.Marker([10, 20]), polyline = new L.Polyline([[10, 20], [2, 5]]), layerGroup = new L.LayerGroup([marker, polyline]); @@ -120,6 +192,16 @@ describe("L.LayerGroup#toGeoJSON", function () { }); }); + it("returns a 3D FeatureCollection object", function () { + var marker = new L.Marker([10, 20, 30]), + polyline = new L.Polyline([[10, 20, 30], [2, 5, 10]]), + layerGroup = new L.LayerGroup([marker, polyline]); + expect(layerGroup.toGeoJSON()).to.eql({ + type: 'FeatureCollection', + features: [marker.toGeoJSON(), polyline.toGeoJSON()] + }); + }); + it("ensures that every member is a Feature", function () { var tileLayer = new L.TileLayer(), layerGroup = new L.LayerGroup([tileLayer]); @@ -128,7 +210,7 @@ describe("L.LayerGroup#toGeoJSON", function () { return { type: 'Point', coordinates: [20, 10] - } + }; }; expect(layerGroup.toGeoJSON()).to.eql({ @@ -144,6 +226,51 @@ describe("L.LayerGroup#toGeoJSON", function () { }); }); + it('roundtrips GeometryCollection features', function () { + var json = { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "GeometryCollection", + "geometries": [{ + "type": "LineString", + "coordinates": [[-122.4425587930444, 37.80666418607323], [-122.4428379594768, 37.80663578323093]] + }, { + "type": "LineString", + "coordinates": [ + [-122.4425509770566, 37.80662588061205], + [-122.4428340530617, 37.8065999493009] + ] + }] + }, + "properties": { + "name": "SF Marina Harbor Master" + } + }] + }; + + expect(L.geoJson(json).toGeoJSON()).to.eql(json); + }); + + it('roundtrips MiltiPoint features', function () { + var json = { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "MultiPoint", + "coordinates": [[-122.4425587930444, 37.80666418607323], [-122.4428379594768, 37.80663578323093]] + }, + "properties": { + "name": "Test MultiPoints" + } + }] + }; + + expect(L.geoJson(json).toGeoJSON()).to.eql(json); + }); + it("omits layers which do not implement toGeoJSON", function () { var tileLayer = new L.TileLayer(), layerGroup = new L.LayerGroup([tileLayer]); diff --git a/spec/suites/layer/LayerGroupSpec.js b/spec/suites/layer/LayerGroupSpec.js index 54c30cb2..98ce5e2b 100644 --- a/spec/suites/layer/LayerGroupSpec.js +++ b/spec/suites/layer/LayerGroupSpec.js @@ -1,6 +1,6 @@ describe('LayerGroup', function () { describe("#addLayer", function () { - it('adds a layer', function() { + it('adds a layer', function () { var lg = L.layerGroup(), marker = L.marker([0, 0]); @@ -10,7 +10,7 @@ }); }); describe("#removeLayer", function () { - it('removes a layer', function() { + it('removes a layer', function () { var lg = L.layerGroup(), marker = L.marker([0, 0]); @@ -21,7 +21,7 @@ }); }); describe("#clearLayers", function () { - it('removes all layers', function() { + it('removes all layers', function () { var lg = L.layerGroup(), marker = L.marker([0, 0]); @@ -32,7 +32,7 @@ }); }); describe("#getLayers", function () { - it('gets all layers', function() { + it('gets all layers', function () { var lg = L.layerGroup(), marker = L.marker([0, 0]); @@ -42,14 +42,14 @@ }); }); describe("#eachLayer", function () { - it('iterates over all layers', function() { + it('iterates over all layers', function () { var lg = L.layerGroup(), marker = L.marker([0, 0]), ctx = { foo: 'bar' }; lg.addLayer(marker); - lg.eachLayer(function(layer) { + lg.eachLayer(function (layer) { expect(layer).to.eql(marker); expect(this).to.eql(ctx); }, ctx); diff --git a/spec/suites/layer/PopupSpec.js b/spec/suites/layer/PopupSpec.js index 586f665b..cef554f6 100644 --- a/spec/suites/layer/PopupSpec.js +++ b/spec/suites/layer/PopupSpec.js @@ -1,4 +1,4 @@ -describe('Popup', function() { +describe('Popup', function () { var c, map; @@ -10,7 +10,7 @@ describe('Popup', function() { map.setView(new L.LatLng(55.8, 37.6), 6); }); - it("closes on map click when map has closePopupOnClick option", function() { + it("closes on map click when map has closePopupOnClick option", function () { map.options.closePopupOnClick = true; var popup = new L.Popup() @@ -22,7 +22,7 @@ describe('Popup', function() { expect(map.hasLayer(popup)).to.be(false); }); - it("closes on map click when popup has closeOnClick option", function() { + it("closes on map click when popup has closeOnClick option", function () { map.options.closePopupOnClick = false; var popup = new L.Popup({closeOnClick: true}) @@ -34,7 +34,7 @@ describe('Popup', function() { expect(map.hasLayer(popup)).to.be(false); }); - it("does not close on map click when popup has closeOnClick: false option", function() { + it("does not close on map click when popup has closeOnClick: false option", function () { map.options.closePopupOnClick = true; var popup = new L.Popup({closeOnClick: false}) @@ -46,7 +46,7 @@ describe('Popup', function() { expect(map.hasLayer(popup)).to.be(true); }); - it("toggles its visibility when marker is clicked", function() { + it("toggles its visibility when marker is clicked", function () { var marker = new L.Marker(new L.LatLng(55.8, 37.6)); map.addLayer(marker); @@ -70,7 +70,7 @@ describe('Popup', function() { marker.closePopup.restore(); }); - it("should trigger popupopen on marker when popup opens", function() { + it("should trigger popupopen on marker when popup opens", function () { var marker1 = new L.Marker(new L.LatLng(55.8, 37.6)); var marker2 = new L.Marker(new L.LatLng(57.123076977278, 44.861962891635)); @@ -91,7 +91,7 @@ describe('Popup', function() { expect(spy.called).to.be(true); }); - it("should trigger popupclose on marker when popup closes", function() { + it("should trigger popupclose on marker when popup closes", function () { var marker1 = new L.Marker(new L.LatLng(55.8, 37.6)); var marker2 = new L.Marker(new L.LatLng(57.123076977278, 44.861962891635)); diff --git a/spec/suites/layer/TileLayerSpec.js b/spec/suites/layer/TileLayerSpec.js index 64a979c1..ab00e920 100644 --- a/spec/suites/layer/TileLayerSpec.js +++ b/spec/suites/layer/TileLayerSpec.js @@ -11,7 +11,8 @@ describe('TileLayer', function () { it("has the same zoomlevels as the tilelayer", function () { var maxZoom = 10, minZoom = 5; - map.setView([0, 0], 1); + + map.setView([0, 0], 1); L.tileLayer(tileUrl, { maxZoom: maxZoom, @@ -36,33 +37,32 @@ describe('TileLayer', function () { it("has its zoomlevels updated to fit the new layer", function () { map.setView([0, 0], 1); - L.tileLayer(tileUrl, { minZoom:10, maxZoom: 15 }).addTo(map); + L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 15}).addTo(map); expect(map.getMinZoom()).to.be(10); expect(map.getMaxZoom()).to.be(15); - L.tileLayer(tileUrl, { minZoom:5, maxZoom: 10 }).addTo(map); + L.tileLayer(tileUrl, {minZoom: 5, maxZoom: 10}).addTo(map); expect(map.getMinZoom()).to.be(5); // changed expect(map.getMaxZoom()).to.be(15); // unchanged - - L.tileLayer(tileUrl,{ minZoom:10, maxZoom: 20 }).addTo(map); + L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 20}).addTo(map); expect(map.getMinZoom()).to.be(5); // unchanged expect(map.getMaxZoom()).to.be(20); // changed - L.tileLayer(tileUrl, { minZoom:0, maxZoom: 25 }).addTo(map); + L.tileLayer(tileUrl, {minZoom: 0, maxZoom: 25}).addTo(map); expect(map.getMinZoom()).to.be(0); // changed expect(map.getMaxZoom()).to.be(25); // changed }); }); describe("when a tilelayer is removed from a map", function () { it("has its zoomlevels updated to only fit the layers it currently has", function () { - var tiles = [ L.tileLayer(tileUrl, { minZoom:10, maxZoom: 15 }).addTo(map), - L.tileLayer(tileUrl, { minZoom:5, maxZoom: 10 }).addTo(map), - L.tileLayer(tileUrl, { minZoom:10, maxZoom: 20 }).addTo(map), - L.tileLayer(tileUrl, { minZoom:0, maxZoom: 25 }).addTo(map) + var tiles = [ L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 15}).addTo(map), + L.tileLayer(tileUrl, {minZoom: 5, maxZoom: 10}).addTo(map), + L.tileLayer(tileUrl, {minZoom: 10, maxZoom: 20}).addTo(map), + L.tileLayer(tileUrl, {minZoom: 0, maxZoom: 25}).addTo(map) ]; - map.whenReady(function() { + map.whenReady(function () { expect(map.getMinZoom()).to.be(0); expect(map.getMaxZoom()).to.be(25); diff --git a/spec/suites/layer/marker/MarkerSpec.js b/spec/suites/layer/marker/MarkerSpec.js index 082de250..86117c9f 100644 --- a/spec/suites/layer/marker/MarkerSpec.js +++ b/spec/suites/layer/marker/MarkerSpec.js @@ -1,31 +1,39 @@ describe("Marker", function () { var map, - spy; + spy, + icon1, + icon2; + beforeEach(function () { map = L.map(document.createElement('div')).setView([0, 0], 0); + icon1 = new L.Icon.Default(); + icon2 = new L.Icon.Default({ + iconUrl: icon1._getIconUrl('icon') + '?2', + shadowUrl: icon1._getIconUrl('shadow') + '?2' + }); }); describe("#setIcon", function () { it("changes the icon to another image", function () { - var marker = new L.Marker([0, 0], {icon: new L.Icon({iconUrl: 'icon1.png' }) }); + var marker = new L.Marker([0, 0], {icon: icon1}); map.addLayer(marker); - + var beforeIcon = marker._icon; - marker.setIcon(new L.Icon({iconUrl: 'icon2.png' })); + marker.setIcon(icon2); var afterIcon = marker._icon; - + expect(beforeIcon).to.be(afterIcon); - expect(afterIcon.src).to.contain('icon2.png'); + expect(afterIcon.src).to.contain(icon2._getIconUrl('icon')); }); it("changes the icon to another DivIcon", function () { var marker = new L.Marker([0, 0], {icon: new L.DivIcon({html: 'Inner1Text' }) }); map.addLayer(marker); - + var beforeIcon = marker._icon; marker.setIcon(new L.DivIcon({html: 'Inner2Text' })); var afterIcon = marker._icon; - + expect(beforeIcon).to.be(afterIcon); expect(afterIcon.innerHTML).to.contain('Inner2Text'); }); @@ -33,10 +41,10 @@ describe("Marker", function () { it("removes text when changing to a blank DivIcon", function () { var marker = new L.Marker([0, 0], {icon: new L.DivIcon({html: 'Inner1Text' }) }); map.addLayer(marker); - + marker.setIcon(new L.DivIcon()); var afterIcon = marker._icon; - + expect(marker._icon.innerHTML).to.not.contain('Inner1Text'); }); @@ -44,37 +52,37 @@ describe("Marker", function () { var marker = new L.Marker([0, 0], {icon: new L.DivIcon({html: 'Inner1Text' }) }); map.addLayer(marker); var oldIcon = marker._icon; - - marker.setIcon(new L.Icon({iconUrl: 'icon1.png' })); - + + marker.setIcon(icon1); + expect(oldIcon).to.not.be(marker._icon); expect(oldIcon.parentNode).to.be(null); - - expect(marker._icon.src).to.contain('icon1.png'); + + expect(marker._icon.src).to.contain('marker-icon.png'); expect(marker._icon.parentNode).to.be(map._panes.markerPane); }); it("changes an image to a DivIcon", function () { - var marker = new L.Marker([0, 0], {icon: new L.Icon({iconUrl: 'icon1.png' }) }); + var marker = new L.Marker([0, 0], {icon: icon1}); map.addLayer(marker); var oldIcon = marker._icon; marker.setIcon(new L.DivIcon({html: 'Inner1Text' })); - + expect(oldIcon).to.not.be(marker._icon); expect(oldIcon.parentNode).to.be(null); - + expect(marker._icon.innerHTML).to.contain('Inner1Text'); expect(marker._icon.parentNode).to.be(map._panes.markerPane); }); it("reuses the icon/shadow when changing icon", function () { - var marker = new L.Marker([0, 0], { icon: new L.Icon({ iconUrl: 'icon1.png', shadowUrl: 'shadow1.png', }) }); + var marker = new L.Marker([0, 0], { icon: icon1}); map.addLayer(marker); var oldIcon = marker._icon; var oldShadow = marker._shadow; - marker.setIcon(new L.Icon({ iconUrl: 'icon2.png', shadowUrl: 'shadow2.png', })); + marker.setIcon(icon2); expect(oldIcon).to.be(marker._icon); expect(oldShadow).to.be(marker._shadow); @@ -83,4 +91,4 @@ describe("Marker", function () { expect(marker._shadow.parentNode).to.be(map._panes.shadowPane); }); }); -}); \ No newline at end of file +}); diff --git a/spec/suites/layer/vector/CircleMarkerSpec.js b/spec/suites/layer/vector/CircleMarkerSpec.js index d64a31c7..81a712ed 100644 --- a/spec/suites/layer/vector/CircleMarkerSpec.js +++ b/spec/suites/layer/vector/CircleMarkerSpec.js @@ -1,13 +1,13 @@ -describe('CircleMarker', function() { - describe("#_radius", function() { +describe('CircleMarker', function () { + describe("#_radius", function () { var map; - beforeEach(function() { + beforeEach(function () { map = L.map(document.createElement('div')); map.setView([0, 0], 1); }); - describe("when a CircleMarker is added to the map ", function() { - describe("with a radius set as an option", function() { - it("takes that radius", function() { + describe("when a CircleMarker is added to the map ", function () { + describe("with a radius set as an option", function () { + it("takes that radius", function () { var marker = L.circleMarker([0, 0], { radius: 20 }).addTo(map); expect(marker._radius).to.be(20); @@ -33,7 +33,7 @@ }); describe("and setStyle is used to change the radius after adding", function () { - it("takes the given radius", function() { + it("takes the given radius", function () { var marker = L.circleMarker([0, 0], { radius: 20 }); marker.addTo(map); marker.setStyle({ radius: 15 }); diff --git a/spec/suites/layer/vector/PolygonSpec.js b/spec/suites/layer/vector/PolygonSpec.js index b0e25f83..7014838f 100644 --- a/spec/suites/layer/vector/PolygonSpec.js +++ b/spec/suites/layer/vector/PolygonSpec.js @@ -1,4 +1,4 @@ -describe('Polygon', function() { +describe('Polygon', function () { var c = document.createElement('div'); c.style.width = '400px'; @@ -6,7 +6,7 @@ describe('Polygon', function() { var map = new L.Map(c); map.setView(new L.LatLng(55.8, 37.6), 6); - describe("#initialize", function() { + describe("#initialize", function () { it("doesn't overwrite the given latlng array", function () { var originalLatLngs = [ [1, 2], @@ -24,6 +24,21 @@ describe('Polygon', function() { var polygon = new L.Polygon([]); expect(polygon.getLatLngs()).to.eql([]); }); + + it("can be initialized with holes", function () { + var originalLatLngs = [ + [ //external rink + [0, 10], [10, 10], [10, 0] + ], [ //hole + [2, 3], [2, 4], [3, 4] + ] + ]; + + var polygon = new L.Polygon(originalLatLngs); + + //getLatLngs() returns only external ring + expect(polygon.getLatLngs()).to.eql([L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])]); + }); }); describe("#setLatLngs", function () { @@ -40,6 +55,22 @@ describe('Polygon', function() { expect(sourceLatLngs).to.eql(originalLatLngs); }); + + it("can be set external ring and holes", function () { + var latLngs = [ + [ //external rink + [0, 10], [10, 10], [10, 0] + ], [ //hole + [2, 3], [2, 4], [3, 4] + ] + ]; + + var polygon = new L.Polygon([]); + polygon.setLatLngs(latLngs); + + //getLatLngs() returns only external ring + expect(polygon.getLatLngs()).to.eql([L.latLng([0, 10]), L.latLng([10, 10]), L.latLng([10, 0])]); + }); }); describe("#spliceLatLngs", function () { diff --git a/spec/suites/layer/vector/PolylineGeometrySpec.js b/spec/suites/layer/vector/PolylineGeometrySpec.js index 42bb93c5..86a9e463 100644 --- a/spec/suites/layer/vector/PolylineGeometrySpec.js +++ b/spec/suites/layer/vector/PolylineGeometrySpec.js @@ -1,4 +1,4 @@ -describe('PolylineGeometry', function() { +describe('PolylineGeometry', function () { var c = document.createElement('div'); c.style.width = '400px'; @@ -6,12 +6,12 @@ describe('PolylineGeometry', function() { var map = new L.Map(c); map.setView(new L.LatLng(55.8, 37.6), 6); - describe("#distanceTo", function() { - it("calculates distances to points", function() { + describe("#distanceTo", function () { + it("calculates distances to points", function () { var p1 = map.latLngToLayerPoint(new L.LatLng(55.8, 37.6)); var p2 = map.latLngToLayerPoint(new L.LatLng(57.123076977278, 44.861962891635)); var latlngs = [[56.485503424111, 35.545556640339], [55.972522915346, 36.116845702918], [55.502459116923, 34.930322265253], [55.31534617509, 38.973291015816]] - .map(function(ll) { + .map(function (ll) { return new L.LatLng(ll[0], ll[1]); }); var polyline = new L.Polyline([], { diff --git a/spec/suites/layer/vector/PolylineSpec.js b/spec/suites/layer/vector/PolylineSpec.js index 7a7ce597..176543e6 100644 --- a/spec/suites/layer/vector/PolylineSpec.js +++ b/spec/suites/layer/vector/PolylineSpec.js @@ -1,4 +1,4 @@ -describe('Polyline', function() { +describe('Polyline', function () { var c = document.createElement('div'); c.style.width = '400px'; @@ -6,7 +6,7 @@ describe('Polyline', function() { var map = new L.Map(c); map.setView(new L.LatLng(55.8, 37.6), 6); - describe("#initialize", function() { + describe("#initialize", function () { it("doesn't overwrite the given latlng array", function () { var originalLatLngs = [ [1, 2], diff --git a/spec/suites/map/MapSpec.js b/spec/suites/map/MapSpec.js index 762594e1..122bba96 100644 --- a/spec/suites/map/MapSpec.js +++ b/spec/suites/map/MapSpec.js @@ -29,8 +29,8 @@ describe("Map", function () { var container = document.createElement('div'), map = new L.Map(container); expect(function () { - new L.Map(container); - }).to.throwException(function(e) { + L.map(container); + }).to.throwException(function (e) { expect(e.message).to.eql("Map container is already initialized."); }); map.remove(); @@ -38,8 +38,8 @@ describe("Map", function () { it("throws an exception if a container is not found", function () { expect(function () { - new L.Map('nonexistentdivelement'); - }).to.throwException(function(e) { + L.map('nonexistentdivelement'); + }).to.throwException(function (e) { expect(e.message).to.eql("Map container not found."); }); map.remove(); @@ -72,11 +72,24 @@ describe("Map", function () { }); describe('#getCenter', function () { - it ('throws if not set before', function () { + it('throws if not set before', function () { expect(function () { map.getCenter(); }).to.throwError(); }); + + it('returns a precise center when zoomed in after being set (#426)', function () { + var center = L.latLng(10, 10); + map.setView(center, 1); + map.setZoom(19); + expect(map.getCenter()).to.eql(center); + }); + + it('returns correct center after invalidateSize (#1919)', function () { + map.setView(L.latLng(10, 10), 1); + map.invalidateSize(); + expect(map.getCenter()).not.to.eql(L.latLng(10, 10)); + }); }); describe("#whenReady", function () { @@ -108,6 +121,12 @@ describe("Map", function () { expect(map.getZoom()).to.be(13); expect(map.getCenter().distanceTo([51.505, -0.09])).to.be.lessThan(5); }); + it("can be passed without a zoom specified", function () { + map.setZoom(13); + expect(map.setView([51.605, -0.11])).to.be(map); + expect(map.getZoom()).to.be(13); + expect(map.getCenter().distanceTo([51.605, -0.11])).to.be.lessThan(5); + }); }); describe("#getBounds", function () { @@ -120,7 +139,53 @@ describe("Map", function () { }); }); + describe('#setMaxBounds', function () { + it("aligns pixel-wise map view center with maxBounds center if it cannot move view bounds inside maxBounds (#1908)", function () { + var container = map.getContainer(); + // large view, cannot fit within maxBounds + container.style.width = container.style.height = "1000px"; + document.body.appendChild(container); + // maxBounds + var bounds = L.latLngBounds([51.5, -0.05], [51.55, 0.05]); + map.setMaxBounds(bounds, {animate: false}); + // set view outside + map.setView(L.latLng([53.0, 0.15]), 12, {animate: false}); + // get center of bounds in pixels + var boundsCenter = map.project(bounds.getCenter()).round(); + expect(map.project(map.getCenter()).round()).to.eql(boundsCenter); + document.body.removeChild(container); + }); + it("moves map view within maxBounds by changing one coordinate", function () { + var container = map.getContainer(); + // small view, can fit within maxBounds + container.style.width = container.style.height = "200px"; + document.body.appendChild(container); + // maxBounds + var bounds = L.latLngBounds([51, -0.2], [52, 0.2]); + map.setMaxBounds(bounds, {animate: false}); + // set view outside maxBounds on one direction only + // leaves untouched the other coordinate (that is not already centered) + var initCenter = [53.0, 0.1]; + map.setView(L.latLng(initCenter), 16, {animate: false}); + // one pixel coordinate hasn't changed, the other has + var pixelCenter = map.project(map.getCenter()).round(); + var pixelInit = map.project(initCenter).round(); + expect(pixelCenter.x).to.eql(pixelInit.x); + expect(pixelCenter.y).not.to.eql(pixelInit.y); + // the view is inside the bounds + expect(bounds.contains(map.getBounds())).to.be(true); + document.body.removeChild(container); + }); + }); + describe("#getMinZoom and #getMaxZoom", function () { + describe('#getMinZoom', function () { + it('returns 0 if not set by Map options or TileLayer options', function () { + var map = L.map(document.createElement('div')); + expect(map.getMinZoom()).to.be(0); + }); + }); + it("minZoom and maxZoom options overrides any minZoom and maxZoom set on layers", function () { var map = L.map(document.createElement('div'), {minZoom: 2, maxZoom: 20}); @@ -189,7 +254,7 @@ describe("Map", function () { it("adds the layer before firing layeradd", function (done) { var layer = { onAdd: sinon.spy(), onRemove: sinon.spy() }; - map.on('layeradd', function() { + map.on('layeradd', function () { expect(map.hasLayer(layer)).to.be.ok(); done(); }); @@ -202,7 +267,7 @@ describe("Map", function () { var spy = sinon.spy(); map.on("zoomlevelschange", spy); expect(spy.called).not.to.be.ok(); - L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map); + L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map); expect(spy.called).to.be.ok(); }); }); @@ -210,10 +275,10 @@ describe("Map", function () { describe("when a new layer with greater zoomlevel coverage than the current layer is added to a map", function () { it("fires a zoomlevelschange event", function () { var spy = sinon.spy(); - L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map); + L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map); map.on("zoomlevelschange", spy); expect(spy.called).not.to.be.ok(); - L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 15 }).addTo(map); + L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 15}).addTo(map); expect(spy.called).to.be.ok(); }); }); @@ -221,12 +286,12 @@ describe("Map", function () { describe("when a new layer with the same or lower zoomlevel coverage as the current layer is added to a map", function () { it("fires no zoomlevelschange event", function () { var spy = sinon.spy(); - L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map); + L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map); map.on("zoomlevelschange", spy); expect(spy.called).not.to.be.ok(); - L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map); + L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map); expect(spy.called).not.to.be.ok(); - L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 5 }).addTo(map); + L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 5}).addTo(map); expect(spy.called).not.to.be.ok(); }); }); @@ -285,7 +350,7 @@ describe("Map", function () { it("removes the layer before firing layerremove", function (done) { var layer = { onAdd: sinon.spy(), onRemove: sinon.spy() }; - map.on('layerremove', function() { + map.on('layerremove', function () { expect(map.hasLayer(layer)).not.to.be.ok(); done(); }); @@ -296,9 +361,9 @@ describe("Map", function () { describe("when the last tile layer on a map is removed", function () { it("fires a zoomlevelschange event", function () { - map.whenReady(function(){ + map.whenReady(function () { var spy = sinon.spy(); - var tl = L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map); + var tl = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map); map.on("zoomlevelschange", spy); expect(spy.called).not.to.be.ok(); @@ -310,10 +375,10 @@ describe("Map", function () { describe("when a tile layer is removed from a map and it had greater zoom level coverage than the remainding layer", function () { it("fires a zoomlevelschange event", function () { - map.whenReady(function(){ + map.whenReady(function () { var spy = sinon.spy(), - tl = L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map), - t2 = L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 15 }).addTo(map); + tl = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map), + t2 = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 15}).addTo(map); map.on("zoomlevelschange", spy); expect(spy.called).to.not.be.ok(); @@ -325,10 +390,10 @@ describe("Map", function () { describe("when a tile layer is removed from a map it and it had lesser or the sa,e zoom level coverage as the remainding layer(s)", function () { it("fires no zoomlevelschange event", function () { - map.whenReady(function(){ - var tl = L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map), - t2 = L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 10 }).addTo(map), - t3 = L.tileLayer("{z}{x}{y}", { minZoom:0, maxZoom: 5 }).addTo(map); + map.whenReady(function () { + var tl = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map), + t2 = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map), + t3 = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 5}).addTo(map); map.on("zoomlevelschange", spy); expect(spy).not.toHaveBeenCalled(); @@ -367,4 +432,105 @@ describe("Map", function () { expect(spy.thisValues[0]).to.eql(map); }); }); + + describe("#invalidateSize", function () { + var container, + origWidth = 100, + clock; + + beforeEach(function () { + container = map.getContainer(); + container.style.width = origWidth + "px"; + document.body.appendChild(container); + map.setView([0, 0], 0); + map.invalidateSize({pan: false}); + clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + document.body.removeChild(container); + clock.restore(); + }); + + it("pans by the right amount when growing in 1px increments", function () { + container.style.width = (origWidth + 1) + "px"; + map.invalidateSize(); + expect(map._getMapPanePos().x).to.be(1); + + container.style.width = (origWidth + 2) + "px"; + map.invalidateSize(); + expect(map._getMapPanePos().x).to.be(1); + + container.style.width = (origWidth + 3) + "px"; + map.invalidateSize(); + expect(map._getMapPanePos().x).to.be(2); + }); + + it("pans by the right amount when shrinking in 1px increments", function () { + container.style.width = (origWidth - 1) + "px"; + map.invalidateSize(); + expect(map._getMapPanePos().x).to.be(0); + + container.style.width = (origWidth - 2) + "px"; + map.invalidateSize(); + expect(map._getMapPanePos().x).to.be(-1); + + container.style.width = (origWidth - 3) + "px"; + map.invalidateSize(); + expect(map._getMapPanePos().x).to.be(-1); + }); + + it("pans back to the original position after growing by an odd size and back", function () { + container.style.width = (origWidth + 5) + "px"; + map.invalidateSize(); + + container.style.width = origWidth + "px"; + map.invalidateSize(); + + expect(map._getMapPanePos().x).to.be(0); + }); + + it("emits no move event if the size has not changed", function () { + var spy = sinon.spy(); + map.on("move", spy); + + map.invalidateSize(); + + expect(spy.called).not.to.be.ok(); + }); + + it("emits a move event if the size has changed", function () { + var spy = sinon.spy(); + map.on("move", spy); + + container.style.width = (origWidth + 5) + "px"; + map.invalidateSize(); + + expect(spy.called).to.be.ok(); + }); + + it("emits a moveend event if the size has changed", function () { + var spy = sinon.spy(); + map.on("moveend", spy); + + container.style.width = (origWidth + 5) + "px"; + map.invalidateSize(); + + expect(spy.called).to.be.ok(); + }); + + it("debounces the moveend event if the debounceMoveend option is given", function () { + var spy = sinon.spy(); + map.on("moveend", spy); + + container.style.width = (origWidth + 5) + "px"; + map.invalidateSize({debounceMoveend: true}); + + expect(spy.called).not.to.be.ok(); + + clock.tick(200); + + expect(spy.called).to.be.ok(); + }); + }); }); diff --git a/spec/suites/map/handler/Map.DragSpec.js b/spec/suites/map/handler/Map.DragSpec.js new file mode 100644 index 00000000..189b9192 --- /dev/null +++ b/spec/suites/map/handler/Map.DragSpec.js @@ -0,0 +1,38 @@ +describe("Map.Drag", function () { + describe("#addHook", function () { + it("calls the map with dragging enabled", function () { + var container = document.createElement('div'), + map = new L.Map(container, { + dragging: true + }); + + expect(map.dragging.enabled()).to.be(true); + map.setView([0, 0], 0); + expect(map.dragging.enabled()).to.be(true); + }); + it("calls the map with dragging and worldCopyJump enabled", function () { + var container = document.createElement('div'), + map = new L.Map(container, { + dragging: true, + worldCopyJump: true + }); + + expect(map.dragging.enabled()).to.be(true); + map.setView([0, 0], 0); + expect(map.dragging.enabled()).to.be(true); + }); + it("calls the map with dragging disabled and worldCopyJump enabled; " + + "enables dragging after setting center and zoom", function () { + var container = document.createElement('div'), + map = new L.Map(container, { + dragging: false, + worldCopyJump: true + }); + + expect(map.dragging.enabled()).to.be(false); + map.setView([0, 0], 0); + map.dragging.enable(); + expect(map.dragging.enabled()).to.be(true); + }); + }); +}); diff --git a/src/Leaflet.js b/src/Leaflet.js index ea330280..6303605b 100644 --- a/src/Leaflet.js +++ b/src/Leaflet.js @@ -2,7 +2,7 @@ var oldL = window.L, L = {}; -L.version = '0.6.4'; +L.version = '0.7'; // define Leaflet for Node module pattern loaders, including Browserify if (typeof module === 'object' && typeof module.exports === 'object') { diff --git a/src/control/Control.Attribution.js b/src/control/Control.Attribution.js index 3902f66e..21eb1c5f 100644 --- a/src/control/Control.Attribution.js +++ b/src/control/Control.Attribution.js @@ -18,6 +18,12 @@ L.Control.Attribution = L.Control.extend({ this._container = L.DomUtil.create('div', 'leaflet-control-attribution'); L.DomEvent.disableClickPropagation(this._container); + for (var i in map._layers) { + if (map._layers[i].getAttribution) { + this.addAttribution(map._layers[i].getAttribution()); + } + } + map .on('layeradd', this._onLayerAdd, this) .on('layerremove', this._onLayerRemove, this); diff --git a/src/control/Control.Layers.js b/src/control/Control.Layers.js index 4a4d72bc..6fc2cbcb 100644 --- a/src/control/Control.Layers.js +++ b/src/control/Control.Layers.js @@ -69,8 +69,9 @@ L.Control.Layers = L.Control.extend({ container.setAttribute('aria-haspopup', true); if (!L.Browser.touch) { - L.DomEvent.disableClickPropagation(container); - L.DomEvent.on(container, 'mousewheel', L.DomEvent.stopPropagation); + L.DomEvent + .disableClickPropagation(container) + .disableScrollPropagation(container); } else { L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation); } @@ -95,6 +96,10 @@ L.Control.Layers = L.Control.extend({ else { L.DomEvent.on(link, 'focus', this._expand, this); } + //Work around for Firefox android issue https://github.com/Leaflet/Leaflet/issues/2033 + L.DomEvent.on(form, 'click', function () { + setTimeout(L.bind(this._onInputClick, this), 0); + }, this); this._map.on('click', this._collapse, this); // TODO keyboard accessibility @@ -229,6 +234,8 @@ L.Control.Layers = L.Control.extend({ } this._handlingClick = false; + + this._refocusOnMap(); }, _expand: function () { diff --git a/src/control/Control.Zoom.js b/src/control/Control.Zoom.js index afb740d1..5acfc560 100644 --- a/src/control/Control.Zoom.js +++ b/src/control/Control.Zoom.js @@ -4,7 +4,11 @@ L.Control.Zoom = L.Control.extend({ options: { - position: 'topleft' + position: 'topleft', + zoomInText: '+', + zoomInTitle: 'Zoom in', + zoomOutText: '-', + zoomOutTitle: 'Zoom out' }, onAdd: function (map) { @@ -14,10 +18,13 @@ L.Control.Zoom = L.Control.extend({ this._map = map; this._zoomInButton = this._createButton( - '+', 'Zoom in', zoomName + '-in', container, this._zoomIn, this); + this.options.zoomInText, this.options.zoomInTitle, + zoomName + '-in', container, this._zoomIn, this); this._zoomOutButton = this._createButton( - '-', 'Zoom out', zoomName + '-out', container, this._zoomOut, this); + this.options.zoomOutText, this.options.zoomOutTitle, + zoomName + '-out', container, this._zoomOut, this); + this._updateDisabled(); map.on('zoomend zoomlevelschange', this._updateDisabled, this); return container; @@ -48,7 +55,8 @@ L.Control.Zoom = L.Control.extend({ .on(link, 'mousedown', stop) .on(link, 'dblclick', stop) .on(link, 'click', L.DomEvent.preventDefault) - .on(link, 'click', fn, context); + .on(link, 'click', fn, context) + .on(link, 'click', this._refocusOnMap, context); return link; }, diff --git a/src/control/Control.js b/src/control/Control.js index 5939fe5b..0e65cf50 100644 --- a/src/control/Control.js +++ b/src/control/Control.js @@ -66,6 +66,12 @@ L.Control = L.Class.extend({ } return this; + }, + + _refocusOnMap: function () { + if (this._map) { + this._map.getContainer().focus(); + } } }); diff --git a/src/core/Browser.js b/src/core/Browser.js index 1239ba7b..53908fc1 100644 --- a/src/core/Browser.js +++ b/src/core/Browser.js @@ -4,9 +4,7 @@ (function () { - var ie = !!window.ActiveXObject, - ie6 = ie && !window.XMLHttpRequest, - ie7 = ie && !document.querySelector, + var ie = 'ActiveXObject' in window, ielt9 = ie && !document.addEventListener, // terrible browser detection to work around Safari / iOS / Android browser bugs @@ -16,10 +14,13 @@ phantomjs = ua.indexOf('phantom') !== -1, android = ua.indexOf('android') !== -1, android23 = ua.search('android [23]') !== -1, + gecko = ua.indexOf('gecko') !== -1, mobile = typeof orientation !== undefined + '', - msTouch = window.navigator && window.navigator.msPointerEnabled && - window.navigator.msMaxTouchPoints, + msPointer = window.navigator && window.navigator.msPointerEnabled && + window.navigator.msMaxTouchPoints && !window.PointerEvent, + pointer = (window.PointerEvent && window.navigator.pointerEnabled && window.navigator.maxTouchPoints) || + msPointer, retina = ('devicePixelRatio' in window && window.devicePixelRatio > 1) || ('matchMedia' in window && window.matchMedia('(min-resolution:144dpi)') && window.matchMedia('(min-resolution:144dpi)').matches), @@ -39,8 +40,8 @@ var startName = 'ontouchstart'; - // IE10+ (We simulate these into touch* events in L.DomEvent and L.DomEvent.MsTouch) or WebKit, etc. - if (msTouch || (startName in doc)) { + // IE10+ (We simulate these into touch* events in L.DomEvent and L.DomEvent.Pointer) or WebKit, etc. + if (pointer || (startName in doc)) { return true; } @@ -66,10 +67,9 @@ L.Browser = { ie: ie, - ie6: ie6, - ie7: ie7, ielt9: ielt9, webkit: webkit, + gecko: gecko && !webkit && !window.opera && !ie, android: android, android23: android23, @@ -88,7 +88,8 @@ mobileOpera: mobile && window.opera, touch: touch, - msTouch: msTouch, + msPointer: msPointer, + pointer: pointer, retina: retina }; diff --git a/src/core/Events.js b/src/core/Events.js index 5c1efb10..cd8b17af 100644 --- a/src/core/Events.js +++ b/src/core/Events.js @@ -14,7 +14,7 @@ L.Mixin.Events = { if (L.Util.invokeEach(types, this.addEventListener, this, fn, context)) { return this; } var events = this[eventsKey] = this[eventsKey] || {}, - contextId = context && L.stamp(context), + contextId = context && context !== this && L.stamp(context), i, len, event, type, indexKey, indexLenKey, typeIndex; // types can be a string of space-separated words @@ -27,7 +27,7 @@ L.Mixin.Events = { }; type = types[i]; - if (context) { + if (contextId) { // store listeners of a particular context in a separate hash (if it has an id) // gives a major performance boost when removing thousands of map layers @@ -74,7 +74,7 @@ L.Mixin.Events = { if (L.Util.invokeEach(types, this.removeEventListener, this, fn, context)) { return this; } var events = this[eventsKey], - contextId = context && L.stamp(context), + contextId = context && context !== this && L.stamp(context), i, len, type, listeners, j, indexKey, indexLenKey, typeIndex, removed; types = L.Util.splitWords(types); @@ -90,9 +90,10 @@ L.Mixin.Events = { // clear all listeners for a type if function isn't specified delete events[type]; delete events[indexKey]; + delete events[indexLenKey]; } else { - listeners = context && typeIndex ? typeIndex[contextId] : events[type]; + listeners = contextId && typeIndex ? typeIndex[contextId] : events[type]; if (listeners) { for (j = listeners.length - 1; j >= 0; j--) { @@ -135,7 +136,7 @@ L.Mixin.Events = { listeners = events[type].slice(); for (i = 0, len = listeners.length; i < len; i++) { - listeners[i].action.call(listeners[i].context || this, event); + listeners[i].action.call(listeners[i].context, event); } } @@ -147,7 +148,7 @@ L.Mixin.Events = { if (listeners) { for (i = 0, len = listeners.length; i < len; i++) { - listeners[i].action.call(listeners[i].context || this, event); + listeners[i].action.call(listeners[i].context, event); } } } diff --git a/src/core/Util.js b/src/core/Util.js index 8fb99479..5e67b17b 100644 --- a/src/core/Util.js +++ b/src/core/Util.js @@ -105,19 +105,23 @@ L.Util = { return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); }, - template: function (str, data) { - return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) { - var value = data[key]; - if (value === undefined) { - throw new Error('No value provided for variable ' + str); - } else if (typeof value === 'function') { - value = value(data); - } - return value; + compileTemplate: function (str, data) { + // based on https://gist.github.com/padolsey/6008842 + str = str.replace(/"/g, '\\\"'); + str = str.replace(/\{ *([\w_]+) *\}/g, function (str, key) { + return '" + o["' + key + '"]' + (typeof data[key] === 'function' ? '(o)' : '') + ' + "'; }); + // jshint evil: true + return new Function('o', 'return "' + str + '";'); }, - isArray: function (obj) { + template: function (str, data) { + var cache = L.Util._templateCache = L.Util._templateCache || {}; + cache[str] = cache[str] || L.Util.compileTemplate(str, data); + return cache[str](data); + }, + + isArray: Array.isArray || function (obj) { return (Object.prototype.toString.call(obj) === '[object Array]'); }, diff --git a/src/dom/DomEvent.DoubleTap.js b/src/dom/DomEvent.DoubleTap.js index 6cd8d03c..50d9bbac 100644 --- a/src/dom/DomEvent.DoubleTap.js +++ b/src/dom/DomEvent.DoubleTap.js @@ -4,8 +4,8 @@ L.extend(L.DomEvent, { - _touchstart: L.Browser.msTouch ? 'MSPointerDown' : 'touchstart', - _touchend: L.Browser.msTouch ? 'MSPointerUp' : 'touchend', + _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart', + _touchend: L.Browser.msPointer ? 'MSPointerUp' : L.Browser.pointer ? 'pointerup' : 'touchend', // inspired by Zepto touch code by Thomas Fuchs addDoubleTapListener: function (obj, handler, id) { @@ -21,7 +21,7 @@ L.extend(L.DomEvent, { function onTouchStart(e) { var count; - if (L.Browser.msTouch) { + if (L.Browser.pointer) { trackedTouches.push(e.pointerId); count = trackedTouches.length; } else { @@ -40,7 +40,7 @@ L.extend(L.DomEvent, { } function onTouchEnd(e) { - if (L.Browser.msTouch) { + if (L.Browser.pointer) { var idx = trackedTouches.indexOf(e.pointerId); if (idx === -1) { return; @@ -49,7 +49,7 @@ L.extend(L.DomEvent, { } if (doubleTap) { - if (L.Browser.msTouch) { + if (L.Browser.pointer) { // work around .type being readonly with MSPointer* events var newTouch = { }, prop; @@ -73,15 +73,15 @@ L.extend(L.DomEvent, { obj[pre + touchstart + id] = onTouchStart; obj[pre + touchend + id] = onTouchEnd; - // on msTouch we need to listen on the document, otherwise a drag starting on the map and moving off screen + // on pointer we need to listen on the document, otherwise a drag starting on the map and moving off screen // will not come through to us, so we will lose track of how many touches are ongoing - var endElement = L.Browser.msTouch ? document.documentElement : obj; + var endElement = L.Browser.pointer ? document.documentElement : obj; obj.addEventListener(touchstart, onTouchStart, false); endElement.addEventListener(touchend, onTouchEnd, false); - if (L.Browser.msTouch) { - endElement.addEventListener('MSPointerCancel', onTouchEnd, false); + if (L.Browser.pointer) { + endElement.addEventListener(L.DomEvent.POINTER_CANCEL, onTouchEnd, false); } return this; @@ -91,11 +91,12 @@ L.extend(L.DomEvent, { var pre = '_leaflet_'; obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false); - (L.Browser.msTouch ? document.documentElement : obj).removeEventListener( + (L.Browser.pointer ? document.documentElement : obj).removeEventListener( this._touchend, obj[pre + this._touchend + id], false); - if (L.Browser.msTouch) { - document.documentElement.removeEventListener('MSPointerCancel', obj[pre + this._touchend + id], false); + if (L.Browser.pointer) { + document.documentElement.removeEventListener(L.DomEvent.POINTER_CANCEL, obj[pre + this._touchend + id], + false); } return this; diff --git a/src/dom/DomEvent.MsTouch.js b/src/dom/DomEvent.MsTouch.js deleted file mode 100644 index 685baaa9..00000000 --- a/src/dom/DomEvent.MsTouch.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. - */ - -L.extend(L.DomEvent, { - - _msTouches: [], - _msDocumentListener: false, - - // Provides a touch events wrapper for msPointer events. - // Based on changes by veproza https://github.com/CloudMade/Leaflet/pull/1019 - - addMsTouchListener: function (obj, type, handler, id) { - - switch (type) { - case 'touchstart': - return this.addMsTouchListenerStart(obj, type, handler, id); - case 'touchend': - return this.addMsTouchListenerEnd(obj, type, handler, id); - case 'touchmove': - return this.addMsTouchListenerMove(obj, type, handler, id); - default: - throw 'Unknown touch event type'; - } - }, - - addMsTouchListenerStart: function (obj, type, handler, id) { - var pre = '_leaflet_', - touches = this._msTouches; - - var cb = function (e) { - - var alreadyInArray = false; - for (var i = 0; i < touches.length; i++) { - if (touches[i].pointerId === e.pointerId) { - alreadyInArray = true; - break; - } - } - if (!alreadyInArray) { - touches.push(e); - } - - e.touches = touches.slice(); - e.changedTouches = [e]; - - handler(e); - }; - - obj[pre + 'touchstart' + id] = cb; - obj.addEventListener('MSPointerDown', cb, false); - - // need to also listen for end events to keep the _msTouches list accurate - // this needs to be on the body and never go away - if (!this._msDocumentListener) { - var internalCb = function (e) { - for (var i = 0; i < touches.length; i++) { - if (touches[i].pointerId === e.pointerId) { - touches.splice(i, 1); - break; - } - } - }; - //We listen on the documentElement as any drags that end by moving the touch off the screen get fired there - document.documentElement.addEventListener('MSPointerUp', internalCb, false); - document.documentElement.addEventListener('MSPointerCancel', internalCb, false); - - this._msDocumentListener = true; - } - - return this; - }, - - addMsTouchListenerMove: function (obj, type, handler, id) { - var pre = '_leaflet_', - touches = this._msTouches; - - function cb(e) { - - // don't fire touch moves when mouse isn't down - if (e.pointerType === e.MSPOINTER_TYPE_MOUSE && e.buttons === 0) { return; } - - for (var i = 0; i < touches.length; i++) { - if (touches[i].pointerId === e.pointerId) { - touches[i] = e; - break; - } - } - - e.touches = touches.slice(); - e.changedTouches = [e]; - - handler(e); - } - - obj[pre + 'touchmove' + id] = cb; - obj.addEventListener('MSPointerMove', cb, false); - - return this; - }, - - addMsTouchListenerEnd: function (obj, type, handler, id) { - var pre = '_leaflet_', - touches = this._msTouches; - - var cb = function (e) { - for (var i = 0; i < touches.length; i++) { - if (touches[i].pointerId === e.pointerId) { - touches.splice(i, 1); - break; - } - } - - e.touches = touches.slice(); - e.changedTouches = [e]; - - handler(e); - }; - - obj[pre + 'touchend' + id] = cb; - obj.addEventListener('MSPointerUp', cb, false); - obj.addEventListener('MSPointerCancel', cb, false); - - return this; - }, - - removeMsTouchListener: function (obj, type, id) { - var pre = '_leaflet_', - cb = obj[pre + type + id]; - - switch (type) { - case 'touchstart': - obj.removeEventListener('MSPointerDown', cb, false); - break; - case 'touchmove': - obj.removeEventListener('MSPointerMove', cb, false); - break; - case 'touchend': - obj.removeEventListener('MSPointerUp', cb, false); - obj.removeEventListener('MSPointerCancel', cb, false); - break; - } - - return this; - } -}); diff --git a/src/dom/DomEvent.Pointer.js b/src/dom/DomEvent.Pointer.js new file mode 100644 index 00000000..4957fcd1 --- /dev/null +++ b/src/dom/DomEvent.Pointer.js @@ -0,0 +1,155 @@ +/* + * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. + */ + +L.extend(L.DomEvent, { + + //static + POINTER_DOWN: L.Browser.msPointer ? 'MSPointerDown' : 'pointerdown', + POINTER_MOVE: L.Browser.msPointer ? 'MSPointerMove' : 'pointermove', + POINTER_UP: L.Browser.msPointer ? 'MSPointerUp' : 'pointerup', + POINTER_CANCEL: L.Browser.msPointer ? 'MSPointerCancel' : 'pointercancel', + + _pointers: [], + _pointerDocumentListener: false, + + // Provides a touch events wrapper for (ms)pointer events. + // Based on changes by veproza https://github.com/CloudMade/Leaflet/pull/1019 + //ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 + + addPointerListener: function (obj, type, handler, id) { + + switch (type) { + case 'touchstart': + return this.addPointerListenerStart(obj, type, handler, id); + case 'touchend': + return this.addPointerListenerEnd(obj, type, handler, id); + case 'touchmove': + return this.addPointerListenerMove(obj, type, handler, id); + default: + throw 'Unknown touch event type'; + } + }, + + addPointerListenerStart: function (obj, type, handler, id) { + var pre = '_leaflet_', + pointers = this._pointers; + + var cb = function (e) { + + L.DomEvent.preventDefault(e); + + var alreadyInArray = false; + for (var i = 0; i < pointers.length; i++) { + if (pointers[i].pointerId === e.pointerId) { + alreadyInArray = true; + break; + } + } + if (!alreadyInArray) { + pointers.push(e); + } + + e.touches = pointers.slice(); + e.changedTouches = [e]; + + handler(e); + }; + + obj[pre + 'touchstart' + id] = cb; + obj.addEventListener(this.POINTER_DOWN, cb, false); + + // need to also listen for end events to keep the _pointers list accurate + // this needs to be on the body and never go away + if (!this._pointerDocumentListener) { + var internalCb = function (e) { + for (var i = 0; i < pointers.length; i++) { + if (pointers[i].pointerId === e.pointerId) { + pointers.splice(i, 1); + break; + } + } + }; + //We listen on the documentElement as any drags that end by moving the touch off the screen get fired there + document.documentElement.addEventListener(this.POINTER_UP, internalCb, false); + document.documentElement.addEventListener(this.POINTER_CANCEL, internalCb, false); + + this._pointerDocumentListener = true; + } + + return this; + }, + + addPointerListenerMove: function (obj, type, handler, id) { + var pre = '_leaflet_', + touches = this._pointers; + + function cb(e) { + + // don't fire touch moves when mouse isn't down + if ((e.pointerType === e.MSPOINTER_TYPE_MOUSE || e.pointerType === 'mouse') && e.buttons === 0) { return; } + + for (var i = 0; i < touches.length; i++) { + if (touches[i].pointerId === e.pointerId) { + touches[i] = e; + break; + } + } + + e.touches = touches.slice(); + e.changedTouches = [e]; + + handler(e); + } + + obj[pre + 'touchmove' + id] = cb; + obj.addEventListener(this.POINTER_MOVE, cb, false); + + return this; + }, + + addPointerListenerEnd: function (obj, type, handler, id) { + var pre = '_leaflet_', + touches = this._pointers; + + var cb = function (e) { + for (var i = 0; i < touches.length; i++) { + if (touches[i].pointerId === e.pointerId) { + touches.splice(i, 1); + break; + } + } + + e.touches = touches.slice(); + e.changedTouches = [e]; + + handler(e); + }; + + obj[pre + 'touchend' + id] = cb; + obj.addEventListener(this.POINTER_UP, cb, false); + obj.addEventListener(this.POINTER_CANCEL, cb, false); + + return this; + }, + + removePointerListener: function (obj, type, id) { + var pre = '_leaflet_', + cb = obj[pre + type + id]; + + switch (type) { + case 'touchstart': + obj.removeEventListener(this.POINTER_DOWN, cb, false); + break; + case 'touchmove': + obj.removeEventListener(this.POINTER_MOVE, cb, false); + break; + case 'touchend': + obj.removeEventListener(this.POINTER_UP, cb, false); + obj.removeEventListener(this.POINTER_CANCEL, cb, false); + break; + } + + return this; + } +}); diff --git a/src/dom/DomEvent.js b/src/dom/DomEvent.js index 7df9e7fd..6a0a5884 100644 --- a/src/dom/DomEvent.js +++ b/src/dom/DomEvent.js @@ -16,8 +16,8 @@ L.DomEvent = { return fn.call(context || obj, e || L.DomEvent._getEvent()); }; - if (L.Browser.msTouch && type.indexOf('touch') === 0) { - return this.addMsTouchListener(obj, type, handler, id); + if (L.Browser.pointer && type.indexOf('touch') === 0) { + return this.addPointerListener(obj, type, handler, id); } if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) { this.addDoubleTapListener(obj, handler, id); @@ -69,8 +69,8 @@ L.DomEvent = { if (!handler) { return this; } - if (L.Browser.msTouch && type.indexOf('touch') === 0) { - this.removeMsTouchListener(obj, type, id); + if (L.Browser.pointer && type.indexOf('touch') === 0) { + this.removePointerListener(obj, type, id); } else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) { this.removeDoubleTapListener(obj, id); @@ -101,19 +101,29 @@ L.DomEvent = { } else { e.cancelBubble = true; } + L.DomEvent._skipped(e); + return this; }, + disableScrollPropagation: function (el) { + var stop = L.DomEvent.stopPropagation; + + return L.DomEvent + .on(el, 'mousewheel', stop) + .on(el, 'MozMousePixelScroll', stop); + }, + disableClickPropagation: function (el) { var stop = L.DomEvent.stopPropagation; for (var i = L.Draggable.START.length - 1; i >= 0; i--) { - L.DomEvent.addListener(el, L.Draggable.START[i], stop); + L.DomEvent.on(el, L.Draggable.START[i], stop); } return L.DomEvent - .addListener(el, 'click', L.DomEvent._fakeStop) - .addListener(el, 'dblclick', stop); + .on(el, 'click', L.DomEvent._fakeStop) + .on(el, 'dblclick', stop); }, preventDefault: function (e) { @@ -127,34 +137,31 @@ L.DomEvent = { }, stop: function (e) { - return L.DomEvent.preventDefault(e).stopPropagation(e); + return L.DomEvent + .preventDefault(e) + .stopPropagation(e); }, getMousePosition: function (e, container) { - - var ie7 = L.Browser.ie7, - body = document.body, + var body = document.body, docEl = document.documentElement, - x = e.pageX ? e.pageX - body.scrollLeft - docEl.scrollLeft: e.clientX, + //gecko makes scrollLeft more negative as you scroll in rtl, other browsers don't + //ref: https://code.google.com/p/closure-library/source/browse/closure/goog/style/bidi.js + x = L.DomUtil.documentIsLtr() ? + (e.pageX ? e.pageX - body.scrollLeft - docEl.scrollLeft : e.clientX) : + (L.Browser.gecko ? e.pageX - body.scrollLeft - docEl.scrollLeft : + e.pageX ? e.pageX - body.scrollLeft + docEl.scrollLeft : e.clientX), y = e.pageY ? e.pageY - body.scrollTop - docEl.scrollTop: e.clientY, - pos = new L.Point(x, y), - rect = container.getBoundingClientRect(), + pos = new L.Point(x, y); + + if (!container) { + return pos; + } + + var rect = container.getBoundingClientRect(), left = rect.left - container.clientLeft, top = rect.top - container.clientTop; - // webkit (and ie <= 7) handles RTL scrollLeft different to everyone else - // https://code.google.com/p/closure-library/source/browse/trunk/closure/goog/style/bidi.js - if (!L.DomUtil.documentIsLtr() && (L.Browser.webkit || ie7)) { - left += container.scrollWidth - container.clientWidth; - - // ie7 shows the scrollbar by default and provides clientWidth counting it, so we - // need to add it back in if it is visible; scrollbar is on the left as we are RTL - if (ie7 && L.DomUtil.getStyle(container, 'overflow-y') !== 'hidden' && - L.DomUtil.getStyle(container, 'overflow') !== 'hidden') { - left += 17; - } - } - return pos._subtract(new L.Point(left, top)); }, diff --git a/src/dom/DomUtil.js b/src/dom/DomUtil.js index 59012168..f0bf761c 100644 --- a/src/dom/DomUtil.js +++ b/src/dom/DomUtil.js @@ -30,8 +30,7 @@ L.DomUtil = { el = element, docBody = document.body, docEl = document.documentElement, - pos, - ie7 = L.Browser.ie7; + pos; do { top += el.offsetTop || 0; @@ -78,19 +77,6 @@ L.DomUtil = { top -= el.scrollTop || 0; left -= el.scrollLeft || 0; - // webkit (and ie <= 7) handles RTL scrollLeft different to everyone else - // https://code.google.com/p/closure-library/source/browse/trunk/closure/goog/style/bidi.js - if (!L.DomUtil.documentIsLtr() && (L.Browser.webkit || ie7)) { - left += el.scrollWidth - el.clientWidth; - - // ie7 shows the scrollbar by default and provides clientWidth counting it, so we - // need to add it back in if it is visible; scrollbar is on the left as we are RTL - if (ie7 && L.DomUtil.getStyle(el, 'overflow-y') !== 'hidden' && - L.DomUtil.getStyle(el, 'overflow') !== 'hidden') { - left += 17; - } - } - el = el.parentNode; } while (el); @@ -118,18 +104,44 @@ L.DomUtil = { }, hasClass: function (el, name) { - return (el.className.length > 0) && - new RegExp('(^|\\s)' + name + '(\\s|$)').test(el.className); + if (el.classList !== undefined) { + return el.classList.contains(name); + } + var className = L.DomUtil._getClass(el); + return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); }, addClass: function (el, name) { - if (!L.DomUtil.hasClass(el, name)) { - el.className += (el.className ? ' ' : '') + name; + if (el.classList !== undefined) { + var classes = L.Util.splitWords(name); + for (var i = 0, len = classes.length; i < len; i++) { + el.classList.add(classes[i]); + } + } else if (!L.DomUtil.hasClass(el, name)) { + var className = L.DomUtil._getClass(el); + L.DomUtil._setClass(el, (className ? className + ' ' : '') + name); } }, removeClass: function (el, name) { - el.className = L.Util.trim((' ' + el.className + ' ').replace(' ' + name + ' ', ' ')); + if (el.classList !== undefined) { + el.classList.remove(name); + } else { + L.DomUtil._setClass(el, L.Util.trim((' ' + L.DomUtil._getClass(el) + ' ').replace(' ' + name + ' ', ' '))); + } + }, + + _setClass: function (el, name) { + if (el.className.baseVal === undefined) { + el.className = name; + } else { + // in case of SVG element + el.className.baseVal = name; + } + }, + + _getClass: function (el) { + return el.className.baseVal === undefined ? el.className : el.className.baseVal; }, setOpacity: function (el, value) { @@ -238,27 +250,39 @@ L.DomUtil.TRANSITION_END = L.DomUtil.TRANSITION + 'End' : 'transitionend'; (function () { - var userSelectProperty = L.DomUtil.testProp( - ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); + if ('onselectstart' in document) { + L.extend(L.DomUtil, { + disableTextSelection: function () { + L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); + }, + + enableTextSelection: function () { + L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); + } + }); + } else { + var userSelectProperty = L.DomUtil.testProp( + ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); + + L.extend(L.DomUtil, { + disableTextSelection: function () { + if (userSelectProperty) { + var style = document.documentElement.style; + this._userSelect = style[userSelectProperty]; + style[userSelectProperty] = 'none'; + } + }, + + enableTextSelection: function () { + if (userSelectProperty) { + document.documentElement.style[userSelectProperty] = this._userSelect; + delete this._userSelect; + } + } + }); + } L.extend(L.DomUtil, { - disableTextSelection: function () { - L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); - if (userSelectProperty) { - var style = document.documentElement.style; - this._userSelect = style[userSelectProperty]; - style[userSelectProperty] = 'none'; - } - }, - - enableTextSelection: function () { - L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); - if (userSelectProperty) { - document.documentElement.style[userSelectProperty] = this._userSelect; - delete this._userSelect; - } - }, - disableImageDrag: function () { L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault); }, diff --git a/src/dom/Draggable.js b/src/dom/Draggable.js index 469e97c2..65b2ae01 100644 --- a/src/dom/Draggable.js +++ b/src/dom/Draggable.js @@ -10,11 +10,13 @@ L.Draggable = L.Class.extend({ END: { mousedown: 'mouseup', touchstart: 'touchend', + pointerdown: 'touchend', MSPointerDown: 'touchend' }, MOVE: { mousedown: 'mousemove', touchstart: 'touchmove', + pointerdown: 'touchmove', MSPointerDown: 'touchmove' } }, @@ -46,28 +48,21 @@ L.Draggable = L.Class.extend({ }, _onDown: function (e) { + this._moved = false; + if (e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; } - L.DomEvent - .stopPropagation(e); + L.DomEvent.stopPropagation(e); if (L.Draggable._disabled) { return; } L.DomUtil.disableImageDrag(); L.DomUtil.disableTextSelection(); - var first = e.touches ? e.touches[0] : e, - el = first.target; - - // if touching a link, highlight it - if (L.Browser.touch && el.tagName.toLowerCase() === 'a') { - L.DomUtil.addClass(el, 'leaflet-active'); - } - - this._moved = false; - if (this._moving) { return; } + var first = e.touches ? e.touches[0] : e; + this._startPoint = new L.Point(first.clientX, first.clientY); this._startPos = this._newPos = L.DomUtil.getPosition(this._element); @@ -77,7 +72,10 @@ L.Draggable = L.Class.extend({ }, _onMove: function (e) { - if (e.touches && e.touches.length > 1) { return; } + if (e.touches && e.touches.length > 1) { + this._moved = true; + return; + } var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e), newPoint = new L.Point(first.clientX, first.clientY), @@ -93,9 +91,8 @@ L.Draggable = L.Class.extend({ this._moved = true; this._startPos = L.DomUtil.getPosition(this._element).subtract(offset); - if (!L.Browser.touch) { - L.DomUtil.addClass(document.body, 'leaflet-dragging'); - } + L.DomUtil.addClass(document.body, 'leaflet-dragging'); + L.DomUtil.addClass((e.target || e.srcElement), 'leaflet-drag-target'); } this._newPos = this._startPos.add(offset); @@ -111,10 +108,9 @@ L.Draggable = L.Class.extend({ this.fire('drag'); }, - _onUp: function () { - if (!L.Browser.touch) { - L.DomUtil.removeClass(document.body, 'leaflet-dragging'); - } + _onUp: function (e) { + L.DomUtil.removeClass(document.body, 'leaflet-dragging'); + L.DomUtil.removeClass((e.target || e.srcElement), 'leaflet-drag-target'); for (var i in L.Draggable.MOVE) { L.DomEvent @@ -129,7 +125,9 @@ L.Draggable = L.Class.extend({ // ensure drag is not fired after dragend L.Util.cancelAnimFrame(this._animRequest); - this.fire('dragend'); + this.fire('dragend', { + distance: this._newPos.distanceTo(this._startPos) + }); } this._moving = false; diff --git a/src/geo/LatLng.js b/src/geo/LatLng.js index 9e2a453c..9b0e9c50 100644 --- a/src/geo/LatLng.js +++ b/src/geo/LatLng.js @@ -2,16 +2,20 @@ * L.LatLng represents a geographical point with latitude and longitude coordinates. */ -L.LatLng = function (rawLat, rawLng) { // (Number, Number) - var lat = parseFloat(rawLat), - lng = parseFloat(rawLng); +L.LatLng = function (lat, lng, alt) { // (Number, Number, Number) + lat = parseFloat(lat); + lng = parseFloat(lng); if (isNaN(lat) || isNaN(lng)) { - throw new Error('Invalid LatLng object: (' + rawLat + ', ' + rawLng + ')'); + throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); } this.lat = lat; this.lng = lng; + + if (alt !== undefined) { + this.alt = parseFloat(alt); + } }; L.extend(L.LatLng, { @@ -75,7 +79,11 @@ L.latLng = function (a, b) { // (LatLng) or ([Number, Number]) or (Number, Numbe return a; } if (L.Util.isArray(a)) { - return new L.LatLng(a[0], a[1]); + if (typeof a[0] === 'number' || typeof a[0] === 'string') { + return new L.LatLng(a[0], a[1], a[2]); + } else { + return null; + } } if (a === undefined || a === null) { return a; @@ -83,6 +91,9 @@ L.latLng = function (a, b) { // (LatLng) or ([Number, Number]) or (Number, Numbe if (typeof a === 'object' && 'lat' in a) { return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon); } + if (b === undefined) { + return null; + } return new L.LatLng(a, b); }; diff --git a/src/geo/LatLngBounds.js b/src/geo/LatLngBounds.js index 166af803..6fda3a88 100644 --- a/src/geo/LatLngBounds.js +++ b/src/geo/LatLngBounds.js @@ -17,8 +17,9 @@ L.LatLngBounds.prototype = { extend: function (obj) { // (LatLng) or (LatLngBounds) if (!obj) { return this; } - if (typeof obj[0] === 'number' || typeof obj[0] === 'string' || obj instanceof L.LatLng) { - obj = L.latLng(obj); + var latLng = L.latLng(obj); + if (latLng !== null) { + obj = latLng; } else { obj = L.latLngBounds(obj); } diff --git a/src/geo/crs/CRS.EPSG3395.js b/src/geo/crs/CRS.EPSG3395.js index ed03562b..786dec91 100644 --- a/src/geo/crs/CRS.EPSG3395.js +++ b/src/geo/crs/CRS.EPSG3395.js @@ -7,8 +7,8 @@ L.CRS.EPSG3395 = L.extend({}, L.CRS, { transformation: (function () { var m = L.Projection.Mercator, r = m.R_MAJOR, - r2 = m.R_MINOR; + scale = 0.5 / (Math.PI * r); - return new L.Transformation(0.5 / (Math.PI * r), 0.5, -0.5 / (Math.PI * r2), 0.5); + return new L.Transformation(scale, 0.5, -scale, 0.5); }()) }); diff --git a/src/geo/crs/CRS.js b/src/geo/crs/CRS.js index cb70d9b8..e577d8f4 100644 --- a/src/geo/crs/CRS.js +++ b/src/geo/crs/CRS.js @@ -23,5 +23,10 @@ L.CRS = { scale: function (zoom) { return 256 * Math.pow(2, zoom); + }, + + getSize: function (zoom) { + var s = this.scale(zoom); + return L.point(s, s); } }; diff --git a/src/layer/FeatureGroup.js b/src/layer/FeatureGroup.js index 83bc1179..710ba96c 100644 --- a/src/layer/FeatureGroup.js +++ b/src/layer/FeatureGroup.js @@ -15,7 +15,9 @@ L.FeatureGroup = L.LayerGroup.extend({ return this; } - layer.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); + if ('on' in layer) { + layer.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); + } L.LayerGroup.prototype.addLayer.call(this, layer); @@ -51,6 +53,15 @@ L.FeatureGroup = L.LayerGroup.extend({ return this.invoke('bindPopup', content, options); }, + openPopup: function (latlng) { + // open popup on the first layer + for (var id in this._layers) { + this._layers[id].openPopup(latlng); + break; + } + return this; + }, + setStyle: function (style) { return this.invoke('setStyle', style); }, @@ -74,11 +85,10 @@ L.FeatureGroup = L.LayerGroup.extend({ }, _propagateEvent: function (e) { - if (!e.layer) { - e.layer = e.target; - } - e.target = this; - + e = L.extend({}, e, { + layer: e.target, + target: this + }); this.fire(e.type, e); } }); diff --git a/src/layer/GeoJSON.js b/src/layer/GeoJSON.js index 67f92775..6cc89c6b 100644 --- a/src/layer/GeoJSON.js +++ b/src/layer/GeoJSON.js @@ -33,7 +33,7 @@ L.GeoJSON = L.FeatureGroup.extend({ if (options.filter && !options.filter(geojson)) { return; } - var layer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng); + var layer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng, options); layer.feature = L.GeoJSON.asFeature(geojson); layer.defaultOptions = layer.options; @@ -73,11 +73,11 @@ L.GeoJSON = L.FeatureGroup.extend({ }); L.extend(L.GeoJSON, { - geometryToLayer: function (geojson, pointToLayer, coordsToLatLng) { + geometryToLayer: function (geojson, pointToLayer, coordsToLatLng, vectorOptions) { var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, coords = geometry.coordinates, layers = [], - latlng, latlngs, i, len, layer; + latlng, latlngs, i, len; coordsToLatLng = coordsToLatLng || this.coordsToLatLng; @@ -89,37 +89,37 @@ L.extend(L.GeoJSON, { case 'MultiPoint': for (i = 0, len = coords.length; i < len; i++) { latlng = coordsToLatLng(coords[i]); - layer = pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng); - layers.push(layer); + layers.push(pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng)); } return new L.FeatureGroup(layers); case 'LineString': latlngs = this.coordsToLatLngs(coords, 0, coordsToLatLng); - return new L.Polyline(latlngs); + return new L.Polyline(latlngs, vectorOptions); case 'Polygon': + if (coords.length === 2 && !coords[1].length) { + throw new Error('Invalid GeoJSON object.'); + } latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng); - return new L.Polygon(latlngs); + return new L.Polygon(latlngs, vectorOptions); case 'MultiLineString': latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng); - return new L.MultiPolyline(latlngs); + return new L.MultiPolyline(latlngs, vectorOptions); case 'MultiPolygon': latlngs = this.coordsToLatLngs(coords, 2, coordsToLatLng); - return new L.MultiPolygon(latlngs); + return new L.MultiPolygon(latlngs, vectorOptions); case 'GeometryCollection': for (i = 0, len = geometry.geometries.length; i < len; i++) { - layer = this.geometryToLayer({ + layers.push(this.geometryToLayer({ geometry: geometry.geometries[i], type: 'Feature', properties: geojson.properties - }, pointToLayer, coordsToLatLng); - - layers.push(layer); + }, pointToLayer, coordsToLatLng, vectorOptions)); } return new L.FeatureGroup(layers); @@ -129,7 +129,7 @@ L.extend(L.GeoJSON, { }, coordsToLatLng: function (coords) { // (Array[, Boolean]) -> LatLng - return new L.LatLng(coords[1], coords[0]); + return new L.LatLng(coords[1], coords[0], coords[2]); }, coordsToLatLngs: function (coords, levelsDeep, coordsToLatLng) { // (Array[, Number, Function]) -> Array @@ -147,8 +147,13 @@ L.extend(L.GeoJSON, { return latlngs; }, - latLngToCoords: function (latLng) { - return [latLng.lng, latLng.lat]; + latLngToCoords: function (latlng) { + var coords = [latlng.lng, latlng.lat]; + + if (latlng.alt !== undefined) { + coords.push(latlng.alt); + } + return coords; }, latLngsToCoords: function (latLngs) { @@ -223,43 +228,58 @@ L.Polygon.include({ }); (function () { - function includeMulti(Klass, type) { - Klass.include({ - toGeoJSON: function () { - var coords = []; + function multiToGeoJSON(type) { + return function () { + var coords = []; - this.eachLayer(function (layer) { - coords.push(layer.toGeoJSON().geometry.coordinates); - }); + this.eachLayer(function (layer) { + coords.push(layer.toGeoJSON().geometry.coordinates); + }); - return L.GeoJSON.getFeature(this, { - type: type, - coordinates: coords - }); - } - }); - } - - includeMulti(L.MultiPolyline, 'MultiLineString'); - includeMulti(L.MultiPolygon, 'MultiPolygon'); -}()); - -L.LayerGroup.include({ - toGeoJSON: function () { - var features = []; - - this.eachLayer(function (layer) { - if (layer.toGeoJSON) { - features.push(L.GeoJSON.asFeature(layer.toGeoJSON())); - } - }); - - return { - type: 'FeatureCollection', - features: features + return L.GeoJSON.getFeature(this, { + type: type, + coordinates: coords + }); }; } -}); + + L.MultiPolyline.include({toGeoJSON: multiToGeoJSON('MultiLineString')}); + L.MultiPolygon.include({toGeoJSON: multiToGeoJSON('MultiPolygon')}); + + L.LayerGroup.include({ + toGeoJSON: function () { + + var geometry = this.feature && this.feature.geometry, + jsons = [], + json; + + if (geometry && geometry.type === 'MultiPoint') { + return multiToGeoJSON('MultiPoint').call(this); + } + + var isGeometryCollection = geometry && geometry.type === 'GeometryCollection'; + + this.eachLayer(function (layer) { + if (layer.toGeoJSON) { + json = layer.toGeoJSON(); + jsons.push(isGeometryCollection ? json.geometry : L.GeoJSON.asFeature(json)); + } + }); + + if (isGeometryCollection) { + return L.GeoJSON.getFeature(this, { + geometries: jsons, + type: 'GeometryCollection' + }); + } + + return { + type: 'FeatureCollection', + features: jsons + }; + } + }); +}()); L.geoJson = function (geojson, options) { return new L.GeoJSON(geojson, options); diff --git a/src/layer/ImageOverlay.js b/src/layer/ImageOverlay.js index 73fc6528..0248fa7e 100644 --- a/src/layer/ImageOverlay.js +++ b/src/layer/ImageOverlay.js @@ -71,6 +71,15 @@ L.ImageOverlay = L.Class.extend({ return this; }, + setUrl: function (url) { + this._url = url; + this._image.src = this._url; + }, + + getAttribution: function () { + return this.options.attribution; + }, + _initImage: function () { this._image = L.DomUtil.create('img', 'leaflet-image-layer'); diff --git a/src/layer/Popup.js b/src/layer/Popup.js index def998b6..290499ef 100644 --- a/src/layer/Popup.js +++ b/src/layer/Popup.js @@ -12,11 +12,13 @@ L.Popup = L.Class.extend({ options: { minWidth: 50, maxWidth: 300, - maxHeight: null, + // maxHeight: null, autoPan: true, closeButton: true, offset: [0, 7], autoPanPadding: [5, 5], + // autoPanPaddingTopLeft: null, + // autoPanPaddingBottomRight: null, keepInView: false, className: '', zoomAnimation: true @@ -36,7 +38,6 @@ L.Popup = L.Class.extend({ if (!this._container) { this._initLayout(); } - this._updateContent(); var animFade = map.options.fadeAnimation; @@ -47,7 +48,7 @@ L.Popup = L.Class.extend({ map.on(this._getEvents(), this); - this._update(); + this.update(); if (animFade) { L.DomUtil.setOpacity(this._container, 1); @@ -94,18 +95,43 @@ L.Popup = L.Class.extend({ } }, + getLatLng: function () { + return this._latlng; + }, + setLatLng: function (latlng) { this._latlng = L.latLng(latlng); - this._update(); + if (this._map) { + this._updatePosition(); + this._adjustPan(); + } return this; }, + getContent: function () { + return this._content; + }, + setContent: function (content) { this._content = content; - this._update(); + this.update(); return this; }, + update: function () { + if (!this._map) { return; } + + this._container.style.visibility = 'hidden'; + + this._updateContent(); + this._updateLayout(); + this._updatePosition(); + + this._container.style.visibility = ''; + + this._adjustPan(); + }, + _getEvents: function () { var events = { viewreset: this._updatePosition @@ -152,27 +178,14 @@ L.Popup = L.Class.extend({ L.DomEvent.disableClickPropagation(wrapper); this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper); - L.DomEvent.on(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation); - L.DomEvent.on(this._contentNode, 'MozMousePixelScroll', L.DomEvent.stopPropagation); + + L.DomEvent.disableScrollPropagation(this._contentNode); L.DomEvent.on(wrapper, 'contextmenu', L.DomEvent.stopPropagation); + this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container); this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer); }, - _update: function () { - if (!this._map) { return; } - - this._container.style.visibility = 'hidden'; - - this._updateContent(); - this._updateLayout(); - this._updatePosition(); - - this._container.style.visibility = ''; - - this._adjustPan(); - }, - _updateContent: function () { if (!this._content) { return; } @@ -257,21 +270,23 @@ L.Popup = L.Class.extend({ var containerPos = map.layerPointToContainerPoint(layerPos), padding = L.point(this.options.autoPanPadding), + paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding), + paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding), size = map.getSize(), dx = 0, dy = 0; - if (containerPos.x + containerWidth > size.x) { // right - dx = containerPos.x + containerWidth - size.x + padding.x; + if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right + dx = containerPos.x + containerWidth - size.x + paddingBR.x; } - if (containerPos.x - dx < 0) { // left - dx = containerPos.x - padding.x; + if (containerPos.x - dx - paddingTL.x < 0) { // left + dx = containerPos.x - paddingTL.x; } - if (containerPos.y + containerHeight > size.y) { // bottom - dy = containerPos.y + containerHeight - size.y + padding.y; + if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom + dy = containerPos.y + containerHeight - size.y + paddingBR.y; } - if (containerPos.y - dy < 0) { // top - dy = containerPos.y - padding.y; + if (containerPos.y - dy - paddingTL.y < 0) { // top + dy = containerPos.y - paddingTL.y; } if (dx || dy) { diff --git a/src/layer/marker/Icon.js b/src/layer/marker/Icon.js index e67ed823..705462ac 100644 --- a/src/layer/marker/Icon.js +++ b/src/layer/marker/Icon.js @@ -80,19 +80,8 @@ L.Icon = L.Class.extend({ }, _createImg: function (src, el) { - - if (!L.Browser.ie6) { - if (!el) { - el = document.createElement('img'); - } - el.src = src; - } else { - if (!el) { - el = document.createElement('div'); - } - el.style.filter = - 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="' + src + '")'; - } + el = el || document.createElement('img'); + el.src = src; return el; }, diff --git a/src/layer/marker/Marker.Drag.js b/src/layer/marker/Marker.Drag.js index d1ecd9f5..812439f8 100644 --- a/src/layer/marker/Marker.Drag.js +++ b/src/layer/marker/Marker.Drag.js @@ -18,6 +18,7 @@ L.Handler.MarkerDrag = L.Handler.extend({ .on('drag', this._onDrag, this) .on('dragend', this._onDragEnd, this); this._draggable.enable(); + L.DomUtil.addClass(this._marker._icon, 'leaflet-marker-draggable'); }, removeHooks: function () { @@ -27,6 +28,7 @@ L.Handler.MarkerDrag = L.Handler.extend({ .off('dragend', this._onDragEnd, this); this._draggable.disable(); + L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable'); }, moved: function () { @@ -58,9 +60,9 @@ L.Handler.MarkerDrag = L.Handler.extend({ .fire('drag'); }, - _onDragEnd: function () { + _onDragEnd: function (e) { this._marker .fire('moveend') - .fire('dragend'); + .fire('dragend', e); } }); diff --git a/src/layer/marker/Marker.Popup.js b/src/layer/marker/Marker.Popup.js index e62cdeeb..e414d1bb 100644 --- a/src/layer/marker/Marker.Popup.js +++ b/src/layer/marker/Marker.Popup.js @@ -41,11 +41,12 @@ L.Marker.include({ options = L.extend({offset: anchor}, options); - if (!this._popup) { + if (!this._popupHandlersAdded) { this .on('click', this.togglePopup, this) .on('remove', this.closePopup, this) .on('move', this._movePopup, this); + this._popupHandlersAdded = true; } if (content instanceof L.Popup) { @@ -70,13 +71,18 @@ L.Marker.include({ if (this._popup) { this._popup = null; this - .off('click', this.togglePopup) - .off('remove', this.closePopup) - .off('move', this._movePopup); + .off('click', this.togglePopup, this) + .off('remove', this.closePopup, this) + .off('move', this._movePopup, this); + this._popupHandlersAdded = false; } return this; }, + getPopup: function () { + return this._popup; + }, + _movePopup: function (e) { this._popup.setLatLng(e.latlng); } diff --git a/src/layer/marker/Marker.js b/src/layer/marker/Marker.js index bc82515d..d0a0f7d0 100644 --- a/src/layer/marker/Marker.js +++ b/src/layer/marker/Marker.js @@ -9,6 +9,7 @@ L.Marker = L.Class.extend({ options: { icon: new L.Icon.Default(), title: '', + alt: '', clickable: true, draggable: false, keyboard: true, @@ -30,6 +31,7 @@ L.Marker = L.Class.extend({ this._initIcon(); this.update(); + this.fire('add'); if (map.options.zoomAnimation && map.options.markerZoomAnimation) { map.on('zoomanim', this._animateZoom, this); @@ -87,6 +89,10 @@ L.Marker = L.Class.extend({ this.update(); } + if (this._popup) { + this.bindPopup(this._popup); + } + return this; }, @@ -118,6 +124,10 @@ L.Marker = L.Class.extend({ if (options.title) { icon.title = options.title; } + + if (options.alt) { + icon.alt = options.alt; + } } L.DomUtil.addClass(icon, classToAdd); @@ -202,7 +212,7 @@ L.Marker = L.Class.extend({ }, _animateZoom: function (opt) { - var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center); + var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round(); this._setPos(pos); }, @@ -283,7 +293,7 @@ L.Marker = L.Class.extend({ if (this._map) { this._updateOpacity(); } - + return this; }, diff --git a/src/layer/tile/TileLayer.Canvas.js b/src/layer/tile/TileLayer.Canvas.js index 6507f1bb..21d28977 100644 --- a/src/layer/tile/TileLayer.Canvas.js +++ b/src/layer/tile/TileLayer.Canvas.js @@ -17,7 +17,7 @@ L.TileLayer.Canvas = L.TileLayer.extend({ this._reset({hard: true}); this._update(); } - + for (var i in this._tiles) { this._redrawTile(this._tiles[i]); } diff --git a/src/layer/tile/TileLayer.WMS.js b/src/layer/tile/TileLayer.WMS.js index 4cdfcdce..3632436f 100644 --- a/src/layer/tile/TileLayer.WMS.js +++ b/src/layer/tile/TileLayer.WMS.js @@ -43,7 +43,9 @@ L.TileLayer.WMS = L.TileLayer.extend({ this._crs = this.options.crs || map.options.crs; - var projectionKey = parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs'; + this._wmsVersion = parseFloat(this.wmsParams.version); + + var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs'; this.wmsParams[projectionKey] = this._crs.code; L.TileLayer.prototype.onAdd.call(this, map); @@ -57,10 +59,11 @@ L.TileLayer.WMS = L.TileLayer.extend({ nwPoint = tilePoint.multiplyBy(tileSize), sePoint = nwPoint.add([tileSize, tileSize]), - nw = this._crs.project(map.unproject(nwPoint, zoom)), - se = this._crs.project(map.unproject(sePoint, zoom)), - - bbox = [nw.x, se.y, se.x, nw.y].join(','), + nw = this._crs.project(map.unproject(nwPoint, tilePoint.z)), + se = this._crs.project(map.unproject(sePoint, tilePoint.z)), + bbox = this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ? + [se.y, nw.x, nw.y, se.x].join(',') : + [nw.x, se.y, se.x, nw.y].join(','), url = L.Util.template(this._url, {s: this._getSubdomain(tilePoint)}); diff --git a/src/layer/tile/TileLayer.js b/src/layer/tile/TileLayer.js index c01b9e00..2794ec35 100644 --- a/src/layer/tile/TileLayer.js +++ b/src/layer/tile/TileLayer.js @@ -230,7 +230,7 @@ L.TileLayer = L.Class.extend({ this._updateZIndex(); if (this._animated) { - var className = 'leaflet-tile-container leaflet-zoom-animated'; + var className = 'leaflet-tile-container'; this._bgBuffer = L.DomUtil.create('div', className, this._container); this._tileContainer = L.DomUtil.create('div', className, this._container); @@ -268,6 +268,19 @@ L.TileLayer = L.Class.extend({ this._initContainer(); }, + _getTileSize: function () { + var map = this._map, + zoom = map.getZoom() + this.options.zoomOffset, + zoomN = this.options.maxNativeZoom, + tileSize = this.options.tileSize; + + if (zoomN && zoom > zoomN) { + tileSize = Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * tileSize); + } + + return tileSize; + }, + _update: function () { if (!this._map) { return; } @@ -343,8 +356,8 @@ L.TileLayer = L.Class.extend({ var limit = this._getWrapTileNum(); // don't load if exceeds world bounds - if ((options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit)) || - tilePoint.y < 0 || tilePoint.y >= limit) { return false; } + if ((options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit.x)) || + tilePoint.y < 0 || tilePoint.y >= limit.y) { return false; } } if (options.bounds) { @@ -459,8 +472,9 @@ L.TileLayer = L.Class.extend({ }, _getWrapTileNum: function () { - // TODO refactor, limit is not valid for non-standard projections - return Math.pow(2, this._getZoomForUrl()); + var crs = this._map.options.crs, + size = crs.getSize(this._map.getZoom()); + return size.divideBy(this.options.tileSize); }, _adjustTilePoint: function (tilePoint) { @@ -469,11 +483,11 @@ L.TileLayer = L.Class.extend({ // wrap tile coordinates if (!this.options.continuousWorld && !this.options.noWrap) { - tilePoint.x = ((tilePoint.x % limit) + limit) % limit; + tilePoint.x = ((tilePoint.x % limit.x) + limit.x) % limit.x; } if (this.options.tms) { - tilePoint.y = limit - tilePoint.y - 1; + tilePoint.y = limit.y - tilePoint.y - 1; } tilePoint.z = this._getZoomForUrl(); @@ -509,6 +523,11 @@ L.TileLayer = L.Class.extend({ if (L.Browser.ielt9 && this.options.opacity !== undefined) { L.DomUtil.setOpacity(tile, this.options.opacity); } + // without this hack, tiles disappear after zoom on Chrome for Android + // https://github.com/Leaflet/Leaflet/issues/2078 + if (L.Browser.mobileWebkit3d) { + tile.style.WebkitBackfaceVisibility = 'hidden'; + } return tile; }, @@ -519,10 +538,20 @@ L.TileLayer = L.Class.extend({ this._adjustTilePoint(tilePoint); tile.src = this.getTileUrl(tilePoint); + + this.fire('tileloadstart', { + tile: tile, + url: tile.src + }); }, _tileLoaded: function () { this._tilesToLoad--; + + if (this._animated) { + L.DomUtil.addClass(this._tileContainer, 'leaflet-zoom-animated'); + } + if (!this._tilesToLoad) { this.fire('load'); diff --git a/src/layer/vector/CircleMarker.js b/src/layer/vector/CircleMarker.js index 5e777ca0..0c67785e 100644 --- a/src/layer/vector/CircleMarker.js +++ b/src/layer/vector/CircleMarker.js @@ -22,9 +22,20 @@ L.CircleMarker = L.Circle.extend({ this.setRadius(this.options.radius); }, + setLatLng: function (latlng) { + L.Circle.prototype.setLatLng.call(this, latlng); + if (this._popup && this._popup._isOpen) { + this._popup.setLatLng(latlng); + } + }, + setRadius: function (radius) { this.options.radius = this._radius = radius; return this.redraw(); + }, + + getRadius: function () { + return this._radius; } }); diff --git a/src/layer/vector/Path.SVG.js b/src/layer/vector/Path.SVG.js index 723b8edd..30c11ad7 100644 --- a/src/layer/vector/Path.SVG.js +++ b/src/layer/vector/Path.SVG.js @@ -50,6 +50,11 @@ L.Path = L.Path.extend({ this._container = this._createElement('g'); this._path = this._createElement('path'); + + if (this.options.className) { + L.DomUtil.addClass(this._path, this.options.className); + } + this._container.appendChild(this._path); }, @@ -80,6 +85,12 @@ L.Path = L.Path.extend({ } else { this._path.removeAttribute('stroke-dasharray'); } + if (this.options.lineCap) { + this._path.setAttribute('stroke-linecap', this.options.lineCap); + } + if (this.options.lineJoin) { + this._path.setAttribute('stroke-linejoin', this.options.lineJoin); + } } else { this._path.setAttribute('stroke', 'none'); } @@ -104,7 +115,7 @@ L.Path = L.Path.extend({ _initEvents: function () { if (this.options.clickable) { if (L.Browser.svg || !L.Browser.vml) { - this._path.setAttribute('class', 'leaflet-clickable'); + L.DomUtil.addClass(this._path, 'leaflet-clickable'); } L.DomEvent.on(this._container, 'click', this._onMouseClick, this); @@ -154,14 +165,14 @@ L.Map.include({ this._panes.overlayPane.appendChild(this._pathRoot); if (this.options.zoomAnimation && L.Browser.any3d) { - this._pathRoot.setAttribute('class', ' leaflet-zoom-animated'); + L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-animated'); this.on({ 'zoomanim': this._animatePathZoom, 'zoomend': this._endPathZoom }); } else { - this._pathRoot.setAttribute('class', ' leaflet-zoom-hide'); + L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-hide'); } this.on('moveend', this._updateSvgViewport); diff --git a/src/layer/vector/Path.VML.js b/src/layer/vector/Path.VML.js index 265b7972..54106040 100644 --- a/src/layer/vector/Path.VML.js +++ b/src/layer/vector/Path.VML.js @@ -40,10 +40,14 @@ L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({ _initPath: function () { var container = this._container = this._createElement('shape'); - L.DomUtil.addClass(container, 'leaflet-vml-shape'); + + L.DomUtil.addClass(container, 'leaflet-vml-shape' + + (this.options.className ? ' ' + this.options.className : '')); + if (this.options.clickable) { L.DomUtil.addClass(container, 'leaflet-clickable'); } + container.coordsize = '1 1'; this._path = this._createElement('path'); @@ -76,12 +80,18 @@ L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({ stroke.opacity = options.opacity; if (options.dashArray) { - stroke.dashStyle = options.dashArray instanceof Array ? + stroke.dashStyle = L.Util.isArray(options.dashArray) ? options.dashArray.join(' ') : options.dashArray.replace(/( *, *)/g, ' '); } else { stroke.dashStyle = ''; } + if (options.lineCap) { + stroke.endcap = options.lineCap.replace('butt', 'flat'); + } + if (options.lineJoin) { + stroke.joinstyle = options.lineJoin; + } } else if (stroke) { container.removeChild(stroke); diff --git a/src/layer/vector/Path.js b/src/layer/vector/Path.js index 5d05249b..47f7b012 100644 --- a/src/layer/vector/Path.js +++ b/src/layer/vector/Path.js @@ -20,6 +20,8 @@ L.Path = L.Class.extend({ stroke: true, color: '#0033ff', dashArray: null, + lineCap: null, + lineJoin: null, weight: 5, opacity: 0.5, diff --git a/src/layer/vector/Polygon.js b/src/layer/vector/Polygon.js index 528efd62..5fd5dd40 100644 --- a/src/layer/vector/Polygon.js +++ b/src/layer/vector/Polygon.js @@ -8,10 +8,12 @@ L.Polygon = L.Polyline.extend({ }, initialize: function (latlngs, options) { - var i, len, hole; - L.Polyline.prototype.initialize.call(this, latlngs, options); + this._initWithHoles(latlngs); + }, + _initWithHoles: function (latlngs) { + var i, len, hole; if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) { this._latlngs = this._convertLatLngs(latlngs[0]); this._holes = latlngs.slice(1); @@ -52,6 +54,15 @@ L.Polygon = L.Polyline.extend({ } }, + setLatLngs: function (latlngs) { + if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) { + this._initWithHoles(latlngs); + return this.redraw(); + } else { + return L.Polyline.prototype.setLatLngs.call(this, latlngs); + } + }, + _clipPoints: function () { var points = this._originalPoints, newParts = []; diff --git a/src/map/Map.js b/src/map/Map.js index 62f16efb..b073e7ac 100644 --- a/src/map/Map.js +++ b/src/map/Map.js @@ -23,8 +23,13 @@ L.Map = L.Class.extend({ initialize: function (id, options) { // (HTMLElement or String, Object) options = L.setOptions(this, options); + this._initContainer(id); this._initLayout(); + + // hack for https://github.com/Leaflet/Leaflet/issues/1980 + this._onResize = L.bind(this._onResize, this); + this._initEvents(); if (options.maxBounds) { @@ -51,11 +56,16 @@ L.Map = L.Class.extend({ // replaced by animation-powered implementation in Map.PanAnimation.js setView: function (center, zoom) { + zoom = zoom === undefined ? this.getZoom() : zoom; this._resetView(L.latLng(center), this._limitZoom(zoom)); return this; }, setZoom: function (zoom, options) { + if (!this._loaded) { + this._zoom = this._limitZoom(zoom); + return this; + } return this.setView(this.getCenter(), zoom, {zoom: options}); }, @@ -93,6 +103,8 @@ L.Map = L.Class.extend({ nePoint = this.project(bounds.getNorthEast(), zoom), center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); + zoom = options && options.maxZoom ? Math.min(options.maxZoom, zoom) : zoom; + return this.setView(center, zoom, options); }, @@ -105,7 +117,7 @@ L.Map = L.Class.extend({ }, panBy: function (offset) { // (Point) - // replaced with animated panBy in Map.Animation.js + // replaced with animated panBy in Map.PanAnimation.js this.fire('movestart'); this._rawPanBy(L.point(offset)); @@ -114,63 +126,29 @@ L.Map = L.Class.extend({ return this.fire('moveend'); }, - setMaxBounds: function (bounds, options) { + setMaxBounds: function (bounds) { bounds = L.latLngBounds(bounds); this.options.maxBounds = bounds; if (!bounds) { - this._boundsMinZoom = null; - this.off('moveend', this._panInsideMaxBounds, this); - return this; + return this.off('moveend', this._panInsideMaxBounds, this); } - var minZoom = this.getBoundsZoom(bounds, true); - - this._boundsMinZoom = minZoom; - if (this._loaded) { - if (this._zoom < minZoom) { - this.setView(bounds.getCenter(), minZoom, options); - } else { - this.panInsideBounds(bounds); - } + this._panInsideMaxBounds(); } - this.on('moveend', this._panInsideMaxBounds, this); - - return this; + return this.on('moveend', this._panInsideMaxBounds, this); }, - panInsideBounds: function (bounds) { - bounds = L.latLngBounds(bounds); + panInsideBounds: function (bounds, options) { + var center = this.getCenter(), + newCenter = this._limitCenter(center, this._zoom, bounds); - var viewBounds = this.getPixelBounds(), - viewSw = viewBounds.getBottomLeft(), - viewNe = viewBounds.getTopRight(), - sw = this.project(bounds.getSouthWest()), - ne = this.project(bounds.getNorthEast()), - dx = 0, - dy = 0; + if (center.equals(newCenter)) { return this; } - if (viewNe.y < ne.y) { // north - dy = Math.ceil(ne.y - viewNe.y); - } - if (viewNe.x > ne.x) { // east - dx = Math.floor(ne.x - viewNe.x); - } - if (viewSw.y > sw.y) { // south - dy = Math.floor(sw.y - viewSw.y); - } - if (viewSw.x < sw.x) { // west - dx = Math.ceil(sw.x - viewSw.x); - } - - if (dx || dy) { - return this.panBy([dx, dy]); - } - - return this; + return this.panTo(newCenter, options); }, addLayer: function (layer) { @@ -205,7 +183,7 @@ L.Map = L.Class.extend({ removeLayer: function (layer) { var id = L.stamp(layer); - if (!this._layers[id]) { return; } + if (!this._layers[id]) { return this; } if (this._loaded) { layer.onRemove(this); @@ -253,15 +231,14 @@ L.Map = L.Class.extend({ var oldSize = this.getSize(); this._sizeChanged = true; - - if (this.options.maxBounds) { - this.setMaxBounds(this.options.maxBounds); - } + this._initialCenter = null; if (!this._loaded) { return this; } var newSize = this.getSize(), - offset = oldSize.subtract(newSize).divideBy(2).round(); + oldCenter = oldSize.divideBy(2).round(), + newCenter = newSize.divideBy(2).round(), + offset = oldCenter.subtract(newCenter); if (!offset.x && !offset.y) { return this; } @@ -275,9 +252,12 @@ L.Map = L.Class.extend({ this.fire('move'); - // make sure moveend is not fired too often on resize - clearTimeout(this._sizeTimer); - this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); + if (options.debounceMoveend) { + clearTimeout(this._sizeTimer); + this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); + } else { + this.fire('moveend'); + } } return this.fire('resize', { @@ -288,7 +268,7 @@ L.Map = L.Class.extend({ // TODO handler.addTo addHandler: function (name, HandlerClass) { - if (!HandlerClass) { return; } + if (!HandlerClass) { return this; } var handler = this[name] = new HandlerClass(this); @@ -308,7 +288,12 @@ L.Map = L.Class.extend({ this._initEvents('off'); - delete this._container._leaflet; + try { + // throws error in IE6-8 + delete this._container._leaflet; + } catch (e) { + this._container._leaflet = undefined; + } this._clearPanes(); if (this._clearControlPos) { @@ -326,7 +311,7 @@ L.Map = L.Class.extend({ getCenter: function () { // (Boolean) -> LatLng this._checkIfLoaded(); - if (!this._moved()) { + if (this._initialCenter && !this._moved()) { return this._initialCenter; } return this.layerPointToLatLng(this._getCenterLayerPoint()); @@ -345,9 +330,9 @@ L.Map = L.Class.extend({ }, getMinZoom: function () { - var z1 = this._layersMinZoom === undefined ? 0 : this._layersMinZoom, - z2 = this._boundsMinZoom === undefined ? 0 : this._boundsMinZoom; - return this.options.minZoom === undefined ? Math.max(z1, z2) : this.options.minZoom; + return this.options.minZoom === undefined ? + (this._layersMinZoom === undefined ? 0 : this._layersMinZoom) : + this.options.minZoom; }, getMaxZoom: function () { @@ -499,6 +484,7 @@ L.Map = L.Class.extend({ L.DomUtil.addClass(container, 'leaflet-container' + (L.Browser.touch ? ' leaflet-touch' : '') + (L.Browser.retina ? ' leaflet-retina' : '') + + (L.Browser.ielt9 ? ' leaflet-oldie' : '') + (this.options.fadeAnimation ? ' leaflet-fade-anim' : '')); var position = L.DomUtil.getStyle(container, 'position'); @@ -669,12 +655,14 @@ L.Map = L.Class.extend({ _onResize: function () { L.Util.cancelAnimFrame(this._resizeRequest); this._resizeRequest = L.Util.requestAnimFrame( - this.invalidateSize, this, false, this._container); + function () { this.invalidateSize({debounceMoveend: true}); }, this, false, this._container); }, _onMouseClick: function (e) { - if (!this._loaded || (!e._simulated && this.dragging && this.dragging.moved()) || - L.DomEvent._skipped(e)) { return; } + if (!this._loaded || (!e._simulated && + ((this.dragging && this.dragging.moved()) || + (this.boxZoom && this.boxZoom.moved()))) || + L.DomEvent._skipped(e)) { return; } this.fire('preclick'); this._fireMouseEvent(e); @@ -769,6 +757,46 @@ L.Map = L.Class.extend({ return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); }, + // adjust center for view to get inside bounds + _limitCenter: function (center, zoom, bounds) { + + if (!bounds) { return center; } + + var centerPoint = this.project(center, zoom), + viewHalf = this.getSize().divideBy(2), + viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), + offset = this._getBoundsOffset(viewBounds, bounds, zoom); + + return this.unproject(centerPoint.add(offset), zoom); + }, + + // adjust offset for view to get inside bounds + _limitOffset: function (offset, bounds) { + if (!bounds) { return offset; } + + var viewBounds = this.getPixelBounds(), + newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); + + return offset.add(this._getBoundsOffset(newBounds, bounds)); + }, + + // returns offset needed for pxBounds to get inside maxBounds at a specified zoom + _getBoundsOffset: function (pxBounds, maxBounds, zoom) { + var nwOffset = this.project(maxBounds.getNorthWest(), zoom).subtract(pxBounds.min), + seOffset = this.project(maxBounds.getSouthEast(), zoom).subtract(pxBounds.max), + + dx = this._rebound(nwOffset.x, -seOffset.x), + dy = this._rebound(nwOffset.y, -seOffset.y); + + return new L.Point(dx, dy); + }, + + _rebound: function (left, right) { + return left + right > 0 ? + Math.round(left - right) / 2 : + Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); + }, + _limitZoom: function (zoom) { var min = this.getMinZoom(), max = this.getMaxZoom(); diff --git a/src/map/anim/Map.PanAnimation.js b/src/map/anim/Map.PanAnimation.js index a76f1217..777ea7b9 100644 --- a/src/map/anim/Map.PanAnimation.js +++ b/src/map/anim/Map.PanAnimation.js @@ -6,8 +6,8 @@ L.Map.include({ setView: function (center, zoom, options) { - zoom = this._limitZoom(zoom); - center = L.latLng(center); + zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom); + center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds); options = options || {}; if (this._panAnim) { diff --git a/src/map/ext/Map.Geolocation.js b/src/map/ext/Map.Geolocation.js index 5443d579..84f5487d 100644 --- a/src/map/ext/Map.Geolocation.js +++ b/src/map/ext/Map.Geolocation.js @@ -83,7 +83,8 @@ L.Map.include({ var data = { latlng: latlng, - bounds: bounds + bounds: bounds, + timestamp: pos.timestamp }; for (var i in pos.coords) { diff --git a/src/map/handler/Map.BoxZoom.js b/src/map/handler/Map.BoxZoom.js index a91095fd..7198854d 100644 --- a/src/map/handler/Map.BoxZoom.js +++ b/src/map/handler/Map.BoxZoom.js @@ -12,6 +12,7 @@ L.Map.BoxZoom = L.Handler.extend({ this._map = map; this._container = map._container; this._pane = map._panes.overlayPane; + this._moved = false; }, addHooks: function () { @@ -20,9 +21,16 @@ L.Map.BoxZoom = L.Handler.extend({ removeHooks: function () { L.DomEvent.off(this._container, 'mousedown', this._onMouseDown); + this._moved = false; + }, + + moved: function () { + return this._moved; }, _onMouseDown: function (e) { + this._moved = false; + if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } L.DomUtil.disableTextSelection(); @@ -30,21 +38,22 @@ L.Map.BoxZoom = L.Handler.extend({ this._startLayerPoint = this._map.mouseEventToLayerPoint(e); - this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane); - L.DomUtil.setPosition(this._box, this._startLayerPoint); - - //TODO refactor: move cursor to styles - this._container.style.cursor = 'crosshair'; - L.DomEvent .on(document, 'mousemove', this._onMouseMove, this) .on(document, 'mouseup', this._onMouseUp, this) .on(document, 'keydown', this._onKeyDown, this); - - this._map.fire('boxzoomstart'); }, _onMouseMove: function (e) { + if (!this._moved) { + this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane); + L.DomUtil.setPosition(this._box, this._startLayerPoint); + + //TODO refactor: move cursor to styles + this._container.style.cursor = 'crosshair'; + this._map.fire('boxzoomstart'); + } + var startPoint = this._startLayerPoint, box = this._box, @@ -57,14 +66,18 @@ L.Map.BoxZoom = L.Handler.extend({ L.DomUtil.setPosition(box, newPos); + this._moved = true; + // TODO refactor: remove hardcoded 4 pixels box.style.width = (Math.max(0, Math.abs(offset.x) - 4)) + 'px'; box.style.height = (Math.max(0, Math.abs(offset.y) - 4)) + 'px'; }, _finish: function () { - this._pane.removeChild(this._box); - this._container.style.cursor = ''; + if (this._moved) { + this._pane.removeChild(this._box); + this._container.style.cursor = ''; + } L.DomUtil.enableTextSelection(); L.DomUtil.enableImageDrag(); diff --git a/src/map/handler/Map.DoubleClickZoom.js b/src/map/handler/Map.DoubleClickZoom.js index b64335b0..6399734e 100644 --- a/src/map/handler/Map.DoubleClickZoom.js +++ b/src/map/handler/Map.DoubleClickZoom.js @@ -8,15 +8,22 @@ L.Map.mergeOptions({ L.Map.DoubleClickZoom = L.Handler.extend({ addHooks: function () { - this._map.on('dblclick', this._onDoubleClick); + this._map.on('dblclick', this._onDoubleClick, this); }, removeHooks: function () { - this._map.off('dblclick', this._onDoubleClick); + this._map.off('dblclick', this._onDoubleClick, this); }, _onDoubleClick: function (e) { - this.setZoomAround(e.containerPoint, this._zoom + 1); + var map = this._map, + zoom = map.getZoom() + (e.originalEvent.shiftKey ? -1 : 1); + + if (map.options.doubleClickZoom === 'center') { + map.setZoom(zoom); + } else { + map.setZoomAround(e.containerPoint, zoom); + } } }); diff --git a/src/map/handler/Map.Drag.js b/src/map/handler/Map.Drag.js index e75345e9..af4eff67 100644 --- a/src/map/handler/Map.Drag.js +++ b/src/map/handler/Map.Drag.js @@ -32,7 +32,7 @@ L.Map.Drag = L.Handler.extend({ this._draggable.on('predrag', this._onPreDrag, this); map.on('viewreset', this._onViewReset, this); - this._onViewReset(); + map.whenReady(this._onViewReset, this); } } this._draggable.enable(); @@ -104,14 +104,14 @@ L.Map.Drag = L.Handler.extend({ this._draggable._newPos.x = newX; }, - _onDragEnd: function () { + _onDragEnd: function (e) { var map = this._map, options = map.options, delay = +new Date() - this._lastTime, noInertia = !options.inertia || delay > options.inertiaThreshold || !this._positions[0]; - map.fire('dragend'); + map.fire('dragend', e); if (noInertia) { map.fire('moveend'); @@ -135,6 +135,8 @@ L.Map.Drag = L.Handler.extend({ map.fire('moveend'); } else { + offset = map._limitOffset(offset, map.options.maxBounds); + L.Util.requestAnimFrame(function () { map.panBy(offset, { duration: decelerationDuration, diff --git a/src/map/handler/Map.Keyboard.js b/src/map/handler/Map.Keyboard.js index ddf67597..3e63993c 100644 --- a/src/map/handler/Map.Keyboard.js +++ b/src/map/handler/Map.Keyboard.js @@ -15,7 +15,7 @@ L.Map.Keyboard = L.Handler.extend({ right: [39], down: [40], up: [38], - zoomIn: [187, 107, 61], + zoomIn: [187, 107, 61, 171], zoomOut: [189, 109, 173] }, diff --git a/src/map/handler/Map.ScrollWheelZoom.js b/src/map/handler/Map.ScrollWheelZoom.js index 43d2edde..bb9cc8fd 100644 --- a/src/map/handler/Map.ScrollWheelZoom.js +++ b/src/map/handler/Map.ScrollWheelZoom.js @@ -51,7 +51,11 @@ L.Map.ScrollWheelZoom = L.Handler.extend({ if (!delta) { return; } - map.setZoomAround(this._lastMousePos, zoom + delta); + if (map.options.scrollWheelZoom === 'center') { + map.setZoom(zoom + delta); + } else { + map.setZoomAround(this._lastMousePos, zoom + delta); + } } }); diff --git a/src/map/handler/Map.Tap.js b/src/map/handler/Map.Tap.js index efd860e5..7682daaa 100644 --- a/src/map/handler/Map.Tap.js +++ b/src/map/handler/Map.Tap.js @@ -36,7 +36,7 @@ L.Map.Tap = L.Handler.extend({ this._startPos = this._newPos = new L.Point(first.clientX, first.clientY); // if touching a link, highlight it - if (el.tagName.toLowerCase() === 'a') { + if (el.tagName && el.tagName.toLowerCase() === 'a') { L.DomUtil.addClass(el, 'leaflet-active'); } @@ -66,7 +66,7 @@ L.Map.Tap = L.Handler.extend({ var first = e.changedTouches[0], el = first.target; - if (el.tagName.toLowerCase() === 'a') { + if (el && el.tagName && el.tagName.toLowerCase() === 'a') { L.DomUtil.removeClass(el, 'leaflet-active'); } @@ -102,6 +102,6 @@ L.Map.Tap = L.Handler.extend({ } }); -if (L.Browser.touch && !L.Browser.msTouch) { +if (L.Browser.touch && !L.Browser.pointer) { L.Map.addInitHook('addHandler', 'tap', L.Map.Tap); } diff --git a/src/map/handler/Map.TouchZoom.js b/src/map/handler/Map.TouchZoom.js index 3dfb241e..7f9ea662 100644 --- a/src/map/handler/Map.TouchZoom.js +++ b/src/map/handler/Map.TouchZoom.js @@ -3,7 +3,8 @@ */ L.Map.mergeOptions({ - touchZoom: L.Browser.touch && !L.Browser.android23 + touchZoom: L.Browser.touch && !L.Browser.android23, + bounceAtZoomLimits: true }); L.Map.TouchZoom = L.Handler.extend({ @@ -56,6 +57,11 @@ L.Map.TouchZoom = L.Handler.extend({ if (this._scale === 1) { return; } + if (!map.options.bounceAtZoomLimits) { + if ((map.getZoom() === map.getMinZoom() && this._scale < 1) || + (map.getZoom() === map.getMaxZoom() && this._scale > 1)) { return; } + } + if (!this._moved) { L.DomUtil.addClass(map._mapPane, 'leaflet-touching');