Leaflet.markercluster-0.5.0/000077500000000000000000000000001270301646500157565ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/.gitignore000066400000000000000000000002011270301646500177370ustar00rootroot00000000000000# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) bin obj # mstest test results TestResults node_modules Leaflet.markercluster-0.5.0/.travis.yml000066400000000000000000000000431270301646500200640ustar00rootroot00000000000000language: node_js node_js: - 0.10Leaflet.markercluster-0.5.0/CHANGELOG.md000066400000000000000000000331621270301646500175740ustar00rootroot00000000000000Leaflet.markercluster ===================== (all changes without author notice are by [@danzel](https://github.com/danzel)) ##0.5 (2016-04-12) Tagging a newer version, still 0.7 compatible. This is basically what was in the hotfix branch. ### Improvements * chunkedLoading option to keep browser more responsive during larging a load data set [#292](https://github.com/Leaflet/Leaflet.markercluster/issues/292) * maxClusterRadius can be a function (by [@Schwanksta](https://github.com/Schwanksta)) [#298](https://github.com/Leaflet/Leaflet.markercluster/issues/298) * Spiderfy without zooming when all markers at same location (by [@rdenniston](https://github.com/rdenniston), [@ghybs](https://github.com/ghybs)) [#415](https://github.com/Leaflet/Leaflet.markercluster/issues/415), [#606](https://github.com/Leaflet/Leaflet.markercluster/issues/606) * On becoming visible, markers retain their original opacity. (by [@IvanSanchez](https://github.com/IvanSanchez)) [#444](https://github.com/Leaflet/Leaflet.markercluster/issues/444) * Spiderleg Polyline options (by [@mikeatlas](https://github.com/mikeatlas)) [#466](https://github.com/Leaflet/Leaflet.markercluster/issues/466) * Extra methods to allow refreshing cluster icons (by [@ghybs](https://github.com/ghybs)) [#564](https://github.com/Leaflet/Leaflet.markercluster/issues/564) * Ability to disable animations (by [@ghybs](https://github.com/ghybs)) [#578](https://github.com/Leaflet/Leaflet.markercluster/issues/578) * Optimized performance of bulk addLayers and removeLayers (by [@ghybs](https://github.com/ghybs)) [#584](https://github.com/Leaflet/Leaflet.markercluster/issues/584) * Replaced spiderfy legs animation from SMIL to CSS transition (by [@ghybs](https://github.com/ghybs)) [#585](https://github.com/Leaflet/Leaflet.markercluster/issues/585) * Provide more detailed context information on the spiderfied event (by [@evanvosberg](https://github.com/evanvosberg)) [#421](https://github.com/Leaflet/Leaflet.markercluster/issues/421) * Add unspiderfied event * Readme updates (by [@ghybs](https://github.com/ghybs), [@tomchadwin](https://github.com/tomchadwin) [@Cyrille37](https://github.com/Cyrille37) [@franckl](https://github.com/franckl) [@mikeatlas](https://github.com/mikeatlas) [@rdenniston](https://github.com/rdenniston) [@maackle](https://github.com/maackle) [@fureigh](https://github.com/fureigh) [@Wildhoney](https://github.com/Wildhoney) [@Schwanksta](https://github.com/Schwanksta) [@frankrowe](https://github.com/frankrowe)) ### Bugfixes * Fix getBounds when removeOutsideVisibleBounds: false is set. [#321](https://github.com/Leaflet/Leaflet.markercluster/issues/321) * Fix zoomToShowLayer fails after initial spiderfy [#286](https://github.com/Leaflet/Leaflet.markercluster/issues/286) * Fix cluster not disappearing on Android [#344](https://github.com/Leaflet/Leaflet.markercluster/issues/344) * Fix RemoveLayers() when spiderified (by [@Grsmto](https://github.com/Grsmto)) [#358](https://github.com/Leaflet/Leaflet.markercluster/issues/358) * Remove lines from map when removing cluster (by [@olive380](https://github.com/olive380)) [#532](https://github.com/Leaflet/Leaflet.markercluster/issues/532) * Fix getConvexHull when all markers are located at same latitude (by [@olive380](https://github.com/olive380)) [#533](https://github.com/Leaflet/Leaflet.markercluster/issues/533) * Fix removeLayers when cluster is not on the map (by [@eschon](https://github.com/eschon)) [#556](https://github.com/Leaflet/Leaflet.markercluster/issues/556) * Improved zoomToShowLayer with callback check (by [@ghybs](https://github.com/ghybs)) [#572](https://github.com/Leaflet/Leaflet.markercluster/issues/572) * Improved reliability of RefreshSpec test suite for PhantomJS (by [@ghybs](https://github.com/ghybs)) [#577](https://github.com/Leaflet/Leaflet.markercluster/issues/577) * Corrected effect of removeOutsideVisibleBounds option (by [@ghybs](https://github.com/ghybs)) [#575](https://github.com/Leaflet/Leaflet.markercluster/issues/575) * Fix getLayer when provided a string [#531](https://github.com/Leaflet/Leaflet.markercluster/issues/531) * Documentation improvements (by [@ghybs](https://github.com/ghybs)) [#579](https://github.com/Leaflet/Leaflet.markercluster/issues/579) * Correct _getExpandedVisibleBounds for Max Latitude (by [@ghybs](https://github.com/ghybs)) [#587](https://github.com/Leaflet/Leaflet.markercluster/issues/587) * Correct unspiderfy vector (by [@ghybs](https://github.com/ghybs)) [#604](https://github.com/Leaflet/Leaflet.markercluster/issues/604) * Remove "leaflet-cluster-anim" class on map remove while spiderfied (by [@ghybs](https://github.com/ghybs)) [#607](https://github.com/Leaflet/Leaflet.markercluster/issues/607) * Fix disableClusteringAtZoom maxZoom troubles (by [@OriginalSin](https://github.com/OriginalSin)) [#609](https://github.com/Leaflet/Leaflet.markercluster/issues/609) ##0.4 (2013-12-19) ### Improvements * Fix Quick Zoom in/out causing everything to disappear in Firefox (Reported by [@paulovieira](https://github.com/paulovieira)) [#140](https://github.com/Leaflet/Leaflet.markercluster/issues/140) * Slow the expand/contract animation down from 200ms to 300ms ### Bugfixes * Fix some cases zoomToShowLayer wouldn't work (Reported by [@absemetov](https://github.com/absemetov)) [#203](https://github.com/Leaflet/Leaflet.markercluster/issues/203) [#228](https://github.com/Leaflet/Leaflet.markercluster/issues/228) [#286](https://github.com/Leaflet/Leaflet.markercluster/issues/286) ##0.3 (2013-12-18) ### Improvements * Work better with custom projections (by [@andersarstrand](https://github.com/andersarstrand)) [#74](https://github.com/Leaflet/Leaflet.markercluster/issues/74) * Add custom getBounds that works (Reported by [@2803media](https://github.com/2803media)) * Allow spacing spiderfied icons further apart (Reported by [@stevevance](https://github.com/stevevance)) [#100](https://github.com/Leaflet/Leaflet.markercluster/issues/100) * Add custom eachLayer that works (Reported by [@cilogi](https://github.com/cilogi)) [#102](https://github.com/Leaflet/Leaflet.markercluster/issues/102) * Add an option (removeOutsideVisibleBounds) to prevent removing clusters that are outside of the visible bounds (by [@wildhoney](https://github.com/wildhoney)) [#103](https://github.com/Leaflet/Leaflet.markercluster/issues/103) * Add getBounds method to cluster (Reported by [@nderambure](https://github.com/nderambure)) [#88](https://github.com/Leaflet/Leaflet.markercluster/issues/88) * Lots of unit tests * Support having Circle / CircleMarker as child markers * Add factory methods (Reported by [@mourner](https://github.com/mourner)) [#21](https://github.com/Leaflet/Leaflet.markercluster/issues/21) * Add getVisibleParent method to allow getting the visible parent cluster or the marker if it is visible. (By [@littleiffel](https://github.com/littleiffel)) [#102](https://github.com/Leaflet/Leaflet.markercluster/issues/102) * Allow adding non-clusterable things to a MarkerClusterGroup, we don't cluster them. (Reported by [@benbalter](https://github.com/benbalter)) [#195](https://github.com/Leaflet/Leaflet.markercluster/issues/195) * removeLayer supports taking a FeatureGroup (Reported by [@pabloalcaraz](https://github.com/pabloalcaraz)) [#236](https://github.com/Leaflet/Leaflet.markercluster/issues/236) * DistanceGrid tests, QuickHull tests and improvements (By [@tmcw](https://github.com/tmcw)) [#247](https://github.com/Leaflet/Leaflet.markercluster/issues/247) [#248](https://github.com/Leaflet/Leaflet.markercluster/issues/248) [#249](https://github.com/Leaflet/Leaflet.markercluster/issues/249) * Implemented getLayers (Reported by [@metajungle](https://github.com/metajungle)) [#222](https://github.com/Leaflet/Leaflet.markercluster/issues/222) * zoomToBounds now only zooms in as far as it needs to to get all of the markers on screen if this is less zoom than zooming to the actual bounds would be (Reported by [@adamyonk](https://github.com/adamyonk)) [#185](https://github.com/Leaflet/Leaflet.markercluster/issues/185) * Keyboard accessibility improvements (By [@Zombienaute](https://github.com/Zombienaute)) [#273](https://github.com/Leaflet/Leaflet.markercluster/issues/273) * IE Specific css in the default styles is no longer a separate file (By [@frankrowe](https://github.com/frankrowe)) [#280](https://github.com/Leaflet/Leaflet.markercluster/issues/280) * Improve usability with small maps (Reported by [@JSCSJSCS](https://github.com/JSCSJSCS)) [#144](https://github.com/Leaflet/Leaflet.markercluster/issues/144) * Implement FeatureGroup.getLayer (Reported by [@newmanw](https://github.com/newmanw)) [#244](https://github.com/Leaflet/Leaflet.markercluster/issues/244) ### Bugfixes * Fix singleMarkerMode when you aren't on the map (by [@duncanparkes](https://github.com/duncanparkes)) [#77](https://github.com/Leaflet/Leaflet.markercluster/issues/77) * Fix clearLayers when you aren't on the map (by [@duncanparkes](https://github.com/duncanparkes)) [#79](https://github.com/Leaflet/Leaflet.markercluster/issues/79) * IE10 Bug fix (Reported by [@theLundquist](https://github.com/theLundquist)) [#86](https://github.com/Leaflet/Leaflet.markercluster/issues/86) * Fixes for hasLayer after removing a layer (Reported by [@cvisto](https://github.com/cvisto)) [#44](https://github.com/Leaflet/Leaflet.markercluster/issues/44) * Fix clearLayers not unsetting __parent of the markers, preventing them from being re-added. (Reported by [@apuntovanini](https://github.com/apuntovanini)) [#99](https://github.com/Leaflet/Leaflet.markercluster/issues/99) * Fix map.removeLayer(markerClusterGroup) not working (Reported by [@Driklyn](https://github.com/Driklyn)) [#108](https://github.com/Leaflet/Leaflet.markercluster/issues/108) * Fix map.addLayers not updating cluster icons (Reported by [@Driklyn](https://github.com/Driklyn)) [#114](https://github.com/Leaflet/Leaflet.markercluster/issues/114) * Fix spiderfied clusters breaking if a marker is added to them (Reported by [@Driklyn](https://github.com/Driklyn)) [#114](https://github.com/Leaflet/Leaflet.markercluster/issues/114) * Don't show coverage for spiderfied clusters as it will be wrong. (Reported by [@ajbeaven](https://github.com/ajbeaven)) [#95](https://github.com/Leaflet/Leaflet.markercluster/issues/95) * Improve zoom in/out immediately making all everything disappear, still issues in Firefox [#140](https://github.com/Leaflet/Leaflet.markercluster/issues/140) * Fix animation not stopping with only one marker. (Reported by [@Driklyn](https://github.com/Driklyn)) [#146](https://github.com/Leaflet/Leaflet.markercluster/issues/146) * Various fixes for new leaflet (Reported by [@PeterAronZentai](https://github.com/PeterAronZentai)) [#159](https://github.com/Leaflet/Leaflet.markercluster/issues/159) * Fix clearLayers when we are spiderfying (Reported by [@skullbooks](https://github.com/skullbooks)) [#162](https://github.com/Leaflet/Leaflet.markercluster/issues/162) * Fix removing layers in certain situations (Reported by [@bpavot](https://github.com/bpavot)) [#160](https://github.com/Leaflet/Leaflet.markercluster/issues/160) * Support calling hasLayer with null (by [@l0c0luke](https://github.com/l0c0luke)) [#170](https://github.com/Leaflet/Leaflet.markercluster/issues/170) * Lots of fixes for removing a MarkerClusterGroup from the map (Reported by [@annetdeboer](https://github.com/annetdeboer)) [#200](https://github.com/Leaflet/Leaflet.markercluster/issues/200) * Throw error when being added to a map with no maxZoom. * Fixes for markers not appearing after a big zoom (Reported by [@arnoldbird](https://github.com/annetdeboer)) [#216](https://github.com/Leaflet/Leaflet.markercluster/issues/216) (Reported by [@mathilde-pellerin](https://github.com/mathilde-pellerin)) [#260](https://github.com/Leaflet/Leaflet.markercluster/issues/260) * Fix coverage polygon not being removed when a MarkerClusterGroup is removed (Reported by [@ZeusTheTrueGod](https://github.com/ZeusTheTrueGod)) [#245](https://github.com/Leaflet/Leaflet.markercluster/issues/245) * Fix getVisibleParent when no parent is visible (Reported by [@ajbeaven](https://github.com/ajbeaven)) [#265](https://github.com/Leaflet/Leaflet.markercluster/issues/265) * Fix spiderfied markers not hiding on a big zoom (Reported by [@Vaesive](https://github.com/Vaesive)) [#268](https://github.com/Leaflet/Leaflet.markercluster/issues/268) * Fix clusters not hiding on a big zoom (Reported by [@versusvoid](https://github.com/versusvoid)) [#281](https://github.com/Leaflet/Leaflet.markercluster/issues/281) * Don't fire multiple clustermouseover/off events due to child divs in the cluster marker (Reported by [@heidemn](https://github.com/heidemn)) [#252](https://github.com/Leaflet/Leaflet.markercluster/issues/252) ## 0.2 (2012-10-11) ### Improvements * Add addLayers/removeLayers bulk add and remove functions that perform better than the individual methods * Allow customising the polygon generated for showing the area a cluster covers (by [@yohanboniface](https://github.com/yohanboniface)) [#68](https://github.com/Leaflet/Leaflet.markercluster/issues/68) * Add zoomToShowLayer method to zoom down to a marker then call a callback once it is visible * Add animateAddingMarkers to allow disabling animations caused when adding/removing markers * Add hasLayer * Pass the L.MarkerCluster to iconCreateFunction to give more flexibility deciding the icon * Make addLayers support geojson layers * Allow disabling clustering at a given zoom level * Allow styling markers that are added like they were clusters of size 1 ### Bugfixes * Support when leaflet is configured to use canvas rather than SVG * Fix some potential crashes in zoom handlers * Tidy up when we are removed from the map ## 0.1 (2012-08-16) Initial Release! Leaflet.markercluster-0.5.0/CONTRIBUTING.md000066400000000000000000000047231270301646500202150ustar00rootroot00000000000000Contributing to Leaflet.MarkerCluster ===================================== 1. [Reporting Bugs](#reporting-bugs) 2. [Contributing Code](#contributing-code) ## Reporting Bugs Before reporting a bug on the project's [issues page](https://github.com/Leaflet/Leaflet.markercluster/issues), first make sure that your issue is caused by Leaflet.MarkerCluster, not your application code (e.g. passing incorrect arguments to methods, etc.). Second, search the already reported issues for similar cases, and if it's already reported, just add any additional details in the comments. After you've made sure that you've found a new Leaflet.markercluster bug, here are some tips for creating a helpful report that will make fixing it much easier and quicker: * Write a **descriptive, specific title**. Bad: *Problem with polylines*. Good: *Doing X in IE9 causes Z*. * Include **browser, OS and Leaflet version** info in the description. * Create a **simple test case** that demonstrates the bug (e.g. using [JSFiddle](http://jsfiddle.net/) or [JS Bin](http://jsbin.com/)). * Check whether the bug can be reproduced in **other browsers**. * Check if the bug occurs in the stable version, master, or both. * *Bonus tip:* if the bug only appears in the master version but the stable version is fine, use `git bisect` to find the exact commit that introduced the bug. If you just want some help with your project, try asking [on the Leaflet forum](https://groups.google.com/forum/#!forum/leaflet-js) instead. ## Contributing Code ### Considerations for Accepting Patches While we happily accept patches, we're also committed to keeping Leaflet simple, lightweight and blazingly fast. So bugfixes, performance optimizations and small improvements that don't add a lot of code are much more likely to get accepted quickly. Before sending a pull request with a new feature, check if it's been discussed before already (either on [GitHub issues](https://github.com/Leaflet/Leaflet/issues) or [Leaflet UserVoice](http://leaflet.uservoice.com/)), and ask yourself two questions: 1. Are you sure that this new feature is important enough to justify its presence in the Leaflet core? Or will it look better as a plugin in a separate repository? 2. Is it written in a simple, concise way that doesn't add bulk to the codebase? If your feature or API improvement did get merged into master, please consider submitting another pull request with the corresponding [documentation update](#improving-documentation). Leaflet.markercluster-0.5.0/Jakefile.js000066400000000000000000000013111270301646500200220ustar00rootroot00000000000000/* Leaflet.markercluster building, testing and linting scripts. To use, install Node, then run the following commands in the project root: npm install -g jake npm install To check the code for errors and build Leaflet from source, run "jake". To run the tests, run "jake test". For a custom build, open build/build.html in the browser and follow the instructions. */ var build = require('./build/build.js'); desc('Check Leaflet.markercluster source for errors with JSHint'); task('lint', build.lint); desc('Combine and compress Leaflet.markercluster source files'); task('build', ['lint'], build.build); desc('Run PhantomJS tests'); task('test', ['lint'], build.test); task('default', ['build']);Leaflet.markercluster-0.5.0/MIT-LICENCE.txt000066400000000000000000000020341270301646500202070ustar00rootroot00000000000000Copyright 2012 David Leaver Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Leaflet.markercluster-0.5.0/README.md000066400000000000000000000400311270301646500172330ustar00rootroot00000000000000Leaflet.markercluster ===================== Provides Beautiful Animated Marker Clustering functionality for [Leaflet](http://leafletjs.com), a JS library for interactive maps. *Requires Leaflet 0.7.x.* *For Leaflet 1.0 use the [leaflet-master branch](https://github.com/Leaflet/Leaflet.markercluster/tree/leaflet-master)* For a Leaflet 0.5 compatible version, [Download b128e950](https://github.com/Leaflet/Leaflet.markercluster/archive/b128e950d8f5d7da5b60bd0aa9a88f6d3dd17c98.zip)
For a Leaflet 0.4 compatible version, [Download the 0.2 release](https://github.com/Leaflet/Leaflet.markercluster/archive/0.2.zip) ## Table of Contents * [Using the plugin](#using-the-plugin) * [Usage](#usage) * [Options](#options) * [Defaults](#defaults) * [Customising the Clustered Markers](#customising-the-clustered-markers) * [All Options](#all-options) * [Enabled by default (boolean options)](#enabled-by-default-boolean-options) * [Other options](#other-options) * [Chunked addLayers options](#chunked-addlayers-options) * [Events](#events) * [Additional MarkerClusterGroup Events](#additional-markerclustergroup-events) * [Methods](#methods) * [Group methods](#group-methods) * [Adding and removing Markers](#adding-and-removing-markers) * [Bulk adding and removing Markers](#bulk-adding-and-removing-markers) * [Getting the visible parent of a marker](#getting-the-visible-parent-of-a-marker) * [Refreshing the clusters icon](#refreshing-the-clusters-icon) * [Other Group Methods](#other-group-methods) * [Clusters methods](#clusters-methods) * [Getting the bounds of a cluster](#getting-the-bounds-of-a-cluster) * [Zooming to the bounds of a cluster](#zooming-to-the-bounds-of-a-cluster) * [Other clusters methods](#other-clusters-methods) * [Handling LOTS of markers](#handling-lots-of-markers) * [License](#license) * [Sub-plugins](#sub-plugins) ## Using the plugin Install with Bower: `bower install leaflet.markercluster` See the included examples for usage. The [realworld example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-realworld.388.html) is a good place to start, it uses all of the defaults of the clusterer. Or check out the [custom example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-custom.html) for how to customise the behaviour and appearance of the clusterer ### Usage Create a new MarkerClusterGroup, add your markers to it, then add it to the map ```javascript var markers = L.markerClusterGroup(); markers.addLayer(L.marker(getRandomLatLng(map))); ... Add more layers ... map.addLayer(markers); ``` ## Options ### Defaults By default the Clusterer enables some nice defaults for you: * **showCoverageOnHover**: When you mouse over a cluster it shows the bounds of its markers. * **zoomToBoundsOnClick**: When you click a cluster we zoom to its bounds. * **spiderfyOnMaxZoom**: When you click a cluster at the bottom zoom level we spiderfy it so you can see all of its markers. (*Note: the spiderfy occurs at the current zoom level if all items within the cluster are physically located at the same latitude and longitude.*) * **removeOutsideVisibleBounds**: Clusters and markers too far from the viewport are removed from the map for performance. * **spiderLegPolylineOptions**: Allows you to specify [PolylineOptions](http://leafletjs.com/reference.html#polyline-options) to style spider legs. By default, they are `{ weight: 1.5, color: '#222', opacity: 0.5 }`. You can disable any of these as you want in the options when you create the MarkerClusterGroup: ```javascript var markers = L.markerClusterGroup({ spiderfyOnMaxZoom: false, showCoverageOnHover: false, zoomToBoundsOnClick: false }); ``` ### Customising the Clustered Markers As an option to MarkerClusterGroup you can provide your own function for creating the Icon for the clustered markers. The default implementation changes color at bounds of 10 and 100, but more advanced uses may require customising this. You do not need to include the .Default css if you go this way. You are passed a MarkerCluster object, you'll probably want to use `getChildCount()` or `getAllChildMarkers()` to work out the icon to show. ```javascript var markers = L.markerClusterGroup({ iconCreateFunction: function(cluster) { return L.divIcon({ html: '' + cluster.getChildCount() + '' }); } }); ``` Check out the [custom example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-custom.html) for an example of this. If you need to update the clusters icon (e.g. they are based on markers real-time data), use the method [refreshClusters()](#refreshing-the-clusters-icon). ### All Options #### Enabled by default (boolean options) * **showCoverageOnHover**: When you mouse over a cluster it shows the bounds of its markers. * **zoomToBoundsOnClick**: When you click a cluster we zoom to its bounds. * **spiderfyOnMaxZoom**: When you click a cluster at the bottom zoom level we spiderfy it so you can see all of its markers. (*Note: the spiderfy occurs at the current zoom level if all items within the cluster are physically located at the same latitude and longitude.*) * **removeOutsideVisibleBounds**: Clusters and markers too far from the viewport are removed from the map for performance. * **animate**: Smoothly split / merge cluster children when zooming and spiderfying. If `L.DomUtil.TRANSITION` is false, this option has no effect (no animation is possible). #### Other options * **animateAddingMarkers**: If set to true (and `animate` option is also true) then adding individual markers to the MarkerClusterGroup after it has been added to the map will add the marker and animate it into the cluster. Defaults to false as this gives better performance when bulk adding markers. addLayers does not support this, only addLayer with individual Markers. * **disableClusteringAtZoom**: If set, at this zoom level and below markers will not be clustered. This defaults to disabled. [See Example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-realworld-maxzoom.388.html) * **maxClusterRadius**: The maximum radius that a cluster will cover from the central marker (in pixels). Default 80. Decreasing will make more, smaller clusters. You can also use a function that accepts the current map zoom and returns the maximum cluster radius in pixels. * **polygonOptions**: Options to pass when creating the L.Polygon(points, options) to show the bounds of a cluster. Defaults to empty, which lets Leaflet use the [default Path options](http://leafletjs.com/reference.html#path-options). * **singleMarkerMode**: If set to true, overrides the icon for all added markers to make them appear as a 1 size cluster. Note: the markers are not replaced by cluster objects, only their icon is replaced. Hence they still react to normal events, and option `disableClusteringAtZoom` does not restore their previous icon (see [#391](https://github.com/Leaflet/Leaflet.markercluster/issues/391)). * **spiderLegPolylineOptions**: Allows you to specify [PolylineOptions](http://leafletjs.com/reference.html#polyline-options) to style spider legs. By default, they are `{ weight: 1.5, color: '#222', opacity: 0.5 }`. * **spiderfyDistanceMultiplier**: Increase from 1 to increase the distance away from the center that spiderfied markers are placed. Use if you are using big marker icons (Default: 1). * **iconCreateFunction**: Function used to create the cluster icon [See default as example](https://github.com/Leaflet/Leaflet.markercluster/blob/15ed12654acdc54a4521789c498e4603fe4bf781/src/MarkerClusterGroup.js#L542). #### Chunked addLayers options Options for the [addLayers](#bulk-adding-and-removing-markers) method. See [#357](https://github.com/Leaflet/Leaflet.markercluster/issues/357) for explanation on how the chunking works. * **chunkedLoading**: Boolean to split the addLayer**s** processing in to small intervals so that the page does not freeze. * **chunkInterval**: Time interval (in ms) during which addLayers works before pausing to let the rest of the page process. In particular, this prevents the page from freezing while adding a lot of markers. Defaults to 200ms. * **chunkDelay**: Time delay (in ms) between consecutive periods of processing for addLayers. Default to 50ms. * **chunkProgress**: Callback function that is called at the end of each chunkInterval. Typically used to implement a progress indicator, e.g. [code in RealWorld 50k](https://github.com/Leaflet/Leaflet.markercluster/blob/master/example/marker-clustering-realworld.50000.html#L33-L49). Defaults to null. Arguments: 1. Number of processed markers 2. Total number of markers being added 3. Elapsed time (in ms) ## Events Leaflet events like `click`, `mouseover`, etc. are just related to _Markers_ in the cluster. To receive events for clusters, listen to `'cluster' + ''`, ex: `clusterclick`, `clustermouseover`, `clustermouseout`. Set your callback up as follows to handle both cases: ```javascript markers.on('click', function (a) { console.log('marker ' + a.layer); }); markers.on('clusterclick', function (a) { // a.layer is actually a cluster console.log('cluster ' + a.layer.getAllChildMarkers().length); }); ``` ### Additional MarkerClusterGroup Events - **animationend**: Fires when marker clustering/unclustering animation has completed - **spiderfied**: Fires when overlapping markers get spiderified (Contains ```cluster``` and ```markers``` attributes) - **unspiderfied**: Fires when overlapping markers get spiderified (Contains ```cluster``` and ```markers``` attributes) ## Methods ### Group methods #### Adding and removing Markers `addLayer`, `removeLayer` and `clearLayers` are supported and they should work for most uses. #### Bulk adding and removing Markers `addLayers` and `removeLayers` are bulk methods for adding and removing markers and should be favoured over the single versions when doing bulk addition/removal of markers. Each takes an array of markers. You can use [dedicated options](#chunked-addlayers) to fine-tune the behaviour of `addLayers`. If you are removing a lot of markers it will almost definitely be better to call `clearLayers` then call `addLayers` to add the markers you don't want to remove back in. See [#59](https://github.com/Leaflet/Leaflet.markercluster/issues/59#issuecomment-9320628) for details. #### Getting the visible parent of a marker If you have a marker in your MarkerClusterGroup and you want to get the visible parent of it (Either itself or a cluster it is contained in that is currently visible on the map). This will return null if the marker and its parent clusters are not visible currently (they are not near the visible viewpoint) ```javascript var visibleOne = markerClusterGroup.getVisibleParent(myMarker); console.log(visibleOne.getLatLng()); ``` #### Refreshing the clusters icon If you have [customized](#customising-the-clustered-markers) the clusters icon to use some data from the contained markers, and later that data changes, use this method to force a refresh of the cluster icons. You can use the method: - without arguments to force all cluster icons in the Marker Cluster Group to be re-drawn. - with an array or a mapping of markers to force only their parent clusters to be re-drawn. - with an L.LayerGroup. The method will look for all markers in it. Make sure it contains only markers which are also within this Marker Cluster Group. - with a single marker. ```javascript markers.refreshClusters(); markers.refreshClusters([myMarker0, myMarker33]); markers.refreshClusters({id_0: myMarker0, id_any: myMarker33}); markers.refreshClusters(myLayerGroup); markers.refreshClusters(myMarker); ``` The plugin also adds a method on L.Marker to easily update the underlying icon options and refresh the icon. If passing a second argument that evaluates to `true`, the method will also trigger a `refreshCluster` on the parent MarkerClusterGroup for that single marker. ```javascript // Use as many times as required to update markers, // then call refreshClusters once finished. for (i in markersSubArray) { markersSubArray[i].refreshIconOptions(newOptionsMappingArray[i]); } markers.refreshClusters(markersSubArray); // If updating only one marker, pass true to // refresh this marker's parent clusters right away. myMarker.refreshIconOptions(optionsMap, true); ``` #### Other Group Methods * **hasLayer**(layer): Returns true if the given layer (marker) is in the MarkerClusterGroup. * **zoomToShowLayer**(layer, callback): Zooms to show the given marker (spiderfying if required), calls the callback when the marker is visible on the map. ### Clusters methods The following methods can be used with clusters (not the group). They are typically used for event handling. #### Getting the bounds of a cluster When you receive an event from a cluster you can query it for the bounds. ```javascript markers.on('clusterclick', function (a) { var latLngBounds = a.layer.getBounds(); }); ``` You can also query for the bounding convex polygon. See [example/marker-clustering-convexhull.html](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-convexhull.html) for a working example. ```javascript markers.on('clusterclick', function (a) { map.addLayer(L.polygon(a.layer.getConvexHull())); }); ``` #### Zooming to the bounds of a cluster When you receive an event from a cluster you can zoom to its bounds in one easy step. If all of the markers will appear at a higher zoom level, that zoom level is zoomed to instead. See [marker-clustering-zoomtobounds.html](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-zoomtobounds.html) for a working example. ```javascript markers.on('clusterclick', function (a) { a.layer.zoomToBounds(); }); ``` #### Other clusters methods * **getChildCount**: Returns the total number of markers contained within that cluster. * **getAllChildMarkers**: Returns the array of total markers contained within that cluster. ## Handling LOTS of markers The Clusterer can handle 10,000 or even 50,000 markers (in chrome). IE9 has some issues with 50,000. - [realworld 10,000 example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-realworld.10000.html) - [realworld 50,000 example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-realworld.50000.html) Note: these two examples use the `chunkedLoading` option set to true in order to avoid locking the browser for a long time. ## License Leaflet.markercluster is free software, and may be redistributed under the MIT-LICENSE. [![Build Status](https://travis-ci.org/Leaflet/Leaflet.markercluster.png?branch=master)](https://travis-ci.org/Leaflet/Leaflet.markercluster) ## Sub-plugins Leaflet.markercluster plugin is very popular and as such it generates high and diverse expectations for increased functionalities. If you are in that case, be sure to have a look first at the repository [issues](https://github.com/Leaflet/Leaflet.markercluster/issues) in case what you are looking for would already be discussed, and some workarounds would be proposed. Check also the below sub-plugins: | Plugin | Description | Maintainer | | :----- | :---------- | :--------- | | [Leaflet.FeatureGroup.SubGroup](https://github.com/ghybs/Leaflet.FeatureGroup.SubGroup) | Creates a Feature Group that adds its child layers into a parent group when added to a map (e.g. through L.Control.Layers). Typical usage is to dynamically add/remove groups of markers from Marker Cluster. | [ghybs](https://github.com/ghybs) | | [Leaflet.MarkerCluster.LayerSupport](https://github.com/ghybs/Leaflet.MarkerCluster.LayerSupport) | Brings compatibility with L.Control.Layers and other Leaflet plugins. I.e. everything that uses direct calls to map.addLayer and map.removeLayer. | [ghybs](https://github.com/ghybs) | | [Leaflet.MarkerCluster.Freezable](https://github.com/ghybs/Leaflet.MarkerCluster.Freezable) | Adds the ability to freeze clusters at a specified zoom. E.g. freezing at maxZoom + 1 makes as if clustering was programmatically disabled. | [ghybs](https://github.com/ghybs) | Leaflet.markercluster-0.5.0/bower.json000066400000000000000000000012051270301646500177650ustar00rootroot00000000000000{ "name": "leaflet.markercluster", "version": "0.5.0", "homepage": "https://github.com/Leaflet/Leaflet.markercluster", "authors": [ "Dave Leaver " ], "description": "Marker Clustering plugin for Leaflet.", "main": [ "dist/leaflet.markercluster-src.js", "dist/MarkerCluster.css", "dist/MarkerCluster.Default.css" ], "license": "MIT", "ignore": [ "**/.*", "node_modules", "bower_components", "example", "spec", "test", "tests" ], "dependencies": { "leaflet": ">= 0.7.0" } }Leaflet.markercluster-0.5.0/build/000077500000000000000000000000001270301646500170555ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/build/build.js000066400000000000000000000102171270301646500205130ustar00rootroot00000000000000var fs = require('fs'), jshint = require('jshint'), UglifyJS = require('uglify-js'), deps = require('./deps.js').deps, hintrc = require('./hintrc.js').config; function lintFiles(files) { var errorsFound = 0, i, j, len, len2, src, errors, e; for (i = 0, len = files.length; i < len; i++) { jshint.JSHINT(fs.readFileSync(files[i], 'utf8'), hintrc, i ? {L: true} : null); errors = jshint.JSHINT.errors; for (j = 0, len2 = errors.length; j < len2; j++) { e = errors[j]; console.log(files[i] + '\tline ' + e.line + '\tcol ' + e.character + '\t ' + e.reason); } errorsFound += len2; } return errorsFound; } function getFiles(compsBase32) { var memo = {}, comps; if (compsBase32) { comps = parseInt(compsBase32, 32).toString(2).split(''); console.log('Managing dependencies...'); } function addFiles(srcs) { for (var j = 0, len = srcs.length; j < len; j++) { memo[srcs[j]] = true; } } for (var i in deps) { if (comps) { if (parseInt(comps.pop(), 2) === 1) { console.log('\t* ' + i); addFiles(deps[i].src); } else { console.log('\t ' + i); } } else { addFiles(deps[i].src); } } var files = []; for (var src in memo) { files.push('src/' + src); } return files; } exports.getFiles = getFiles; exports.lint = function () { var files = getFiles(); console.log('Checking for JS errors...'); var errorsFound = lintFiles(files); if (errorsFound > 0) { console.log(errorsFound + ' error(s) found.\n'); fail(); } else { console.log('\tCheck passed'); } }; function getSizeDelta(newContent, oldContent) { if (!oldContent) { return 'new'; } var newLen = newContent.replace(/\r\n?/g, '\n').length, oldLen = oldContent.replace(/\r\n?/g, '\n').length, delta = newLen - oldLen; return (delta >= 0 ? '+' : '') + delta; } function loadSilently(path) { try { return fs.readFileSync(path, 'utf8'); } catch (e) { return null; } } function combineFiles(files) { var content = ''; for (var i = 0, len = files.length; i < len; i++) { content += fs.readFileSync(files[i], 'utf8') + '\n\n'; } return content; } exports.build = function (compsBase32, buildName) { var files = getFiles(compsBase32); console.log('Concatenating ' + files.length + ' files...'); var copy = fs.readFileSync('src/copyright.js', 'utf8'), intro = '(function (window, document, undefined) {', outro = '}(window, document));', newSrc = copy + intro + combineFiles(files) + outro, pathPart = 'dist/leaflet.markercluster' + (buildName ? '-' + buildName : ''), srcPath = pathPart + '-src.js', oldSrc = loadSilently(srcPath), srcDelta = getSizeDelta(newSrc, oldSrc); console.log('\tUncompressed size: ' + newSrc.length + ' bytes (' + srcDelta + ')'); if (newSrc === oldSrc) { console.log('\tNo changes'); } else { fs.writeFileSync(srcPath, newSrc); console.log('\tSaved to ' + srcPath); } console.log('Compressing...'); var path = pathPart + '.js', oldCompressed = loadSilently(path), newCompressed = copy + UglifyJS.minify(newSrc, { warnings: true, fromString: true }).code, delta = getSizeDelta(newCompressed, oldCompressed); console.log('\tCompressed size: ' + newCompressed.length + ' bytes (' + delta + ')'); if (newCompressed === oldCompressed) { console.log('\tNo changes'); } else { fs.writeFileSync(path, newCompressed); console.log('\tSaved to ' + path); } }; exports.test = function() { var karma = require('karma'), testConfig = {configFile : __dirname + '/../spec/karma.conf.js'}; testConfig.browsers = ['PhantomJS']; if (isArgv('--chrome')) { testConfig.browsers.push('Chrome'); } if (isArgv('--safari')) { testConfig.browsers.push('Safari'); } if (isArgv('--ff')) { testConfig.browsers.push('Firefox'); } if (isArgv('--ie')) { testConfig.browsers.push('IE'); } if (isArgv('--cov')) { testConfig.preprocessors = { '../src/**/*.js': 'coverage' }; testConfig.coverageReporter = { type : 'html', dir : 'coverage/' }; testConfig.reporters = ['coverage']; } karma.server.start(testConfig); function isArgv(optName) { return process.argv.indexOf(optName) !== -1; } }; Leaflet.markercluster-0.5.0/build/deps.js000066400000000000000000000013661270301646500203540ustar00rootroot00000000000000var deps = { Core: { src: ['MarkerClusterGroup.js', 'MarkerCluster.js', 'MarkerOpacity.js', 'DistanceGrid.js'], desc: 'The core of the library.' }, QuickHull: { src: ['MarkerCluster.QuickHull.js'], desc: 'ConvexHull generation. Used to show the area outline of the markers within a cluster.', heading: 'QuickHull' }, Spiderfier: { src: ['MarkerCluster.Spiderfier.js'], desc: 'Provides the ability to show all of the child markers of a cluster.', heading: 'Spiderfier' }, Refresh: { src: ['MarkerClusterGroup.Refresh.js'], desc: 'Method to request refreshing of clusters icon to reflect changes in markers data.', heading: 'Refresh' } }; if (typeof exports !== 'undefined') { exports.deps = deps; } Leaflet.markercluster-0.5.0/build/hintrc.js000066400000000000000000000012011270301646500206740ustar00rootroot00000000000000exports.config = { // environment "browser": true, "node": true, "predef": ['L', 'define'], "strict": false, // code style "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", // whitespace "indent": 4, "trailing": true, "white": true, "smarttabs": true, //"maxlen": 120 // code simplicity - not enforced but nice to check from time to time // "maxstatements": 20, // "maxcomplexity": 5 // "maxparams": 4, // "maxdepth": 4 }; Leaflet.markercluster-0.5.0/dist/000077500000000000000000000000001270301646500167215ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/dist/MarkerCluster.Default.css000066400000000000000000000024071270301646500236040ustar00rootroot00000000000000.marker-cluster-small { background-color: rgba(181, 226, 140, 0.6); } .marker-cluster-small div { background-color: rgba(110, 204, 57, 0.6); } .marker-cluster-medium { background-color: rgba(241, 211, 87, 0.6); } .marker-cluster-medium div { background-color: rgba(240, 194, 12, 0.6); } .marker-cluster-large { background-color: rgba(253, 156, 115, 0.6); } .marker-cluster-large div { background-color: rgba(241, 128, 23, 0.6); } /* IE 6-8 fallback colors */ .leaflet-oldie .marker-cluster-small { background-color: rgb(181, 226, 140); } .leaflet-oldie .marker-cluster-small div { background-color: rgb(110, 204, 57); } .leaflet-oldie .marker-cluster-medium { background-color: rgb(241, 211, 87); } .leaflet-oldie .marker-cluster-medium div { background-color: rgb(240, 194, 12); } .leaflet-oldie .marker-cluster-large { background-color: rgb(253, 156, 115); } .leaflet-oldie .marker-cluster-large div { background-color: rgb(241, 128, 23); } .marker-cluster { background-clip: padding-box; border-radius: 20px; } .marker-cluster div { width: 30px; height: 30px; margin-left: 5px; margin-top: 5px; text-align: center; border-radius: 15px; font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; } .marker-cluster span { line-height: 30px; }Leaflet.markercluster-0.5.0/dist/MarkerCluster.css000066400000000000000000000015501270301646500222170ustar00rootroot00000000000000.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; transition: transform 0.3s ease-out, opacity 0.3s ease-in; } .leaflet-cluster-spider-leg { /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; } Leaflet.markercluster-0.5.0/example/000077500000000000000000000000001270301646500174115ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/example/geojson-sample.js000066400000000000000000000017721270301646500227010ustar00rootroot00000000000000var geojsonSample = { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [102.0, 0.5] }, "properties": { "prop0": "value0", "color": "blue" } }, { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]] }, "properties": { "color": "red", "prop1": 0.0 } }, { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]] }, "properties": { "color": "green", "prop1": { "this": "that" } } }, { "type": "Feature", "geometry": { "type": "MultiPolygon", "coordinates": [[[[100.0, 1.5], [100.5, 1.5], [100.5, 2.0], [100.0, 2.0], [100.0, 1.5]]], [[[100.5, 2.0], [100.5, 2.5], [101.0, 2.5], [101.0, 2.0], [100.5, 2.0]]]] }, "properties": { "color": "purple" } } ] }; Leaflet.markercluster-0.5.0/example/geojson.html000066400000000000000000000041661270301646500217520ustar00rootroot00000000000000 Leaflet debug page
Leaflet.markercluster-0.5.0/example/marker-clustering-convexhull.html000066400000000000000000000041261270301646500261250ustar00rootroot00000000000000 Leaflet debug page
Leaflet.markercluster-0.5.0/example/marker-clustering-custom.html000066400000000000000000000053731270301646500252550ustar00rootroot00000000000000 Leaflet debug page
Leaflet.markercluster-0.5.0/example/marker-clustering-everything.html000066400000000000000000000045401270301646500261220ustar00rootroot00000000000000 Leaflet debug page
Mouse over a cluster to see the bounds of its children and click a cluster to zoom to those bounds Leaflet.markercluster-0.5.0/example/marker-clustering-geojson.html000066400000000000000000000044011270301646500253760ustar00rootroot00000000000000 Leaflet debug page
Mouse over a cluster to see the bounds of its children and click a cluster to zoom to those bounds Leaflet.markercluster-0.5.0/example/marker-clustering-realworld-maxzoom.388.html000066400000000000000000000026271270301646500277460ustar00rootroot00000000000000 Leaflet debug page
Markers will show on the bottom 2 zoom levels even though the markers would normally cluster. Leaflet.markercluster-0.5.0/example/marker-clustering-realworld-mobile.388.html000066400000000000000000000024601270301646500275160ustar00rootroot00000000000000 Leaflet debug page
Leaflet.markercluster-0.5.0/example/marker-clustering-realworld.10000.html000066400000000000000000000026311270301646500263670ustar00rootroot00000000000000 Leaflet debug page
Mouse over a cluster to see the bounds of its children and click a cluster to zoom to those bounds Leaflet.markercluster-0.5.0/example/marker-clustering-realworld.388.html000066400000000000000000000026011270301646500262460ustar00rootroot00000000000000 Leaflet debug page
Mouse over a cluster to see the bounds of its children and click a cluster to zoom to those bounds Leaflet.markercluster-0.5.0/example/marker-clustering-realworld.50000.html000066400000000000000000000050361270301646500263750ustar00rootroot00000000000000 Leaflet debug page
Mouse over a cluster to see the bounds of its children and click a cluster to zoom to those bounds Leaflet.markercluster-0.5.0/example/marker-clustering-singlemarkermode.html000066400000000000000000000032051270301646500272630ustar00rootroot00000000000000 Leaflet debug page
Click a cluster to zoom to its bounds Leaflet.markercluster-0.5.0/example/marker-clustering-spiderfier.html000066400000000000000000000033521270301646500260720ustar00rootroot00000000000000 Leaflet debug page
Leaflet.markercluster-0.5.0/example/marker-clustering-zoomtobounds.html000066400000000000000000000032761270301646500265050ustar00rootroot00000000000000 Leaflet debug page
Click a cluster to zoom to its bounds Leaflet.markercluster-0.5.0/example/marker-clustering-zoomtoshowlayer.html000066400000000000000000000033041270301646500272200ustar00rootroot00000000000000 Leaflet debug page
When clicked we will zoom down to a marker, spiderfying if required to show it and then open its popup Leaflet.markercluster-0.5.0/example/marker-clustering.html000066400000000000000000000050261270301646500237400ustar00rootroot00000000000000 Leaflet debug page
Leaflet.markercluster-0.5.0/example/mobile.css000066400000000000000000000001121270301646500213640ustar00rootroot00000000000000html, body, #map { margin: 0; padding: 0; width: 100%; height: 100%; }Leaflet.markercluster-0.5.0/example/old-bugs/000077500000000000000000000000001270301646500211255ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/example/old-bugs/add-1000-after.html000066400000000000000000000053311270301646500242220ustar00rootroot00000000000000 Leaflet debug page

Bug #51. Click the button. It will add 1000 markers to the map. this should be fast, but previously in (non-IE browsers) it was very slow.
Bug #43. Improving performance more.
Leaflet.markercluster-0.5.0/example/old-bugs/add-markers-offscreen.html000066400000000000000000000034241270301646500261600ustar00rootroot00000000000000 Leaflet debug page
Bug #69. Click the button 2+ times. Zoom out. Should just be a single cluster but instead one of the child markers is still visible.
Leaflet.markercluster-0.5.0/example/old-bugs/add-remove-before-addtomap.html000066400000000000000000000036621270301646500270740ustar00rootroot00000000000000 Leaflet debug page
Bug #64. Nothing should appear on the map.
Leaflet.markercluster-0.5.0/example/old-bugs/animationless-zoom.html000066400000000000000000000036131270301646500256460ustar00rootroot00000000000000 Leaflet debug page

Bug #216. Click the button. It will zoom in, leaflet will not do an animation for the zoom. A marker should be visible.
Leaflet.markercluster-0.5.0/example/old-bugs/click-cluster-at-screen-edge.html000066400000000000000000000040411270301646500273370ustar00rootroot00000000000000 Leaflet debug page
Bug #344. Click the cluster at the screen edge. Map will zoom to it and its markers will appear, but it will not disappear.
Leaflet.markercluster-0.5.0/example/old-bugs/disappearing-marker-from-spider.html000066400000000000000000000101401270301646500301610ustar00rootroot00000000000000 Leaflet debug page
Click on the cluster to spiderfy and then

Note: The marker on the old cluster position comes back on next move or on map scrolling.
Leaflet.markercluster-0.5.0/example/old-bugs/doesnt-update-cluster-on-bottom-level.html000066400000000000000000000043361270301646500312750ustar00rootroot00000000000000 Leaflet debug page

Bug #114. Markers are added to the map periodically using addLayers. Bug was that after becoming a cluster (size 2 or 3 usually) they would never change again even if more markers were added to them.
Leaflet.markercluster-0.5.0/example/old-bugs/remove-add-clustering.html000066400000000000000000000043241270301646500262160ustar00rootroot00000000000000 Leaflet debug page

Whenever a marker is clicked it is removed from the clusterer and added directly to the map instead.

Click Marker on Left, zoom out 1 layer, click marker on right.

Expected behaviour: Both markers are shown. Bugged behaviour: Both markers are on map with opacity 0.



	


Leaflet.markercluster-0.5.0/example/old-bugs/remove-when-spiderfied.html000066400000000000000000000047251270301646500263730ustar00rootroot00000000000000


	Leaflet debug page

	
	
	
	

	
	
	
	
	
	
	



	

Bug #54. Spiderfy the cluster then click the button. Should result in 2 markers right beside each other on the map.
Bug #53. Spiderfy the cluster then click the button. Spider lines remain on the map.
Bug #49. Spiderfy the cluster then click the second button. Spider lines remain on the map. Click the map to get an error. Leaflet.markercluster-0.5.0/example/old-bugs/removelayer-after-remove-from-map.html000066400000000000000000000043261270301646500304600ustar00rootroot00000000000000 Leaflet debug page
1 - Swap layers
2 - Remove all markers
3 - Swap layers again => Marker is still there
Bug
#160. Click 1,2,3. There should be nothing on the map.
Leaflet.markercluster-0.5.0/example/old-bugs/setView-doesnt-remove.html000066400000000000000000000044631270301646500262350ustar00rootroot00000000000000 Leaflet debug page

Bug #63. Zoom down on the very left side untill markers are visible. Click the button. Scroll to the left in one go, those markers should be in clusters but the actual markers will still be visible.
Leaflet.markercluster-0.5.0/example/old-bugs/zoomtoshowlayer-doesnt-need-to-zoom.html000066400000000000000000000037271270301646500311160ustar00rootroot00000000000000 Leaflet debug page

Bug #65. Click 2 then click the button. You should be scrolled to the marker, old behaviour would zoom you out.
Leaflet.markercluster-0.5.0/example/old-bugs/zoomtoshowlayer-doesnt-zoom-if-centered-on.html000066400000000000000000000035011270301646500323500ustar00rootroot00000000000000 Leaflet debug page

Bug #286 (from @Grsmto). Click the button. The cluster should spiderfy and show the popup, old behaviour did nothing.
Leaflet.markercluster-0.5.0/example/remove-geoJSON-when-spiderfied.html000066400000000000000000000062461270301646500261210ustar00rootroot00000000000000 Leaflet debug page

New Bug. Spiderfy the cluster then click the button #1. All markers disapear, but it should remain marker #2.
New Bug. Spiderfy the cluster then click the button #2. All markers disapear, but it should remain marker #1.
Leaflet.markercluster-0.5.0/example/screen.css000066400000000000000000000007451270301646500214100ustar00rootroot00000000000000#map { width: 800px; height: 600px; border: 1px solid #ccc; } #progress { display: none; position: absolute; z-index: 1000; left: 400px; top: 300px; width: 200px; height: 20px; margin-top: -20px; margin-left: -100px; background-color: #fff; background-color: rgba(255, 255, 255, 0.7); border-radius: 4px; padding: 2px; } #progress-bar { width: 0; height: 100%; background-color: #76A6FC; border-radius: 4px; } Leaflet.markercluster-0.5.0/package.json000066400000000000000000000010471270301646500202460ustar00rootroot00000000000000{ "name": "leaflet.markercluster", "repository": "https://github.com/Leaflet/Leaflet.markercluster", "version": "0.5.0", "description": "Provides Beautiful Animated Marker Clustering functionality for Leaflet", "dependencies": { "leaflet": "~0.7.1" }, "devDependencies": { "jshint": "~2.1.3", "mocha": "~1.10.0", "karma": "~0.8.5", "uglify-js": "~2.3.6", "jake": "~0.5.16" }, "main": "dist/leaflet.markercluster.js", "scripts": { "test": "jake test", "prepublish": "jake" }, "keywords": ["gis", "map"], "license": "MIT" } Leaflet.markercluster-0.5.0/spec/000077500000000000000000000000001270301646500167105ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/spec/after.js000066400000000000000000000001721270301646500203470ustar00rootroot00000000000000// put after Leaflet files as imagePath can't be detected in a PhantomJS env L.Icon.Default.imagePath = "../dist/images"; Leaflet.markercluster-0.5.0/spec/expect.js000066400000000000000000001052751270301646500205500ustar00rootroot00000000000000 (function (global, module) { if ('undefined' == typeof module) { var module = { exports: {} } , exports = module.exports } /** * Exports. */ module.exports = expect; expect.Assertion = Assertion; /** * Exports version. */ expect.version = '0.1.2'; /** * Possible assertion flags. */ var flags = { not: ['to', 'be', 'have', 'include', 'only'] , to: ['be', 'have', 'include', 'only', 'not'] , only: ['have'] , have: ['own'] , be: ['an'] }; function expect (obj) { return new Assertion(obj); } /** * Constructor * * @api private */ function Assertion (obj, flag, parent) { this.obj = obj; this.flags = {}; if (undefined != parent) { this.flags[flag] = true; for (var i in parent.flags) { if (parent.flags.hasOwnProperty(i)) { this.flags[i] = true; } } } var $flags = flag ? flags[flag] : keys(flags) , self = this if ($flags) { for (var i = 0, l = $flags.length; i < l; i++) { // avoid recursion if (this.flags[$flags[i]]) continue; var name = $flags[i] , assertion = new Assertion(this.obj, name, this) if ('function' == typeof Assertion.prototype[name]) { // clone the function, make sure we dont touch the prot reference var old = this[name]; this[name] = function () { return old.apply(self, arguments); } for (var fn in Assertion.prototype) { if (Assertion.prototype.hasOwnProperty(fn) && fn != name) { this[name][fn] = bind(assertion[fn], assertion); } } } else { this[name] = assertion; } } } }; /** * Performs an assertion * * @api private */ Assertion.prototype.assert = function (truth, msg, error) { var msg = this.flags.not ? error : msg , ok = this.flags.not ? !truth : truth; if (!ok) { throw new Error(msg.call(this)); } this.and = new Assertion(this.obj); }; /** * Check if the value is truthy * * @api public */ Assertion.prototype.ok = function () { this.assert( !!this.obj , function(){ return 'expected ' + i(this.obj) + ' to be truthy' } , function(){ return 'expected ' + i(this.obj) + ' to be falsy' }); }; /** * Assert that the function throws. * * @param {Function|RegExp} callback, or regexp to match error string against * @api public */ Assertion.prototype.throwError = Assertion.prototype.throwException = function (fn) { expect(this.obj).to.be.a('function'); var thrown = false , not = this.flags.not try { this.obj(); } catch (e) { if ('function' == typeof fn) { fn(e); } else if ('object' == typeof fn) { var subject = 'string' == typeof e ? e : e.message; if (not) { expect(subject).to.not.match(fn); } else { expect(subject).to.match(fn); } } thrown = true; } if ('object' == typeof fn && not) { // in the presence of a matcher, ensure the `not` only applies to // the matching. this.flags.not = false; } var name = this.obj.name || 'fn'; this.assert( thrown , function(){ return 'expected ' + name + ' to throw an exception' } , function(){ return 'expected ' + name + ' not to throw an exception' }); }; /** * Checks if the array is empty. * * @api public */ Assertion.prototype.empty = function () { var expectation; if ('object' == typeof this.obj && null !== this.obj && !isArray(this.obj)) { if ('number' == typeof this.obj.length) { expectation = !this.obj.length; } else { expectation = !keys(this.obj).length; } } else { if ('string' != typeof this.obj) { expect(this.obj).to.be.an('object'); } expect(this.obj).to.have.property('length'); expectation = !this.obj.length; } this.assert( expectation , function(){ return 'expected ' + i(this.obj) + ' to be empty' } , function(){ return 'expected ' + i(this.obj) + ' to not be empty' }); return this; }; /** * Checks if the obj exactly equals another. * * @api public */ Assertion.prototype.be = Assertion.prototype.equal = function (obj) { this.assert( obj === this.obj , function(){ return 'expected ' + i(this.obj) + ' to equal ' + i(obj) } , function(){ return 'expected ' + i(this.obj) + ' to not equal ' + i(obj) }); return this; }; /** * Checks if the obj sortof equals another. * * @api public */ Assertion.prototype.eql = function (obj) { this.assert( expect.eql(obj, this.obj) , function(){ return 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) } , function(){ return 'expected ' + i(this.obj) + ' to sort of not equal ' + i(obj) }); return this; }; /** * Assert within start to finish (inclusive). * * @param {Number} start * @param {Number} finish * @api public */ Assertion.prototype.within = function (start, finish) { var range = start + '..' + finish; this.assert( this.obj >= start && this.obj <= finish , function(){ return 'expected ' + i(this.obj) + ' to be within ' + range } , function(){ return 'expected ' + i(this.obj) + ' to not be within ' + range }); return this; }; /** * Assert typeof / instance of * * @api public */ Assertion.prototype.a = Assertion.prototype.an = function (type) { if ('string' == typeof type) { // proper english in error msg var n = /^[aeiou]/.test(type) ? 'n' : ''; // typeof with support for 'array' this.assert( 'array' == type ? isArray(this.obj) : 'object' == type ? 'object' == typeof this.obj && null !== this.obj : type == typeof this.obj , function(){ return 'expected ' + i(this.obj) + ' to be a' + n + ' ' + type } , function(){ return 'expected ' + i(this.obj) + ' not to be a' + n + ' ' + type }); } else { // instanceof var name = type.name || 'supplied constructor'; this.assert( this.obj instanceof type , function(){ return 'expected ' + i(this.obj) + ' to be an instance of ' + name } , function(){ return 'expected ' + i(this.obj) + ' not to be an instance of ' + name }); } return this; }; /** * Assert numeric value above _n_. * * @param {Number} n * @api public */ Assertion.prototype.greaterThan = Assertion.prototype.above = function (n) { this.assert( this.obj > n , function(){ return 'expected ' + i(this.obj) + ' to be above ' + n } , function(){ return 'expected ' + i(this.obj) + ' to be below ' + n }); return this; }; /** * Assert numeric value below _n_. * * @param {Number} n * @api public */ Assertion.prototype.lessThan = Assertion.prototype.below = function (n) { this.assert( this.obj < n , function(){ return 'expected ' + i(this.obj) + ' to be below ' + n } , function(){ return 'expected ' + i(this.obj) + ' to be above ' + n }); return this; }; /** * Assert string value matches _regexp_. * * @param {RegExp} regexp * @api public */ Assertion.prototype.match = function (regexp) { this.assert( regexp.exec(this.obj) , function(){ return 'expected ' + i(this.obj) + ' to match ' + regexp } , function(){ return 'expected ' + i(this.obj) + ' not to match ' + regexp }); return this; }; /** * Assert property "length" exists and has value of _n_. * * @param {Number} n * @api public */ Assertion.prototype.length = function (n) { expect(this.obj).to.have.property('length'); var len = this.obj.length; this.assert( n == len , function(){ return 'expected ' + i(this.obj) + ' to have a length of ' + n + ' but got ' + len } , function(){ return 'expected ' + i(this.obj) + ' to not have a length of ' + len }); return this; }; /** * Assert property _name_ exists, with optional _val_. * * @param {String} name * @param {Mixed} val * @api public */ Assertion.prototype.property = function (name, val) { if (this.flags.own) { this.assert( Object.prototype.hasOwnProperty.call(this.obj, name) , function(){ return 'expected ' + i(this.obj) + ' to have own property ' + i(name) } , function(){ return 'expected ' + i(this.obj) + ' to not have own property ' + i(name) }); return this; } if (this.flags.not && undefined !== val) { if (undefined === this.obj[name]) { throw new Error(i(this.obj) + ' has no property ' + i(name)); } } else { var hasProp; try { hasProp = name in this.obj } catch (e) { hasProp = undefined !== this.obj[name] } this.assert( hasProp , function(){ return 'expected ' + i(this.obj) + ' to have a property ' + i(name) } , function(){ return 'expected ' + i(this.obj) + ' to not have a property ' + i(name) }); } if (undefined !== val) { this.assert( val === this.obj[name] , function(){ return 'expected ' + i(this.obj) + ' to have a property ' + i(name) + ' of ' + i(val) + ', but got ' + i(this.obj[name]) } , function(){ return 'expected ' + i(this.obj) + ' to not have a property ' + i(name) + ' of ' + i(val) }); } this.obj = this.obj[name]; return this; }; /** * Assert that the array contains _obj_ or string contains _obj_. * * @param {Mixed} obj|string * @api public */ Assertion.prototype.string = Assertion.prototype.contain = function (obj) { if ('string' == typeof this.obj) { this.assert( ~this.obj.indexOf(obj) , function(){ return 'expected ' + i(this.obj) + ' to contain ' + i(obj) } , function(){ return 'expected ' + i(this.obj) + ' to not contain ' + i(obj) }); } else { this.assert( ~indexOf(this.obj, obj) , function(){ return 'expected ' + i(this.obj) + ' to contain ' + i(obj) } , function(){ return 'expected ' + i(this.obj) + ' to not contain ' + i(obj) }); } return this; }; /** * Assert exact keys or inclusion of keys by using * the `.own` modifier. * * @param {Array|String ...} keys * @api public */ Assertion.prototype.key = Assertion.prototype.keys = function ($keys) { var str , ok = true; $keys = isArray($keys) ? $keys : Array.prototype.slice.call(arguments); if (!$keys.length) throw new Error('keys required'); var actual = keys(this.obj) , len = $keys.length; // Inclusion ok = every($keys, function (key) { return ~indexOf(actual, key); }); // Strict if (!this.flags.not && this.flags.only) { ok = ok && $keys.length == actual.length; } // Key string if (len > 1) { $keys = map($keys, function (key) { return i(key); }); var last = $keys.pop(); str = $keys.join(', ') + ', and ' + last; } else { str = i($keys[0]); } // Form str = (len > 1 ? 'keys ' : 'key ') + str; // Have / include str = (!this.flags.only ? 'include ' : 'only have ') + str; // Assertion this.assert( ok , function(){ return 'expected ' + i(this.obj) + ' to ' + str } , function(){ return 'expected ' + i(this.obj) + ' to not ' + str }); return this; }; /** * Assert a failure. * * @param {String ...} custom message * @api public */ Assertion.prototype.fail = function (msg) { msg = msg || "explicit failure"; this.assert(false, msg, msg); return this; }; /** * Function bind implementation. */ function bind (fn, scope) { return function () { return fn.apply(scope, arguments); } } /** * Array every compatibility * * @see bit.ly/5Fq1N2 * @api public */ function every (arr, fn, thisObj) { var scope = thisObj || global; for (var i = 0, j = arr.length; i < j; ++i) { if (!fn.call(scope, arr[i], i, arr)) { return false; } } return true; }; /** * Array indexOf compatibility. * * @see bit.ly/a5Dxa2 * @api public */ function indexOf (arr, o, i) { if (Array.prototype.indexOf) { return Array.prototype.indexOf.call(arr, o, i); } if (arr.length === undefined) { return -1; } for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0 ; i < j && arr[i] !== o; i++); return j <= i ? -1 : i; }; // https://gist.github.com/1044128/ var getOuterHTML = function(element) { if ('outerHTML' in element) return element.outerHTML; var ns = "http://www.w3.org/1999/xhtml"; var container = document.createElementNS(ns, '_'); var elemProto = (window.HTMLElement || window.Element).prototype; var xmlSerializer = new XMLSerializer(); var html; if (document.xmlVersion) { return xmlSerializer.serializeToString(element); } else { container.appendChild(element.cloneNode(false)); html = container.innerHTML.replace('><', '>' + element.innerHTML + '<'); container.innerHTML = ''; return html; } }; // Returns true if object is a DOM element. var isDOMElement = function (object) { if (typeof HTMLElement === 'object') { return object instanceof HTMLElement; } else { return object && typeof object === 'object' && object.nodeType === 1 && typeof object.nodeName === 'string'; } }; /** * Inspects an object. * * @see taken from node.js `util` module (copyright Joyent, MIT license) * @api private */ function i (obj, showHidden, depth) { var seen = []; function stylize (str) { return str; }; function format (value, recurseTimes) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it if (value && typeof value.inspect === 'function' && // Filter out the util module, it's inspect function is special value !== exports && // Also filter out any prototype objects using the circular check. !(value.constructor && value.constructor.prototype === value)) { return value.inspect(recurseTimes); } // Primitive types cannot have properties switch (typeof value) { case 'undefined': return stylize('undefined', 'undefined'); case 'string': var simple = '\'' + json.stringify(value).replace(/^"|"$/g, '') .replace(/'/g, "\\'") .replace(/\\"/g, '"') + '\''; return stylize(simple, 'string'); case 'number': return stylize('' + value, 'number'); case 'boolean': return stylize('' + value, 'boolean'); } // For some reason typeof null is "object", so special case here. if (value === null) { return stylize('null', 'null'); } if (isDOMElement(value)) { return getOuterHTML(value); } // Look up the keys of the object. var visible_keys = keys(value); var $keys = showHidden ? Object.getOwnPropertyNames(value) : visible_keys; // Functions without properties can be shortcutted. if (typeof value === 'function' && $keys.length === 0) { if (isRegExp(value)) { return stylize('' + value, 'regexp'); } else { var name = value.name ? ': ' + value.name : ''; return stylize('[Function' + name + ']', 'special'); } } // Dates without properties can be shortcutted if (isDate(value) && $keys.length === 0) { return stylize(value.toUTCString(), 'date'); } var base, type, braces; // Determine the object type if (isArray(value)) { type = 'Array'; braces = ['[', ']']; } else { type = 'Object'; braces = ['{', '}']; } // Make functions say that they are functions if (typeof value === 'function') { var n = value.name ? ': ' + value.name : ''; base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; } else { base = ''; } // Make dates with properties first say the date if (isDate(value)) { base = ' ' + value.toUTCString(); } if ($keys.length === 0) { return braces[0] + base + braces[1]; } if (recurseTimes < 0) { if (isRegExp(value)) { return stylize('' + value, 'regexp'); } else { return stylize('[Object]', 'special'); } } seen.push(value); var output = map($keys, function (key) { var name, str; if (value.__lookupGetter__) { if (value.__lookupGetter__(key)) { if (value.__lookupSetter__(key)) { str = stylize('[Getter/Setter]', 'special'); } else { str = stylize('[Getter]', 'special'); } } else { if (value.__lookupSetter__(key)) { str = stylize('[Setter]', 'special'); } } } if (indexOf(visible_keys, key) < 0) { name = '[' + key + ']'; } if (!str) { if (indexOf(seen, value[key]) < 0) { if (recurseTimes === null) { str = format(value[key]); } else { str = format(value[key], recurseTimes - 1); } if (str.indexOf('\n') > -1) { if (isArray(value)) { str = map(str.split('\n'), function (line) { return ' ' + line; }).join('\n').substr(2); } else { str = '\n' + map(str.split('\n'), function (line) { return ' ' + line; }).join('\n'); } } } else { str = stylize('[Circular]', 'special'); } } if (typeof name === 'undefined') { if (type === 'Array' && key.match(/^\d+$/)) { return str; } name = json.stringify('' + key); if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { name = name.substr(1, name.length - 2); name = stylize(name, 'name'); } else { name = name.replace(/'/g, "\\'") .replace(/\\"/g, '"') .replace(/(^"|"$)/g, "'"); name = stylize(name, 'string'); } } return name + ': ' + str; }); seen.pop(); var numLinesEst = 0; var length = reduce(output, function (prev, cur) { numLinesEst++; if (indexOf(cur, '\n') >= 0) numLinesEst++; return prev + cur.length + 1; }, 0); if (length > 50) { output = braces[0] + (base === '' ? '' : base + '\n ') + ' ' + output.join(',\n ') + ' ' + braces[1]; } else { output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; } return output; } return format(obj, (typeof depth === 'undefined' ? 2 : depth)); }; function isArray (ar) { return Object.prototype.toString.call(ar) == '[object Array]'; }; function isRegExp(re) { var s; try { s = '' + re; } catch (e) { return false; } return re instanceof RegExp || // easy case // duck-type for context-switching evalcx case typeof(re) === 'function' && re.constructor.name === 'RegExp' && re.compile && re.test && re.exec && s.match(/^\/.*\/[gim]{0,3}$/); }; function isDate(d) { if (d instanceof Date) return true; return false; }; function keys (obj) { if (Object.keys) { return Object.keys(obj); } var keys = []; for (var i in obj) { if (Object.prototype.hasOwnProperty.call(obj, i)) { keys.push(i); } } return keys; } function map (arr, mapper, that) { if (Array.prototype.map) { return Array.prototype.map.call(arr, mapper, that); } var other= new Array(arr.length); for (var i= 0, n = arr.length; i= 2) { var rv = arguments[1]; } else { do { if (i in this) { rv = this[i++]; break; } // if array contains no values, no initial value to return if (++i >= len) throw new TypeError(); } while (true); } for (; i < len; i++) { if (i in this) rv = fun.call(null, rv, this[i], i, this); } return rv; }; /** * Asserts deep equality * * @see taken from node.js `assert` module (copyright Joyent, MIT license) * @api private */ expect.eql = function eql (actual, expected) { // 7.1. All identical values are equivalent, as determined by ===. if (actual === expected) { return true; } else if ('undefined' != typeof Buffer && Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { if (actual.length != expected.length) return false; for (var i = 0; i < actual.length; i++) { if (actual[i] !== expected[i]) return false; } return true; // 7.2. If the expected value is a Date object, the actual value is // equivalent if it is also a Date object that refers to the same time. } else if (actual instanceof Date && expected instanceof Date) { return actual.getTime() === expected.getTime(); // 7.3. Other pairs that do not both pass typeof value == "object", // equivalence is determined by ==. } else if (typeof actual != 'object' && typeof expected != 'object') { return actual == expected; // 7.4. For all other Object pairs, including Array objects, equivalence is // determined by having the same number of owned properties (as verified // with Object.prototype.hasOwnProperty.call), the same set of keys // (although not necessarily the same order), equivalent values for every // corresponding key, and an identical "prototype" property. Note: this // accounts for both named and indexed properties on Arrays. } else { return objEquiv(actual, expected); } } function isUndefinedOrNull (value) { return value === null || value === undefined; } function isArguments (object) { return Object.prototype.toString.call(object) == '[object Arguments]'; } function objEquiv (a, b) { if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) return false; // an identical "prototype" property. if (a.prototype !== b.prototype) return false; //~~~I've managed to break Object.keys through screwy arguments passing. // Converting to array solves the problem. if (isArguments(a)) { if (!isArguments(b)) { return false; } a = pSlice.call(a); b = pSlice.call(b); return expect.eql(a, b); } try{ var ka = keys(a), kb = keys(b), key, i; } catch (e) {//happens when one is a string literal and the other isn't return false; } // having the same number of owned properties (keys incorporates hasOwnProperty) if (ka.length != kb.length) return false; //the same set of keys (although not necessarily the same order), ka.sort(); kb.sort(); //~~~cheap key test for (i = ka.length - 1; i >= 0; i--) { if (ka[i] != kb[i]) return false; } //equivalent values for every corresponding key, and //~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; if (!expect.eql(a[key], b[key])) return false; } return true; } var json = (function () { "use strict"; if ('object' == typeof JSON && JSON.parse && JSON.stringify) { return { parse: nativeJSON.parse , stringify: nativeJSON.stringify } } var JSON = {}; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } function date(d, key) { return isFinite(d.valueOf()) ? d.getUTCFullYear() + '-' + f(d.getUTCMonth() + 1) + '-' + f(d.getUTCDate()) + 'T' + f(d.getUTCHours()) + ':' + f(d.getUTCMinutes()) + ':' + f(d.getUTCSeconds()) + 'Z' : null; }; var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value instanceof Date) { value = date(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; // If the JSON object does not yet have a parse method, give it one. JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; return JSON; })(); if ('undefined' != typeof window) { window.expect = module.exports; } })( this , 'undefined' != typeof module ? module : {} , 'undefined' != typeof exports ? exports : {} ); Leaflet.markercluster-0.5.0/spec/happen.js000066400000000000000000000063151270301646500205260ustar00rootroot00000000000000// https://github.com/tmcw/happen !(function(context) { var h = {}; // Make inheritance bearable: clone one level of properties function extend(child, parent) { for (var property in parent) { if (typeof child[property] == 'undefined') { child[property] = parent[property]; } } return child; } h.once = function(x, o) { var evt; if (o.type.slice(0, 3) === 'key') { if (typeof Event === 'function') { evt = new Event(o.type); evt.keyCode = o.keyCode || 0; evt.charCode = o.charCode || 0; evt.shift = o.shift || false; evt.meta = o.meta || false; evt.ctrl = o.ctrl || false; evt.alt = o.alt || false; } else { evt = document.createEvent('KeyboardEvent'); // https://developer.mozilla.org/en/DOM/event.initKeyEvent // https://developer.mozilla.org/en/DOM/KeyboardEvent evt[(evt.initKeyEvent) ? 'initKeyEvent' : 'initKeyboardEvent']( o.type, // in DOMString typeArg, true, // in boolean canBubbleArg, true, // in boolean cancelableArg, null, // in nsIDOMAbstractView viewArg, Specifies UIEvent.view. This value may be null. o.ctrl || false, // in boolean ctrlKeyArg, o.alt || false, // in boolean altKeyArg, o.shift || false, // in boolean shiftKeyArg, o.meta || false, // in boolean metaKeyArg, o.keyCode || 0, // in unsigned long keyCodeArg, o.charCode || 0 // in unsigned long charCodeArg); ); } } else { evt = document.createEvent('MouseEvents'); // https://developer.mozilla.org/en/DOM/event.initMouseEvent evt.initMouseEvent(o.type, true, // canBubble true, // cancelable window, // 'AbstractView' o.clicks || 0, // click count o.screenX || 0, // screenX o.screenY || 0, // screenY o.clientX || 0, // clientX o.clientY || 0, // clientY o.ctrl || 0, // ctrl o.alt || false, // alt o.shift || false, // shift o.meta || false, // meta o.button || false, // mouse button null // relatedTarget ); } x.dispatchEvent(evt); }; var shortcuts = ['click', 'mousedown', 'mouseup', 'mousemove', 'keydown', 'keyup', 'keypress'], s, i = 0; while (s = shortcuts[i++]) { h[s] = (function(s) { return function(x, o) { h.once(x, extend(o || {}, { type: s })); }; })(s); } h.dblclick = function(x, o) { h.once(x, extend(o || {}, { type: 'dblclick', clicks: 2 })); }; this.happen = h; if (typeof module !== 'undefined') { module.exports = this.happen; } })(this); Leaflet.markercluster-0.5.0/spec/index.html000066400000000000000000000062301270301646500207060ustar00rootroot00000000000000 Spec Runner
Leaflet.markercluster-0.5.0/spec/karma.conf.js000066400000000000000000000031261270301646500212670ustar00rootroot00000000000000// Karma configuration var libSources = require(__dirname+'/../build/build.js').getFiles(); var leafletSources = require(__dirname+'/../node_modules/leaflet/build/build.js').getFiles(); // base path, that will be used to resolve files and exclude basePath = ''; for (var i=0; i < libSources.length; i++) { libSources[i] = "../" + libSources[i]; } for (var i=0; i < leafletSources.length; i++) { leafletSources[i] = "../node_modules/leaflet/" + leafletSources[i]; } // list of files / patterns to load in the browser files = [].concat([ "../node_modules/mocha/mocha.js", MOCHA_ADAPTER, "sinon.js", "expect.js" ], leafletSources, libSources, [ "after.js", "happen.js", "suites/SpecHelper.js", "suites/**/*.js" ]); // list of files to exclude exclude = [ ]; // test results reporter to use // possible values: 'dots', 'progress', 'junit' reporters = ['dots']; // web server port port = 8080; // cli runner port runnerPort = 9100; // enable / disable colors in the output (reporters and logs) colors = true; // level of logging // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG logLevel = LOG_WARN; // enable / disable watching file and executing tests whenever any file changes autoWatch = false; // Start these browsers, currently available: // - Chrome // - ChromeCanary // - Firefox // - Opera // - Safari (only Mac) // - PhantomJS // - IE (only Windows) browsers = ['PhantomJS']; // 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; Leaflet.markercluster-0.5.0/spec/sinon.js000066400000000000000000004017661270301646500204120ustar00rootroot00000000000000/** * Sinon.JS 1.6.0, 2013/02/18 * * @author Christian Johansen (christian@cjohansen.no) * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS * * (The BSD License) * * Copyright (c) 2010-2013, Christian Johansen, christian@cjohansen.no * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Christian Johansen nor the names of his contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var sinon = (function () { "use strict"; var buster = (function (setTimeout, B) { var isNode = typeof require == "function" && typeof module == "object"; var div = typeof document != "undefined" && document.createElement("div"); var F = function () {}; var buster = { bind: function bind(obj, methOrProp) { var method = typeof methOrProp == "string" ? obj[methOrProp] : methOrProp; var args = Array.prototype.slice.call(arguments, 2); return function () { var allArgs = args.concat(Array.prototype.slice.call(arguments)); return method.apply(obj, allArgs); }; }, partial: function partial(fn) { var args = [].slice.call(arguments, 1); return function () { return fn.apply(this, args.concat([].slice.call(arguments))); }; }, create: function create(object) { F.prototype = object; return new F(); }, extend: function extend(target) { if (!target) { return; } for (var i = 1, l = arguments.length, prop; i < l; ++i) { for (prop in arguments[i]) { target[prop] = arguments[i][prop]; } } return target; }, nextTick: function nextTick(callback) { if (typeof process != "undefined" && process.nextTick) { return process.nextTick(callback); } setTimeout(callback, 0); }, functionName: function functionName(func) { if (!func) return ""; if (func.displayName) return func.displayName; if (func.name) return func.name; var matches = func.toString().match(/function\s+([^\(]+)/m); return matches && matches[1] || ""; }, isNode: function isNode(obj) { if (!div) return false; try { obj.appendChild(div); obj.removeChild(div); } catch (e) { return false; } return true; }, isElement: function isElement(obj) { return obj && obj.nodeType === 1 && buster.isNode(obj); }, isArray: function isArray(arr) { return Object.prototype.toString.call(arr) == "[object Array]"; }, flatten: function flatten(arr) { var result = [], arr = arr || []; for (var i = 0, l = arr.length; i < l; ++i) { result = result.concat(buster.isArray(arr[i]) ? flatten(arr[i]) : arr[i]); } return result; }, each: function each(arr, callback) { for (var i = 0, l = arr.length; i < l; ++i) { callback(arr[i]); } }, map: function map(arr, callback) { var results = []; for (var i = 0, l = arr.length; i < l; ++i) { results.push(callback(arr[i])); } return results; }, parallel: function parallel(fns, callback) { function cb(err, res) { if (typeof callback == "function") { callback(err, res); callback = null; } } if (fns.length == 0) { return cb(null, []); } var remaining = fns.length, results = []; function makeDone(num) { return function done(err, result) { if (err) { return cb(err); } results[num] = result; if (--remaining == 0) { cb(null, results); } }; } for (var i = 0, l = fns.length; i < l; ++i) { fns[i](makeDone(i)); } }, series: function series(fns, callback) { function cb(err, res) { if (typeof callback == "function") { callback(err, res); } } var remaining = fns.slice(); var results = []; function callNext() { if (remaining.length == 0) return cb(null, results); var promise = remaining.shift()(next); if (promise && typeof promise.then == "function") { promise.then(buster.partial(next, null), next); } } function next(err, result) { if (err) return cb(err); results.push(result); callNext(); } callNext(); }, countdown: function countdown(num, done) { return function () { if (--num == 0) done(); }; } }; if (typeof process === "object" && typeof require === "function" && typeof module === "object") { var crypto = require("crypto"); var path = require("path"); buster.tmpFile = function (fileName) { var hashed = crypto.createHash("sha1"); hashed.update(fileName); var tmpfileName = hashed.digest("hex"); if (process.platform == "win32") { return path.join(process.env["TEMP"], tmpfileName); } else { return path.join("/tmp", tmpfileName); } }; } if (Array.prototype.some) { buster.some = function (arr, fn, thisp) { return arr.some(fn, thisp); }; } else { // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some buster.some = function (arr, fun, thisp) { if (arr == null) { throw new TypeError(); } arr = Object(arr); var len = arr.length >>> 0; if (typeof fun !== "function") { throw new TypeError(); } for (var i = 0; i < len; i++) { if (arr.hasOwnProperty(i) && fun.call(thisp, arr[i], i, arr)) { return true; } } return false; }; } if (Array.prototype.filter) { buster.filter = function (arr, fn, thisp) { return arr.filter(fn, thisp); }; } else { // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter buster.filter = function (fn, thisp) { if (this == null) { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; if (typeof fn != "function") { throw new TypeError(); } var res = []; for (var i = 0; i < len; i++) { if (i in t) { var val = t[i]; // in case fun mutates this if (fn.call(thisp, val, i, t)) { res.push(val); } } } return res; }; } if (isNode) { module.exports = buster; buster.eventEmitter = require("./buster-event-emitter"); Object.defineProperty(buster, "defineVersionGetter", { get: function () { return require("./define-version-getter"); } }); } return buster.extend(B || {}, buster); }(setTimeout, buster)); if (typeof buster === "undefined") { var buster = {}; } if (typeof module === "object" && typeof require === "function") { buster = require("buster-core"); } buster.format = buster.format || {}; buster.format.excludeConstructors = ["Object", /^.$/]; buster.format.quoteStrings = true; buster.format.ascii = (function () { var hasOwn = Object.prototype.hasOwnProperty; var specialObjects = []; if (typeof global != "undefined") { specialObjects.push({ obj: global, value: "[object global]" }); } if (typeof document != "undefined") { specialObjects.push({ obj: document, value: "[object HTMLDocument]" }); } if (typeof window != "undefined") { specialObjects.push({ obj: window, value: "[object Window]" }); } function keys(object) { var k = Object.keys && Object.keys(object) || []; if (k.length == 0) { for (var prop in object) { if (hasOwn.call(object, prop)) { k.push(prop); } } } return k.sort(); } function isCircular(object, objects) { if (typeof object != "object") { return false; } for (var i = 0, l = objects.length; i < l; ++i) { if (objects[i] === object) { return true; } } return false; } function ascii(object, processed, indent) { if (typeof object == "string") { var quote = typeof this.quoteStrings != "boolean" || this.quoteStrings; return processed || quote ? '"' + object + '"' : object; } if (typeof object == "function" && !(object instanceof RegExp)) { return ascii.func(object); } processed = processed || []; if (isCircular(object, processed)) { return "[Circular]"; } if (Object.prototype.toString.call(object) == "[object Array]") { return ascii.array.call(this, object, processed); } if (!object) { return "" + object; } if (buster.isElement(object)) { return ascii.element(object); } if (typeof object.toString == "function" && object.toString !== Object.prototype.toString) { return object.toString(); } for (var i = 0, l = specialObjects.length; i < l; i++) { if (object === specialObjects[i].obj) { return specialObjects[i].value; } } return ascii.object.call(this, object, processed, indent); } ascii.func = function (func) { return "function " + buster.functionName(func) + "() {}"; }; ascii.array = function (array, processed) { processed = processed || []; processed.push(array); var pieces = []; for (var i = 0, l = array.length; i < l; ++i) { pieces.push(ascii.call(this, array[i], processed)); } return "[" + pieces.join(", ") + "]"; }; ascii.object = function (object, processed, indent) { processed = processed || []; processed.push(object); indent = indent || 0; var pieces = [], properties = keys(object), prop, str, obj; var is = ""; var length = 3; for (var i = 0, l = indent; i < l; ++i) { is += " "; } for (i = 0, l = properties.length; i < l; ++i) { prop = properties[i]; obj = object[prop]; if (isCircular(obj, processed)) { str = "[Circular]"; } else { str = ascii.call(this, obj, processed, indent + 2); } str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str; length += str.length; pieces.push(str); } var cons = ascii.constructorName.call(this, object); var prefix = cons ? "[" + cons + "] " : "" return (length + indent) > 80 ? prefix + "{\n " + is + pieces.join(",\n " + is) + "\n" + is + "}" : prefix + "{ " + pieces.join(", ") + " }"; }; ascii.element = function (element) { var tagName = element.tagName.toLowerCase(); var attrs = element.attributes, attribute, pairs = [], attrName; for (var i = 0, l = attrs.length; i < l; ++i) { attribute = attrs.item(i); attrName = attribute.nodeName.toLowerCase().replace("html:", ""); if (attrName == "contenteditable" && attribute.nodeValue == "inherit") { continue; } if (!!attribute.nodeValue) { pairs.push(attrName + "=\"" + attribute.nodeValue + "\""); } } var formatted = "<" + tagName + (pairs.length > 0 ? " " : ""); var content = element.innerHTML; if (content.length > 20) { content = content.substr(0, 20) + "[...]"; } var res = formatted + pairs.join(" ") + ">" + content + ""; return res.replace(/ contentEditable="inherit"/, ""); }; ascii.constructorName = function (object) { var name = buster.functionName(object && object.constructor); var excludes = this.excludeConstructors || buster.format.excludeConstructors || []; for (var i = 0, l = excludes.length; i < l; ++i) { if (typeof excludes[i] == "string" && excludes[i] == name) { return ""; } else if (excludes[i].test && excludes[i].test(name)) { return ""; } } return name; }; return ascii; }()); if (typeof module != "undefined") { module.exports = buster.format; } /*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ /*global module, require, __dirname, document*/ /** * Sinon core utilities. For internal use only. * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ var sinon = (function (buster) { var div = typeof document != "undefined" && document.createElement("div"); var hasOwn = Object.prototype.hasOwnProperty; function isDOMNode(obj) { var success = false; try { obj.appendChild(div); success = div.parentNode == obj; } catch (e) { return false; } finally { try { obj.removeChild(div); } catch (e) { // Remove failed, not much we can do about that } } return success; } function isElement(obj) { return div && obj && obj.nodeType === 1 && isDOMNode(obj); } function isFunction(obj) { return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply); } function mirrorProperties(target, source) { for (var prop in source) { if (!hasOwn.call(target, prop)) { target[prop] = source[prop]; } } } var sinon = { wrapMethod: function wrapMethod(object, property, method) { if (!object) { throw new TypeError("Should wrap property of object"); } if (typeof method != "function") { throw new TypeError("Method wrapper should be function"); } var wrappedMethod = object[property]; if (!isFunction(wrappedMethod)) { throw new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " + property + " as function"); } if (wrappedMethod.restore && wrappedMethod.restore.sinon) { throw new TypeError("Attempted to wrap " + property + " which is already wrapped"); } if (wrappedMethod.calledBefore) { var verb = !!wrappedMethod.returns ? "stubbed" : "spied on"; throw new TypeError("Attempted to wrap " + property + " which is already " + verb); } // IE 8 does not support hasOwnProperty on the window object. var owned = hasOwn.call(object, property); object[property] = method; method.displayName = property; method.restore = function () { // For prototype properties try to reset by delete first. // If this fails (ex: localStorage on mobile safari) then force a reset // via direct assignment. if (!owned) { delete object[property]; } if (object[property] === method) { object[property] = wrappedMethod; } }; method.restore.sinon = true; mirrorProperties(method, wrappedMethod); return method; }, extend: function extend(target) { for (var i = 1, l = arguments.length; i < l; i += 1) { for (var prop in arguments[i]) { if (arguments[i].hasOwnProperty(prop)) { target[prop] = arguments[i][prop]; } // DONT ENUM bug, only care about toString if (arguments[i].hasOwnProperty("toString") && arguments[i].toString != target.toString) { target.toString = arguments[i].toString; } } } return target; }, create: function create(proto) { var F = function () {}; F.prototype = proto; return new F(); }, deepEqual: function deepEqual(a, b) { if (sinon.match && sinon.match.isMatcher(a)) { return a.test(b); } if (typeof a != "object" || typeof b != "object") { return a === b; } if (isElement(a) || isElement(b)) { return a === b; } if (a === b) { return true; } if ((a === null && b !== null) || (a !== null && b === null)) { return false; } var aString = Object.prototype.toString.call(a); if (aString != Object.prototype.toString.call(b)) { return false; } if (aString == "[object Array]") { if (a.length !== b.length) { return false; } for (var i = 0, l = a.length; i < l; i += 1) { if (!deepEqual(a[i], b[i])) { return false; } } return true; } var prop, aLength = 0, bLength = 0; for (prop in a) { aLength += 1; if (!deepEqual(a[prop], b[prop])) { return false; } } for (prop in b) { bLength += 1; } if (aLength != bLength) { return false; } return true; }, functionName: function functionName(func) { var name = func.displayName || func.name; // Use function decomposition as a last resort to get function // name. Does not rely on function decomposition to work - if it // doesn't debugging will be slightly less informative // (i.e. toString will say 'spy' rather than 'myFunc'). if (!name) { var matches = func.toString().match(/function ([^\s\(]+)/); name = matches && matches[1]; } return name; }, functionToString: function toString() { if (this.getCall && this.callCount) { var thisValue, prop, i = this.callCount; while (i--) { thisValue = this.getCall(i).thisValue; for (prop in thisValue) { if (thisValue[prop] === this) { return prop; } } } } return this.displayName || "sinon fake"; }, getConfig: function (custom) { var config = {}; custom = custom || {}; var defaults = sinon.defaultConfig; for (var prop in defaults) { if (defaults.hasOwnProperty(prop)) { config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop]; } } return config; }, format: function (val) { return "" + val; }, defaultConfig: { injectIntoThis: true, injectInto: null, properties: ["spy", "stub", "mock", "clock", "server", "requests"], useFakeTimers: true, useFakeServer: true }, timesInWords: function timesInWords(count) { return count == 1 && "once" || count == 2 && "twice" || count == 3 && "thrice" || (count || 0) + " times"; }, calledInOrder: function (spies) { for (var i = 1, l = spies.length; i < l; i++) { if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) { return false; } } return true; }, orderByFirstCall: function (spies) { return spies.sort(function (a, b) { // uuid, won't ever be equal var aCall = a.getCall(0); var bCall = b.getCall(0); var aId = aCall && aCall.callId || -1; var bId = bCall && bCall.callId || -1; return aId < bId ? -1 : 1; }); }, log: function () {}, logError: function (label, err) { var msg = label + " threw exception: " sinon.log(msg + "[" + err.name + "] " + err.message); if (err.stack) { sinon.log(err.stack); } setTimeout(function () { err.message = msg + err.message; throw err; }, 0); }, typeOf: function (value) { if (value === null) { return "null"; } else if (value === undefined) { return "undefined"; } var string = Object.prototype.toString.call(value); return string.substring(8, string.length - 1).toLowerCase(); }, createStubInstance: function (constructor) { if (typeof constructor !== "function") { throw new TypeError("The constructor should be a function."); } return sinon.stub(sinon.create(constructor.prototype)); } }; var isNode = typeof module == "object" && typeof require == "function"; if (isNode) { try { buster = { format: require("buster-format") }; } catch (e) {} module.exports = sinon; module.exports.spy = require("./sinon/spy"); module.exports.stub = require("./sinon/stub"); module.exports.mock = require("./sinon/mock"); module.exports.collection = require("./sinon/collection"); module.exports.assert = require("./sinon/assert"); module.exports.sandbox = require("./sinon/sandbox"); module.exports.test = require("./sinon/test"); module.exports.testCase = require("./sinon/test_case"); module.exports.assert = require("./sinon/assert"); module.exports.match = require("./sinon/match"); } if (buster) { var formatter = sinon.create(buster.format); formatter.quoteStrings = false; sinon.format = function () { return formatter.ascii.apply(formatter, arguments); }; } else if (isNode) { try { var util = require("util"); sinon.format = function (value) { return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value; }; } catch (e) { /* Node, but no util module - would be very old, but better safe than sorry */ } } return sinon; }(typeof buster == "object" && buster)); /* @depend ../sinon.js */ /*jslint eqeqeq: false, onevar: false, plusplus: false*/ /*global module, require, sinon*/ /** * Match functions * * @author Maximilian Antoni (mail@maxantoni.de) * @license BSD * * Copyright (c) 2012 Maximilian Antoni */ (function (sinon) { var commonJSModule = typeof module == "object" && typeof require == "function"; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon) { return; } function assertType(value, type, name) { var actual = sinon.typeOf(value); if (actual !== type) { throw new TypeError("Expected type of " + name + " to be " + type + ", but was " + actual); } } var matcher = { toString: function () { return this.message; } }; function isMatcher(object) { return matcher.isPrototypeOf(object); } function matchObject(expectation, actual) { if (actual === null || actual === undefined) { return false; } for (var key in expectation) { if (expectation.hasOwnProperty(key)) { var exp = expectation[key]; var act = actual[key]; if (match.isMatcher(exp)) { if (!exp.test(act)) { return false; } } else if (sinon.typeOf(exp) === "object") { if (!matchObject(exp, act)) { return false; } } else if (!sinon.deepEqual(exp, act)) { return false; } } } return true; } matcher.or = function (m2) { if (!isMatcher(m2)) { throw new TypeError("Matcher expected"); } var m1 = this; var or = sinon.create(matcher); or.test = function (actual) { return m1.test(actual) || m2.test(actual); }; or.message = m1.message + ".or(" + m2.message + ")"; return or; }; matcher.and = function (m2) { if (!isMatcher(m2)) { throw new TypeError("Matcher expected"); } var m1 = this; var and = sinon.create(matcher); and.test = function (actual) { return m1.test(actual) && m2.test(actual); }; and.message = m1.message + ".and(" + m2.message + ")"; return and; }; var match = function (expectation, message) { var m = sinon.create(matcher); var type = sinon.typeOf(expectation); switch (type) { case "object": if (typeof expectation.test === "function") { m.test = function (actual) { return expectation.test(actual) === true; }; m.message = "match(" + sinon.functionName(expectation.test) + ")"; return m; } var str = []; for (var key in expectation) { if (expectation.hasOwnProperty(key)) { str.push(key + ": " + expectation[key]); } } m.test = function (actual) { return matchObject(expectation, actual); }; m.message = "match(" + str.join(", ") + ")"; break; case "number": m.test = function (actual) { return expectation == actual; }; break; case "string": m.test = function (actual) { if (typeof actual !== "string") { return false; } return actual.indexOf(expectation) !== -1; }; m.message = "match(\"" + expectation + "\")"; break; case "regexp": m.test = function (actual) { if (typeof actual !== "string") { return false; } return expectation.test(actual); }; break; case "function": m.test = expectation; if (message) { m.message = message; } else { m.message = "match(" + sinon.functionName(expectation) + ")"; } break; default: m.test = function (actual) { return sinon.deepEqual(expectation, actual); }; } if (!m.message) { m.message = "match(" + expectation + ")"; } return m; }; match.isMatcher = isMatcher; match.any = match(function () { return true; }, "any"); match.defined = match(function (actual) { return actual !== null && actual !== undefined; }, "defined"); match.truthy = match(function (actual) { return !!actual; }, "truthy"); match.falsy = match(function (actual) { return !actual; }, "falsy"); match.same = function (expectation) { return match(function (actual) { return expectation === actual; }, "same(" + expectation + ")"); }; match.typeOf = function (type) { assertType(type, "string", "type"); return match(function (actual) { return sinon.typeOf(actual) === type; }, "typeOf(\"" + type + "\")"); }; match.instanceOf = function (type) { assertType(type, "function", "type"); return match(function (actual) { return actual instanceof type; }, "instanceOf(" + sinon.functionName(type) + ")"); }; function createPropertyMatcher(propertyTest, messagePrefix) { return function (property, value) { assertType(property, "string", "property"); var onlyProperty = arguments.length === 1; var message = messagePrefix + "(\"" + property + "\""; if (!onlyProperty) { message += ", " + value; } message += ")"; return match(function (actual) { if (actual === undefined || actual === null || !propertyTest(actual, property)) { return false; } return onlyProperty || sinon.deepEqual(value, actual[property]); }, message); }; } match.has = createPropertyMatcher(function (actual, property) { if (typeof actual === "object") { return property in actual; } return actual[property] !== undefined; }, "has"); match.hasOwn = createPropertyMatcher(function (actual, property) { return actual.hasOwnProperty(property); }, "hasOwn"); match.bool = match.typeOf("boolean"); match.number = match.typeOf("number"); match.string = match.typeOf("string"); match.object = match.typeOf("object"); match.func = match.typeOf("function"); match.array = match.typeOf("array"); match.regexp = match.typeOf("regexp"); match.date = match.typeOf("date"); if (commonJSModule) { module.exports = match; } else { sinon.match = match; } }(typeof sinon == "object" && sinon || null)); /** * @depend ../sinon.js * @depend match.js */ /*jslint eqeqeq: false, onevar: false, plusplus: false*/ /*global module, require, sinon*/ /** * Spy functions * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function (sinon) { var commonJSModule = typeof module == "object" && typeof require == "function"; var spyCall; var callId = 0; var push = [].push; var slice = Array.prototype.slice; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon) { return; } function spy(object, property) { if (!property && typeof object == "function") { return spy.create(object); } if (!object && !property) { return spy.create(function () { }); } var method = object[property]; return sinon.wrapMethod(object, property, spy.create(method)); } sinon.extend(spy, (function () { function delegateToCalls(api, method, matchAny, actual, notCalled) { api[method] = function () { if (!this.called) { if (notCalled) { return notCalled.apply(this, arguments); } return false; } var currentCall; var matches = 0; for (var i = 0, l = this.callCount; i < l; i += 1) { currentCall = this.getCall(i); if (currentCall[actual || method].apply(currentCall, arguments)) { matches += 1; if (matchAny) { return true; } } } return matches === this.callCount; }; } function matchingFake(fakes, args, strict) { if (!fakes) { return; } var alen = args.length; for (var i = 0, l = fakes.length; i < l; i++) { if (fakes[i].matches(args, strict)) { return fakes[i]; } } } function incrementCallCount() { this.called = true; this.callCount += 1; this.notCalled = false; this.calledOnce = this.callCount == 1; this.calledTwice = this.callCount == 2; this.calledThrice = this.callCount == 3; } function createCallProperties() { this.firstCall = this.getCall(0); this.secondCall = this.getCall(1); this.thirdCall = this.getCall(2); this.lastCall = this.getCall(this.callCount - 1); } var vars = "a,b,c,d,e,f,g,h,i,j,k,l"; function createProxy(func) { // Retain the function length: var p; if (func.length) { eval("p = (function proxy(" + vars.substring(0, func.length * 2 - 1) + ") { return p.invoke(func, this, slice.call(arguments)); });"); } else { p = function proxy() { return p.invoke(func, this, slice.call(arguments)); }; } return p; } var uuid = 0; // Public API var spyApi = { reset: function () { this.called = false; this.notCalled = true; this.calledOnce = false; this.calledTwice = false; this.calledThrice = false; this.callCount = 0; this.firstCall = null; this.secondCall = null; this.thirdCall = null; this.lastCall = null; this.args = []; this.returnValues = []; this.thisValues = []; this.exceptions = []; this.callIds = []; if (this.fakes) { for (var i = 0; i < this.fakes.length; i++) { this.fakes[i].reset(); } } }, create: function create(func) { var name; if (typeof func != "function") { func = function () { }; } else { name = sinon.functionName(func); } var proxy = createProxy(func); sinon.extend(proxy, spy); delete proxy.create; sinon.extend(proxy, func); proxy.reset(); proxy.prototype = func.prototype; proxy.displayName = name || "spy"; proxy.toString = sinon.functionToString; proxy._create = sinon.spy.create; proxy.id = "spy#" + uuid++; return proxy; }, invoke: function invoke(func, thisValue, args) { var matching = matchingFake(this.fakes, args); var exception, returnValue; incrementCallCount.call(this); push.call(this.thisValues, thisValue); push.call(this.args, args); push.call(this.callIds, callId++); try { if (matching) { returnValue = matching.invoke(func, thisValue, args); } else { returnValue = (this.func || func).apply(thisValue, args); } } catch (e) { push.call(this.returnValues, undefined); exception = e; throw e; } finally { push.call(this.exceptions, exception); } push.call(this.returnValues, returnValue); createCallProperties.call(this); return returnValue; }, getCall: function getCall(i) { if (i < 0 || i >= this.callCount) { return null; } return spyCall.create(this, this.thisValues[i], this.args[i], this.returnValues[i], this.exceptions[i], this.callIds[i]); }, calledBefore: function calledBefore(spyFn) { if (!this.called) { return false; } if (!spyFn.called) { return true; } return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1]; }, calledAfter: function calledAfter(spyFn) { if (!this.called || !spyFn.called) { return false; } return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1]; }, withArgs: function () { var args = slice.call(arguments); if (this.fakes) { var match = matchingFake(this.fakes, args, true); if (match) { return match; } } else { this.fakes = []; } var original = this; var fake = this._create(); fake.matchingAguments = args; push.call(this.fakes, fake); fake.withArgs = function () { return original.withArgs.apply(original, arguments); }; for (var i = 0; i < this.args.length; i++) { if (fake.matches(this.args[i])) { incrementCallCount.call(fake); push.call(fake.thisValues, this.thisValues[i]); push.call(fake.args, this.args[i]); push.call(fake.returnValues, this.returnValues[i]); push.call(fake.exceptions, this.exceptions[i]); push.call(fake.callIds, this.callIds[i]); } } createCallProperties.call(fake); return fake; }, matches: function (args, strict) { var margs = this.matchingAguments; if (margs.length <= args.length && sinon.deepEqual(margs, args.slice(0, margs.length))) { return !strict || margs.length == args.length; } }, printf: function (format) { var spy = this; var args = slice.call(arguments, 1); var formatter; return (format || "").replace(/%(.)/g, function (match, specifyer) { formatter = spyApi.formatters[specifyer]; if (typeof formatter == "function") { return formatter.call(null, spy, args); } else if (!isNaN(parseInt(specifyer), 10)) { return sinon.format(args[specifyer - 1]); } return "%" + specifyer; }); } }; delegateToCalls(spyApi, "calledOn", true); delegateToCalls(spyApi, "alwaysCalledOn", false, "calledOn"); delegateToCalls(spyApi, "calledWith", true); delegateToCalls(spyApi, "calledWithMatch", true); delegateToCalls(spyApi, "alwaysCalledWith", false, "calledWith"); delegateToCalls(spyApi, "alwaysCalledWithMatch", false, "calledWithMatch"); delegateToCalls(spyApi, "calledWithExactly", true); delegateToCalls(spyApi, "alwaysCalledWithExactly", false, "calledWithExactly"); delegateToCalls(spyApi, "neverCalledWith", false, "notCalledWith", function () { return true; }); delegateToCalls(spyApi, "neverCalledWithMatch", false, "notCalledWithMatch", function () { return true; }); delegateToCalls(spyApi, "threw", true); delegateToCalls(spyApi, "alwaysThrew", false, "threw"); delegateToCalls(spyApi, "returned", true); delegateToCalls(spyApi, "alwaysReturned", false, "returned"); delegateToCalls(spyApi, "calledWithNew", true); delegateToCalls(spyApi, "alwaysCalledWithNew", false, "calledWithNew"); delegateToCalls(spyApi, "callArg", false, "callArgWith", function () { throw new Error(this.toString() + " cannot call arg since it was not yet invoked."); }); spyApi.callArgWith = spyApi.callArg; delegateToCalls(spyApi, "callArgOn", false, "callArgOnWith", function () { throw new Error(this.toString() + " cannot call arg since it was not yet invoked."); }); spyApi.callArgOnWith = spyApi.callArgOn; delegateToCalls(spyApi, "yield", false, "yield", function () { throw new Error(this.toString() + " cannot yield since it was not yet invoked."); }); // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode. spyApi.invokeCallback = spyApi.yield; delegateToCalls(spyApi, "yieldOn", false, "yieldOn", function () { throw new Error(this.toString() + " cannot yield since it was not yet invoked."); }); delegateToCalls(spyApi, "yieldTo", false, "yieldTo", function (property) { throw new Error(this.toString() + " cannot yield to '" + property + "' since it was not yet invoked."); }); delegateToCalls(spyApi, "yieldToOn", false, "yieldToOn", function (property) { throw new Error(this.toString() + " cannot yield to '" + property + "' since it was not yet invoked."); }); spyApi.formatters = { "c": function (spy) { return sinon.timesInWords(spy.callCount); }, "n": function (spy) { return spy.toString(); }, "C": function (spy) { var calls = []; for (var i = 0, l = spy.callCount; i < l; ++i) { var stringifiedCall = " " + spy.getCall(i).toString(); if (/\n/.test(calls[i - 1])) { stringifiedCall = "\n" + stringifiedCall; } push.call(calls, stringifiedCall); } return calls.length > 0 ? "\n" + calls.join("\n") : ""; }, "t": function (spy) { var objects = []; for (var i = 0, l = spy.callCount; i < l; ++i) { push.call(objects, sinon.format(spy.thisValues[i])); } return objects.join(", "); }, "*": function (spy, args) { var formatted = []; for (var i = 0, l = args.length; i < l; ++i) { push.call(formatted, sinon.format(args[i])); } return formatted.join(", "); } }; return spyApi; }())); spyCall = (function () { function throwYieldError(proxy, text, args) { var msg = sinon.functionName(proxy) + text; if (args.length) { msg += " Received [" + slice.call(args).join(", ") + "]"; } throw new Error(msg); } var callApi = { create: function create(spy, thisValue, args, returnValue, exception, id) { var proxyCall = sinon.create(spyCall); delete proxyCall.create; proxyCall.proxy = spy; proxyCall.thisValue = thisValue; proxyCall.args = args; proxyCall.returnValue = returnValue; proxyCall.exception = exception; proxyCall.callId = typeof id == "number" && id || callId++; return proxyCall; }, calledOn: function calledOn(thisValue) { if (sinon.match && sinon.match.isMatcher(thisValue)) { return thisValue.test(this.thisValue); } return this.thisValue === thisValue; }, calledWith: function calledWith() { for (var i = 0, l = arguments.length; i < l; i += 1) { if (!sinon.deepEqual(arguments[i], this.args[i])) { return false; } } return true; }, calledWithMatch: function calledWithMatch() { for (var i = 0, l = arguments.length; i < l; i += 1) { var actual = this.args[i]; var expectation = arguments[i]; if (!sinon.match || !sinon.match(expectation).test(actual)) { return false; } } return true; }, calledWithExactly: function calledWithExactly() { return arguments.length == this.args.length && this.calledWith.apply(this, arguments); }, notCalledWith: function notCalledWith() { return !this.calledWith.apply(this, arguments); }, notCalledWithMatch: function notCalledWithMatch() { return !this.calledWithMatch.apply(this, arguments); }, returned: function returned(value) { return sinon.deepEqual(value, this.returnValue); }, threw: function threw(error) { if (typeof error == "undefined" || !this.exception) { return !!this.exception; } if (typeof error == "string") { return this.exception.name == error; } return this.exception === error; }, calledWithNew: function calledWithNew(thisValue) { return this.thisValue instanceof this.proxy; }, calledBefore: function (other) { return this.callId < other.callId; }, calledAfter: function (other) { return this.callId > other.callId; }, callArg: function (pos) { this.args[pos](); }, callArgOn: function (pos, thisValue) { this.args[pos].apply(thisValue); }, callArgWith: function (pos) { this.callArgOnWith.apply(this, [pos, null].concat(slice.call(arguments, 1))); }, callArgOnWith: function (pos, thisValue) { var args = slice.call(arguments, 2); this.args[pos].apply(thisValue, args); }, "yield": function () { this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0))); }, yieldOn: function (thisValue) { var args = this.args; for (var i = 0, l = args.length; i < l; ++i) { if (typeof args[i] === "function") { args[i].apply(thisValue, slice.call(arguments, 1)); return; } } throwYieldError(this.proxy, " cannot yield since no callback was passed.", args); }, yieldTo: function (prop) { this.yieldToOn.apply(this, [prop, null].concat(slice.call(arguments, 1))); }, yieldToOn: function (prop, thisValue) { var args = this.args; for (var i = 0, l = args.length; i < l; ++i) { if (args[i] && typeof args[i][prop] === "function") { args[i][prop].apply(thisValue, slice.call(arguments, 2)); return; } } throwYieldError(this.proxy, " cannot yield to '" + prop + "' since no callback was passed.", args); }, toString: function () { var callStr = this.proxy.toString() + "("; var args = []; for (var i = 0, l = this.args.length; i < l; ++i) { push.call(args, sinon.format(this.args[i])); } callStr = callStr + args.join(", ") + ")"; if (typeof this.returnValue != "undefined") { callStr += " => " + sinon.format(this.returnValue); } if (this.exception) { callStr += " !" + this.exception.name; if (this.exception.message) { callStr += "(" + this.exception.message + ")"; } } return callStr; } }; callApi.invokeCallback = callApi.yield; return callApi; }()); spy.spyCall = spyCall; // This steps outside the module sandbox and will be removed sinon.spyCall = spyCall; if (commonJSModule) { module.exports = spy; } else { sinon.spy = spy; } }(typeof sinon == "object" && sinon || null)); /** * @depend ../sinon.js * @depend spy.js */ /*jslint eqeqeq: false, onevar: false*/ /*global module, require, sinon*/ /** * Stub functions * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function (sinon) { var commonJSModule = typeof module == "object" && typeof require == "function"; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon) { return; } function stub(object, property, func) { if (!!func && typeof func != "function") { throw new TypeError("Custom stub should be function"); } var wrapper; if (func) { wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func; } else { wrapper = stub.create(); } if (!object && !property) { return sinon.stub.create(); } if (!property && !!object && typeof object == "object") { for (var prop in object) { if (typeof object[prop] === "function") { stub(object, prop); } } return object; } return sinon.wrapMethod(object, property, wrapper); } function getChangingValue(stub, property) { var index = stub.callCount - 1; var values = stub[property]; var prop = index in values ? values[index] : values[values.length - 1]; stub[property + "Last"] = prop; return prop; } function getCallback(stub, args) { var callArgAt = getChangingValue(stub, "callArgAts"); if (callArgAt < 0) { var callArgProp = getChangingValue(stub, "callArgProps"); for (var i = 0, l = args.length; i < l; ++i) { if (!callArgProp && typeof args[i] == "function") { return args[i]; } if (callArgProp && args[i] && typeof args[i][callArgProp] == "function") { return args[i][callArgProp]; } } return null; } return args[callArgAt]; } var join = Array.prototype.join; function getCallbackError(stub, func, args) { if (stub.callArgAtsLast < 0) { var msg; if (stub.callArgPropsLast) { msg = sinon.functionName(stub) + " expected to yield to '" + stub.callArgPropsLast + "', but no object with such a property was passed." } else { msg = sinon.functionName(stub) + " expected to yield, but no callback was passed." } if (args.length > 0) { msg += " Received [" + join.call(args, ", ") + "]"; } return msg; } return "argument at index " + stub.callArgAtsLast + " is not a function: " + func; } var nextTick = (function () { if (typeof process === "object" && typeof process.nextTick === "function") { return process.nextTick; } else if (typeof setImmediate === "function") { return setImmediate; } else { return function (callback) { setTimeout(callback, 0); }; } })(); function callCallback(stub, args) { if (stub.callArgAts.length > 0) { var func = getCallback(stub, args); if (typeof func != "function") { throw new TypeError(getCallbackError(stub, func, args)); } var callbackArguments = getChangingValue(stub, "callbackArguments"); var callbackContext = getChangingValue(stub, "callbackContexts"); if (stub.callbackAsync) { nextTick(function() { func.apply(callbackContext, callbackArguments); }); } else { func.apply(callbackContext, callbackArguments); } } } var uuid = 0; sinon.extend(stub, (function () { var slice = Array.prototype.slice, proto; function throwsException(error, message) { if (typeof error == "string") { this.exception = new Error(message || ""); this.exception.name = error; } else if (!error) { this.exception = new Error("Error"); } else { this.exception = error; } return this; } proto = { create: function create() { var functionStub = function () { callCallback(functionStub, arguments); if (functionStub.exception) { throw functionStub.exception; } else if (typeof functionStub.returnArgAt == 'number') { return arguments[functionStub.returnArgAt]; } else if (functionStub.returnThis) { return this; } return functionStub.returnValue; }; functionStub.id = "stub#" + uuid++; var orig = functionStub; functionStub = sinon.spy.create(functionStub); functionStub.func = orig; functionStub.callArgAts = []; functionStub.callbackArguments = []; functionStub.callbackContexts = []; functionStub.callArgProps = []; sinon.extend(functionStub, stub); functionStub._create = sinon.stub.create; functionStub.displayName = "stub"; functionStub.toString = sinon.functionToString; return functionStub; }, resetBehavior: function () { var i; this.callArgAts = []; this.callbackArguments = []; this.callbackContexts = []; this.callArgProps = []; delete this.returnValue; delete this.returnArgAt; this.returnThis = false; if (this.fakes) { for (i = 0; i < this.fakes.length; i++) { this.fakes[i].resetBehavior(); } } }, returns: function returns(value) { this.returnValue = value; return this; }, returnsArg: function returnsArg(pos) { if (typeof pos != "number") { throw new TypeError("argument index is not number"); } this.returnArgAt = pos; return this; }, returnsThis: function returnsThis() { this.returnThis = true; return this; }, "throws": throwsException, throwsException: throwsException, callsArg: function callsArg(pos) { if (typeof pos != "number") { throw new TypeError("argument index is not number"); } this.callArgAts.push(pos); this.callbackArguments.push([]); this.callbackContexts.push(undefined); this.callArgProps.push(undefined); return this; }, callsArgOn: function callsArgOn(pos, context) { if (typeof pos != "number") { throw new TypeError("argument index is not number"); } if (typeof context != "object") { throw new TypeError("argument context is not an object"); } this.callArgAts.push(pos); this.callbackArguments.push([]); this.callbackContexts.push(context); this.callArgProps.push(undefined); return this; }, callsArgWith: function callsArgWith(pos) { if (typeof pos != "number") { throw new TypeError("argument index is not number"); } this.callArgAts.push(pos); this.callbackArguments.push(slice.call(arguments, 1)); this.callbackContexts.push(undefined); this.callArgProps.push(undefined); return this; }, callsArgOnWith: function callsArgWith(pos, context) { if (typeof pos != "number") { throw new TypeError("argument index is not number"); } if (typeof context != "object") { throw new TypeError("argument context is not an object"); } this.callArgAts.push(pos); this.callbackArguments.push(slice.call(arguments, 2)); this.callbackContexts.push(context); this.callArgProps.push(undefined); return this; }, yields: function () { this.callArgAts.push(-1); this.callbackArguments.push(slice.call(arguments, 0)); this.callbackContexts.push(undefined); this.callArgProps.push(undefined); return this; }, yieldsOn: function (context) { if (typeof context != "object") { throw new TypeError("argument context is not an object"); } this.callArgAts.push(-1); this.callbackArguments.push(slice.call(arguments, 1)); this.callbackContexts.push(context); this.callArgProps.push(undefined); return this; }, yieldsTo: function (prop) { this.callArgAts.push(-1); this.callbackArguments.push(slice.call(arguments, 1)); this.callbackContexts.push(undefined); this.callArgProps.push(prop); return this; }, yieldsToOn: function (prop, context) { if (typeof context != "object") { throw new TypeError("argument context is not an object"); } this.callArgAts.push(-1); this.callbackArguments.push(slice.call(arguments, 2)); this.callbackContexts.push(context); this.callArgProps.push(prop); return this; } }; // create asynchronous versions of callsArg* and yields* methods for (var method in proto) { // need to avoid creating anotherasync versions of the newly added async methods if (proto.hasOwnProperty(method) && method.match(/^(callsArg|yields|thenYields$)/) && !method.match(/Async/)) { proto[method + 'Async'] = (function (syncFnName) { return function () { this.callbackAsync = true; return this[syncFnName].apply(this, arguments); }; })(method); } } return proto; }())); if (commonJSModule) { module.exports = stub; } else { sinon.stub = stub; } }(typeof sinon == "object" && sinon || null)); /** * @depend ../sinon.js * @depend stub.js */ /*jslint eqeqeq: false, onevar: false, nomen: false*/ /*global module, require, sinon*/ /** * Mock functions. * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function (sinon) { var commonJSModule = typeof module == "object" && typeof require == "function"; var push = [].push; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon) { return; } function mock(object) { if (!object) { return sinon.expectation.create("Anonymous mock"); } return mock.create(object); } sinon.mock = mock; sinon.extend(mock, (function () { function each(collection, callback) { if (!collection) { return; } for (var i = 0, l = collection.length; i < l; i += 1) { callback(collection[i]); } } return { create: function create(object) { if (!object) { throw new TypeError("object is null"); } var mockObject = sinon.extend({}, mock); mockObject.object = object; delete mockObject.create; return mockObject; }, expects: function expects(method) { if (!method) { throw new TypeError("method is falsy"); } if (!this.expectations) { this.expectations = {}; this.proxies = []; } if (!this.expectations[method]) { this.expectations[method] = []; var mockObject = this; sinon.wrapMethod(this.object, method, function () { return mockObject.invokeMethod(method, this, arguments); }); push.call(this.proxies, method); } var expectation = sinon.expectation.create(method); push.call(this.expectations[method], expectation); return expectation; }, restore: function restore() { var object = this.object; each(this.proxies, function (proxy) { if (typeof object[proxy].restore == "function") { object[proxy].restore(); } }); }, verify: function verify() { var expectations = this.expectations || {}; var messages = [], met = []; each(this.proxies, function (proxy) { each(expectations[proxy], function (expectation) { if (!expectation.met()) { push.call(messages, expectation.toString()); } else { push.call(met, expectation.toString()); } }); }); this.restore(); if (messages.length > 0) { sinon.expectation.fail(messages.concat(met).join("\n")); } else { sinon.expectation.pass(messages.concat(met).join("\n")); } return true; }, invokeMethod: function invokeMethod(method, thisValue, args) { var expectations = this.expectations && this.expectations[method]; var length = expectations && expectations.length || 0, i; for (i = 0; i < length; i += 1) { if (!expectations[i].met() && expectations[i].allowsCall(thisValue, args)) { return expectations[i].apply(thisValue, args); } } var messages = [], available, exhausted = 0; for (i = 0; i < length; i += 1) { if (expectations[i].allowsCall(thisValue, args)) { available = available || expectations[i]; } else { exhausted += 1; } push.call(messages, " " + expectations[i].toString()); } if (exhausted === 0) { return available.apply(thisValue, args); } messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({ proxy: method, args: args })); sinon.expectation.fail(messages.join("\n")); } }; }())); var times = sinon.timesInWords; sinon.expectation = (function () { var slice = Array.prototype.slice; var _invoke = sinon.spy.invoke; function callCountInWords(callCount) { if (callCount == 0) { return "never called"; } else { return "called " + times(callCount); } } function expectedCallCountInWords(expectation) { var min = expectation.minCalls; var max = expectation.maxCalls; if (typeof min == "number" && typeof max == "number") { var str = times(min); if (min != max) { str = "at least " + str + " and at most " + times(max); } return str; } if (typeof min == "number") { return "at least " + times(min); } return "at most " + times(max); } function receivedMinCalls(expectation) { var hasMinLimit = typeof expectation.minCalls == "number"; return !hasMinLimit || expectation.callCount >= expectation.minCalls; } function receivedMaxCalls(expectation) { if (typeof expectation.maxCalls != "number") { return false; } return expectation.callCount == expectation.maxCalls; } return { minCalls: 1, maxCalls: 1, create: function create(methodName) { var expectation = sinon.extend(sinon.stub.create(), sinon.expectation); delete expectation.create; expectation.method = methodName; return expectation; }, invoke: function invoke(func, thisValue, args) { this.verifyCallAllowed(thisValue, args); return _invoke.apply(this, arguments); }, atLeast: function atLeast(num) { if (typeof num != "number") { throw new TypeError("'" + num + "' is not number"); } if (!this.limitsSet) { this.maxCalls = null; this.limitsSet = true; } this.minCalls = num; return this; }, atMost: function atMost(num) { if (typeof num != "number") { throw new TypeError("'" + num + "' is not number"); } if (!this.limitsSet) { this.minCalls = null; this.limitsSet = true; } this.maxCalls = num; return this; }, never: function never() { return this.exactly(0); }, once: function once() { return this.exactly(1); }, twice: function twice() { return this.exactly(2); }, thrice: function thrice() { return this.exactly(3); }, exactly: function exactly(num) { if (typeof num != "number") { throw new TypeError("'" + num + "' is not a number"); } this.atLeast(num); return this.atMost(num); }, met: function met() { return !this.failed && receivedMinCalls(this); }, verifyCallAllowed: function verifyCallAllowed(thisValue, args) { if (receivedMaxCalls(this)) { this.failed = true; sinon.expectation.fail(this.method + " already called " + times(this.maxCalls)); } if ("expectedThis" in this && this.expectedThis !== thisValue) { sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " + this.expectedThis); } if (!("expectedArguments" in this)) { return; } if (!args) { sinon.expectation.fail(this.method + " received no arguments, expected " + sinon.format(this.expectedArguments)); } if (args.length < this.expectedArguments.length) { sinon.expectation.fail(this.method + " received too few arguments (" + sinon.format(args) + "), expected " + sinon.format(this.expectedArguments)); } if (this.expectsExactArgCount && args.length != this.expectedArguments.length) { sinon.expectation.fail(this.method + " received too many arguments (" + sinon.format(args) + "), expected " + sinon.format(this.expectedArguments)); } for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) + ", expected " + sinon.format(this.expectedArguments)); } } }, allowsCall: function allowsCall(thisValue, args) { if (this.met() && receivedMaxCalls(this)) { return false; } if ("expectedThis" in this && this.expectedThis !== thisValue) { return false; } if (!("expectedArguments" in this)) { return true; } args = args || []; if (args.length < this.expectedArguments.length) { return false; } if (this.expectsExactArgCount && args.length != this.expectedArguments.length) { return false; } for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { return false; } } return true; }, withArgs: function withArgs() { this.expectedArguments = slice.call(arguments); return this; }, withExactArgs: function withExactArgs() { this.withArgs.apply(this, arguments); this.expectsExactArgCount = true; return this; }, on: function on(thisValue) { this.expectedThis = thisValue; return this; }, toString: function () { var args = (this.expectedArguments || []).slice(); if (!this.expectsExactArgCount) { push.call(args, "[...]"); } var callStr = sinon.spyCall.toString.call({ proxy: this.method || "anonymous mock expectation", args: args }); var message = callStr.replace(", [...", "[, ...") + " " + expectedCallCountInWords(this); if (this.met()) { return "Expectation met: " + message; } return "Expected " + message + " (" + callCountInWords(this.callCount) + ")"; }, verify: function verify() { if (!this.met()) { sinon.expectation.fail(this.toString()); } else { sinon.expectation.pass(this.toString()); } return true; }, pass: function(message) { sinon.assert.pass(message); }, fail: function (message) { var exception = new Error(message); exception.name = "ExpectationError"; throw exception; } }; }()); if (commonJSModule) { module.exports = mock; } else { sinon.mock = mock; } }(typeof sinon == "object" && sinon || null)); /** * @depend ../sinon.js * @depend stub.js * @depend mock.js */ /*jslint eqeqeq: false, onevar: false, forin: true*/ /*global module, require, sinon*/ /** * Collections of stubs, spies and mocks. * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function (sinon) { var commonJSModule = typeof module == "object" && typeof require == "function"; var push = [].push; var hasOwnProperty = Object.prototype.hasOwnProperty; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon) { return; } function getFakes(fakeCollection) { if (!fakeCollection.fakes) { fakeCollection.fakes = []; } return fakeCollection.fakes; } function each(fakeCollection, method) { var fakes = getFakes(fakeCollection); for (var i = 0, l = fakes.length; i < l; i += 1) { if (typeof fakes[i][method] == "function") { fakes[i][method](); } } } function compact(fakeCollection) { var fakes = getFakes(fakeCollection); var i = 0; while (i < fakes.length) { fakes.splice(i, 1); } } var collection = { verify: function resolve() { each(this, "verify"); }, restore: function restore() { each(this, "restore"); compact(this); }, verifyAndRestore: function verifyAndRestore() { var exception; try { this.verify(); } catch (e) { exception = e; } this.restore(); if (exception) { throw exception; } }, add: function add(fake) { push.call(getFakes(this), fake); return fake; }, spy: function spy() { return this.add(sinon.spy.apply(sinon, arguments)); }, stub: function stub(object, property, value) { if (property) { var original = object[property]; if (typeof original != "function") { if (!hasOwnProperty.call(object, property)) { throw new TypeError("Cannot stub non-existent own property " + property); } object[property] = value; return this.add({ restore: function () { object[property] = original; } }); } } if (!property && !!object && typeof object == "object") { var stubbedObj = sinon.stub.apply(sinon, arguments); for (var prop in stubbedObj) { if (typeof stubbedObj[prop] === "function") { this.add(stubbedObj[prop]); } } return stubbedObj; } return this.add(sinon.stub.apply(sinon, arguments)); }, mock: function mock() { return this.add(sinon.mock.apply(sinon, arguments)); }, inject: function inject(obj) { var col = this; obj.spy = function () { return col.spy.apply(col, arguments); }; obj.stub = function () { return col.stub.apply(col, arguments); }; obj.mock = function () { return col.mock.apply(col, arguments); }; return obj; } }; if (commonJSModule) { module.exports = collection; } else { sinon.collection = collection; } }(typeof sinon == "object" && sinon || null)); /*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ /*global module, require, window*/ /** * Fake timer API * setTimeout * setInterval * clearTimeout * clearInterval * tick * reset * Date * * Inspired by jsUnitMockTimeOut from JsUnit * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ if (typeof sinon == "undefined") { var sinon = {}; } (function (global) { var id = 1; function addTimer(args, recurring) { if (args.length === 0) { throw new Error("Function requires at least 1 parameter"); } var toId = id++; var delay = args[1] || 0; if (!this.timeouts) { this.timeouts = {}; } this.timeouts[toId] = { id: toId, func: args[0], callAt: this.now + delay, invokeArgs: Array.prototype.slice.call(args, 2) }; if (recurring === true) { this.timeouts[toId].interval = delay; } return toId; } function parseTime(str) { if (!str) { return 0; } var strings = str.split(":"); var l = strings.length, i = l; var ms = 0, parsed; if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { throw new Error("tick only understands numbers and 'h:m:s'"); } while (i--) { parsed = parseInt(strings[i], 10); if (parsed >= 60) { throw new Error("Invalid time " + str); } ms += parsed * Math.pow(60, (l - i - 1)); } return ms * 1000; } function createObject(object) { var newObject; if (Object.create) { newObject = Object.create(object); } else { var F = function () {}; F.prototype = object; newObject = new F(); } newObject.Date.clock = newObject; return newObject; } sinon.clock = { now: 0, create: function create(now) { var clock = createObject(this); if (typeof now == "number") { clock.now = now; } if (!!now && typeof now == "object") { throw new TypeError("now should be milliseconds since UNIX epoch"); } return clock; }, setTimeout: function setTimeout(callback, timeout) { return addTimer.call(this, arguments, false); }, clearTimeout: function clearTimeout(timerId) { if (!this.timeouts) { this.timeouts = []; } if (timerId in this.timeouts) { delete this.timeouts[timerId]; } }, setInterval: function setInterval(callback, timeout) { return addTimer.call(this, arguments, true); }, clearInterval: function clearInterval(timerId) { this.clearTimeout(timerId); }, tick: function tick(ms) { ms = typeof ms == "number" ? ms : parseTime(ms); var tickFrom = this.now, tickTo = this.now + ms, previous = this.now; var timer = this.firstTimerInRange(tickFrom, tickTo); var firstException; while (timer && tickFrom <= tickTo) { if (this.timeouts[timer.id]) { tickFrom = this.now = timer.callAt; try { this.callTimer(timer); } catch (e) { firstException = firstException || e; } } timer = this.firstTimerInRange(previous, tickTo); previous = tickFrom; } this.now = tickTo; if (firstException) { throw firstException; } return this.now; }, firstTimerInRange: function (from, to) { var timer, smallest, originalTimer; for (var id in this.timeouts) { if (this.timeouts.hasOwnProperty(id)) { if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) { continue; } if (!smallest || this.timeouts[id].callAt < smallest) { originalTimer = this.timeouts[id]; smallest = this.timeouts[id].callAt; timer = { func: this.timeouts[id].func, callAt: this.timeouts[id].callAt, interval: this.timeouts[id].interval, id: this.timeouts[id].id, invokeArgs: this.timeouts[id].invokeArgs }; } } } return timer || null; }, callTimer: function (timer) { if (typeof timer.interval == "number") { this.timeouts[timer.id].callAt += timer.interval; } else { delete this.timeouts[timer.id]; } try { if (typeof timer.func == "function") { timer.func.apply(null, timer.invokeArgs); } else { eval(timer.func); } } catch (e) { var exception = e; } if (!this.timeouts[timer.id]) { if (exception) { throw exception; } return; } if (exception) { throw exception; } }, reset: function reset() { this.timeouts = {}; }, Date: (function () { var NativeDate = Date; function ClockDate(year, month, date, hour, minute, second, ms) { // Defensive and verbose to avoid potential harm in passing // explicit undefined when user does not pass argument switch (arguments.length) { case 0: return new NativeDate(ClockDate.clock.now); case 1: return new NativeDate(year); case 2: return new NativeDate(year, month); case 3: return new NativeDate(year, month, date); case 4: return new NativeDate(year, month, date, hour); case 5: return new NativeDate(year, month, date, hour, minute); case 6: return new NativeDate(year, month, date, hour, minute, second); default: return new NativeDate(year, month, date, hour, minute, second, ms); } } return mirrorDateProperties(ClockDate, NativeDate); }()) }; function mirrorDateProperties(target, source) { if (source.now) { target.now = function now() { return target.clock.now; }; } else { delete target.now; } if (source.toSource) { target.toSource = function toSource() { return source.toSource(); }; } else { delete target.toSource; } target.toString = function toString() { return source.toString(); }; target.prototype = source.prototype; target.parse = source.parse; target.UTC = source.UTC; target.prototype.toUTCString = source.prototype.toUTCString; return target; } var methods = ["Date", "setTimeout", "setInterval", "clearTimeout", "clearInterval"]; function restore() { var method; for (var i = 0, l = this.methods.length; i < l; i++) { method = this.methods[i]; if (global[method].hadOwnProperty) { global[method] = this["_" + method]; } else { delete global[method]; } } // Prevent multiple executions which will completely remove these props this.methods = []; } function stubGlobal(method, clock) { clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method); clock["_" + method] = global[method]; if (method == "Date") { var date = mirrorDateProperties(clock[method], global[method]); global[method] = date; } else { global[method] = function () { return clock[method].apply(clock, arguments); }; for (var prop in clock[method]) { if (clock[method].hasOwnProperty(prop)) { global[method][prop] = clock[method][prop]; } } } global[method].clock = clock; } sinon.useFakeTimers = function useFakeTimers(now) { var clock = sinon.clock.create(now); clock.restore = restore; clock.methods = Array.prototype.slice.call(arguments, typeof now == "number" ? 1 : 0); if (clock.methods.length === 0) { clock.methods = methods; } for (var i = 0, l = clock.methods.length; i < l; i++) { stubGlobal(clock.methods[i], clock); } return clock; }; }(typeof global != "undefined" && typeof global !== "function" ? global : this)); sinon.timers = { setTimeout: setTimeout, clearTimeout: clearTimeout, setInterval: setInterval, clearInterval: clearInterval, Date: Date }; if (typeof module == "object" && typeof require == "function") { module.exports = sinon; } /*jslint eqeqeq: false, onevar: false*/ /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ /** * Minimal Event interface implementation * * Original implementation by Sven Fuchs: https://gist.github.com/995028 * Modifications and tests by Christian Johansen. * * @author Sven Fuchs (svenfuchs@artweb-design.de) * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2011 Sven Fuchs, Christian Johansen */ if (typeof sinon == "undefined") { this.sinon = {}; } (function () { var push = [].push; sinon.Event = function Event(type, bubbles, cancelable) { this.initEvent(type, bubbles, cancelable); }; sinon.Event.prototype = { initEvent: function(type, bubbles, cancelable) { this.type = type; this.bubbles = bubbles; this.cancelable = cancelable; }, stopPropagation: function () {}, preventDefault: function () { this.defaultPrevented = true; } }; sinon.EventTarget = { addEventListener: function addEventListener(event, listener, useCapture) { this.eventListeners = this.eventListeners || {}; this.eventListeners[event] = this.eventListeners[event] || []; push.call(this.eventListeners[event], listener); }, removeEventListener: function removeEventListener(event, listener, useCapture) { var listeners = this.eventListeners && this.eventListeners[event] || []; for (var i = 0, l = listeners.length; i < l; ++i) { if (listeners[i] == listener) { return listeners.splice(i, 1); } } }, dispatchEvent: function dispatchEvent(event) { var type = event.type; var listeners = this.eventListeners && this.eventListeners[type] || []; for (var i = 0; i < listeners.length; i++) { if (typeof listeners[i] == "function") { listeners[i].call(this, event); } else { listeners[i].handleEvent(event); } } return !!event.defaultPrevented; } }; }()); /** * @depend ../../sinon.js * @depend event.js */ /*jslint eqeqeq: false, onevar: false*/ /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ /** * Fake XMLHttpRequest object * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ if (typeof sinon == "undefined") { this.sinon = {}; } sinon.xhr = { XMLHttpRequest: this.XMLHttpRequest }; // wrapper for global (function(global) { var xhr = sinon.xhr; xhr.GlobalXMLHttpRequest = global.XMLHttpRequest; xhr.GlobalActiveXObject = global.ActiveXObject; xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined"; xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined"; xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false; /*jsl:ignore*/ var unsafeHeaders = { "Accept-Charset": true, "Accept-Encoding": true, "Connection": true, "Content-Length": true, "Cookie": true, "Cookie2": true, "Content-Transfer-Encoding": true, "Date": true, "Expect": true, "Host": true, "Keep-Alive": true, "Referer": true, "TE": true, "Trailer": true, "Transfer-Encoding": true, "Upgrade": true, "User-Agent": true, "Via": true }; /*jsl:end*/ function FakeXMLHttpRequest() { this.readyState = FakeXMLHttpRequest.UNSENT; this.requestHeaders = {}; this.requestBody = null; this.status = 0; this.statusText = ""; if (typeof FakeXMLHttpRequest.onCreate == "function") { FakeXMLHttpRequest.onCreate(this); } } function verifyState(xhr) { if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { throw new Error("INVALID_STATE_ERR"); } if (xhr.sendFlag) { throw new Error("INVALID_STATE_ERR"); } } // filtering to enable a white-list version of Sinon FakeXhr, // where whitelisted requests are passed through to real XHR function each(collection, callback) { if (!collection) return; for (var i = 0, l = collection.length; i < l; i += 1) { callback(collection[i]); } } function some(collection, callback) { for (var index = 0; index < collection.length; index++) { if(callback(collection[index]) === true) return true; }; return false; } // largest arity in XHR is 5 - XHR#open var apply = function(obj,method,args) { switch(args.length) { case 0: return obj[method](); case 1: return obj[method](args[0]); case 2: return obj[method](args[0],args[1]); case 3: return obj[method](args[0],args[1],args[2]); case 4: return obj[method](args[0],args[1],args[2],args[3]); case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]); }; }; FakeXMLHttpRequest.filters = []; FakeXMLHttpRequest.addFilter = function(fn) { this.filters.push(fn) }; var IE6Re = /MSIE 6/; FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) { var xhr = new sinon.xhr.workingXHR(); each(["open","setRequestHeader","send","abort","getResponseHeader", "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"], function(method) { fakeXhr[method] = function() { return apply(xhr,method,arguments); }; }); var copyAttrs = function(args) { each(args, function(attr) { try { fakeXhr[attr] = xhr[attr] } catch(e) { if(!IE6Re.test(navigator.userAgent)) throw e; } }); }; var stateChange = function() { fakeXhr.readyState = xhr.readyState; if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) { copyAttrs(["status","statusText"]); } if(xhr.readyState >= FakeXMLHttpRequest.LOADING) { copyAttrs(["responseText"]); } if(xhr.readyState === FakeXMLHttpRequest.DONE) { copyAttrs(["responseXML"]); } if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr); }; if(xhr.addEventListener) { for(var event in fakeXhr.eventListeners) { if(fakeXhr.eventListeners.hasOwnProperty(event)) { each(fakeXhr.eventListeners[event],function(handler) { xhr.addEventListener(event, handler); }); } } xhr.addEventListener("readystatechange",stateChange); } else { xhr.onreadystatechange = stateChange; } apply(xhr,"open",xhrArgs); }; FakeXMLHttpRequest.useFilters = false; function verifyRequestSent(xhr) { if (xhr.readyState == FakeXMLHttpRequest.DONE) { throw new Error("Request done"); } } function verifyHeadersReceived(xhr) { if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { throw new Error("No headers received"); } } function verifyResponseBodyType(body) { if (typeof body != "string") { var error = new Error("Attempted to respond to fake XMLHttpRequest with " + body + ", which is not a string."); error.name = "InvalidBodyException"; throw error; } } sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, { async: true, open: function open(method, url, async, username, password) { this.method = method; this.url = url; this.async = typeof async == "boolean" ? async : true; this.username = username; this.password = password; this.responseText = null; this.responseXML = null; this.requestHeaders = {}; this.sendFlag = false; if(sinon.FakeXMLHttpRequest.useFilters === true) { var xhrArgs = arguments; var defake = some(FakeXMLHttpRequest.filters,function(filter) { return filter.apply(this,xhrArgs) }); if (defake) { return sinon.FakeXMLHttpRequest.defake(this,arguments); } } this.readyStateChange(FakeXMLHttpRequest.OPENED); }, readyStateChange: function readyStateChange(state) { this.readyState = state; if (typeof this.onreadystatechange == "function") { try { this.onreadystatechange(); } catch (e) { sinon.logError("Fake XHR onreadystatechange handler", e); } } this.dispatchEvent(new sinon.Event("readystatechange")); }, setRequestHeader: function setRequestHeader(header, value) { verifyState(this); if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { throw new Error("Refused to set unsafe header \"" + header + "\""); } if (this.requestHeaders[header]) { this.requestHeaders[header] += "," + value; } else { this.requestHeaders[header] = value; } }, // Helps testing setResponseHeaders: function setResponseHeaders(headers) { this.responseHeaders = {}; for (var header in headers) { if (headers.hasOwnProperty(header)) { this.responseHeaders[header] = headers[header]; } } if (this.async) { this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); } else { this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; } }, // Currently treats ALL data as a DOMString (i.e. no Document) send: function send(data) { verifyState(this); if (!/^(get|head)$/i.test(this.method)) { if (this.requestHeaders["Content-Type"]) { var value = this.requestHeaders["Content-Type"].split(";"); this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; } else { this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; } this.requestBody = data; } this.errorFlag = false; this.sendFlag = this.async; this.readyStateChange(FakeXMLHttpRequest.OPENED); if (typeof this.onSend == "function") { this.onSend(this); } }, abort: function abort() { this.aborted = true; this.responseText = null; this.errorFlag = true; this.requestHeaders = {}; if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) { this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); this.sendFlag = false; } this.readyState = sinon.FakeXMLHttpRequest.UNSENT; }, getResponseHeader: function getResponseHeader(header) { if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { return null; } if (/^Set-Cookie2?$/i.test(header)) { return null; } header = header.toLowerCase(); for (var h in this.responseHeaders) { if (h.toLowerCase() == header) { return this.responseHeaders[h]; } } return null; }, getAllResponseHeaders: function getAllResponseHeaders() { if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { return ""; } var headers = ""; for (var header in this.responseHeaders) { if (this.responseHeaders.hasOwnProperty(header) && !/^Set-Cookie2?$/i.test(header)) { headers += header + ": " + this.responseHeaders[header] + "\r\n"; } } return headers; }, setResponseBody: function setResponseBody(body) { verifyRequestSent(this); verifyHeadersReceived(this); verifyResponseBodyType(body); var chunkSize = this.chunkSize || 10; var index = 0; this.responseText = ""; do { if (this.async) { this.readyStateChange(FakeXMLHttpRequest.LOADING); } this.responseText += body.substring(index, index + chunkSize); index += chunkSize; } while (index < body.length); var type = this.getResponseHeader("Content-Type"); if (this.responseText && (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { try { this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); } catch (e) { // Unable to parse XML - no biggie } } if (this.async) { this.readyStateChange(FakeXMLHttpRequest.DONE); } else { this.readyState = FakeXMLHttpRequest.DONE; } }, respond: function respond(status, headers, body) { this.setResponseHeaders(headers || {}); this.status = typeof status == "number" ? status : 200; this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; this.setResponseBody(body || ""); } }); sinon.extend(FakeXMLHttpRequest, { UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4 }); // Borrowed from JSpec FakeXMLHttpRequest.parseXML = function parseXML(text) { var xmlDoc; if (typeof DOMParser != "undefined") { var parser = new DOMParser(); xmlDoc = parser.parseFromString(text, "text/xml"); } else { xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); xmlDoc.async = "false"; xmlDoc.loadXML(text); } return xmlDoc; }; FakeXMLHttpRequest.statusCodes = { 100: "Continue", 101: "Switching Protocols", 200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 300: "Multiple Choice", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Request Entity Too Large", 414: "Request-URI Too Long", 415: "Unsupported Media Type", 416: "Requested Range Not Satisfiable", 417: "Expectation Failed", 422: "Unprocessable Entity", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported" }; sinon.useFakeXMLHttpRequest = function () { sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { if (xhr.supportsXHR) { global.XMLHttpRequest = xhr.GlobalXMLHttpRequest; } if (xhr.supportsActiveX) { global.ActiveXObject = xhr.GlobalActiveXObject; } delete sinon.FakeXMLHttpRequest.restore; if (keepOnCreate !== true) { delete sinon.FakeXMLHttpRequest.onCreate; } }; if (xhr.supportsXHR) { global.XMLHttpRequest = sinon.FakeXMLHttpRequest; } if (xhr.supportsActiveX) { global.ActiveXObject = function ActiveXObject(objId) { if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) { return new sinon.FakeXMLHttpRequest(); } return new xhr.GlobalActiveXObject(objId); }; } return sinon.FakeXMLHttpRequest; }; sinon.FakeXMLHttpRequest = FakeXMLHttpRequest; })(this); if (typeof module == "object" && typeof require == "function") { module.exports = sinon; } /** * @depend fake_xml_http_request.js */ /*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ /*global module, require, window*/ /** * The Sinon "server" mimics a web server that receives requests from * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, * both synchronously and asynchronously. To respond synchronuously, canned * answers have to be provided upfront. * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ if (typeof sinon == "undefined") { var sinon = {}; } sinon.fakeServer = (function () { var push = [].push; function F() {} function create(proto) { F.prototype = proto; return new F(); } function responseArray(handler) { var response = handler; if (Object.prototype.toString.call(handler) != "[object Array]") { response = [200, {}, handler]; } if (typeof response[2] != "string") { throw new TypeError("Fake server response body should be string, but was " + typeof response[2]); } return response; } var wloc = typeof window !== "undefined" ? window.location : {}; var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host); function matchOne(response, reqMethod, reqUrl) { var rmeth = response.method; var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); var url = response.url; var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl)); return matchMethod && matchUrl; } function match(response, request) { var requestMethod = this.getHTTPMethod(request); var requestUrl = request.url; if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { requestUrl = requestUrl.replace(rCurrLoc, ""); } if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { if (typeof response.response == "function") { var ru = response.url; var args = [request].concat(!ru ? [] : requestUrl.match(ru).slice(1)); return response.response.apply(response, args); } return true; } return false; } function log(response, request) { var str; str = "Request:\n" + sinon.format(request) + "\n\n"; str += "Response:\n" + sinon.format(response) + "\n\n"; sinon.log(str); } return { create: function () { var server = create(this); this.xhr = sinon.useFakeXMLHttpRequest(); server.requests = []; this.xhr.onCreate = function (xhrObj) { server.addRequest(xhrObj); }; return server; }, addRequest: function addRequest(xhrObj) { var server = this; push.call(this.requests, xhrObj); xhrObj.onSend = function () { server.handleRequest(this); }; if (this.autoRespond && !this.responding) { setTimeout(function () { server.responding = false; server.respond(); }, this.autoRespondAfter || 10); this.responding = true; } }, getHTTPMethod: function getHTTPMethod(request) { if (this.fakeHTTPMethods && /post/i.test(request.method)) { var matches = (request.requestBody || "").match(/_method=([^\b;]+)/); return !!matches ? matches[1] : request.method; } return request.method; }, handleRequest: function handleRequest(xhr) { if (xhr.async) { if (!this.queue) { this.queue = []; } push.call(this.queue, xhr); } else { this.processRequest(xhr); } }, respondWith: function respondWith(method, url, body) { if (arguments.length == 1 && typeof method != "function") { this.response = responseArray(method); return; } if (!this.responses) { this.responses = []; } if (arguments.length == 1) { body = method; url = method = null; } if (arguments.length == 2) { body = url; url = method; method = null; } push.call(this.responses, { method: method, url: url, response: typeof body == "function" ? body : responseArray(body) }); }, respond: function respond() { if (arguments.length > 0) this.respondWith.apply(this, arguments); var queue = this.queue || []; var request; while(request = queue.shift()) { this.processRequest(request); } }, processRequest: function processRequest(request) { try { if (request.aborted) { return; } var response = this.response || [404, {}, ""]; if (this.responses) { for (var i = 0, l = this.responses.length; i < l; i++) { if (match.call(this, this.responses[i], request)) { response = this.responses[i].response; break; } } } if (request.readyState != 4) { log(response, request); request.respond(response[0], response[1], response[2]); } } catch (e) { sinon.logError("Fake server request processing", e); } }, restore: function restore() { return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); } }; }()); if (typeof module == "object" && typeof require == "function") { module.exports = sinon; } /** * @depend fake_server.js * @depend fake_timers.js */ /*jslint browser: true, eqeqeq: false, onevar: false*/ /*global sinon*/ /** * Add-on for sinon.fakeServer that automatically handles a fake timer along with * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, * it polls the object for completion with setInterval. Dispite the direct * motivation, there is nothing jQuery-specific in this file, so it can be used * in any environment where the ajax implementation depends on setInterval or * setTimeout. * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function () { function Server() {} Server.prototype = sinon.fakeServer; sinon.fakeServerWithClock = new Server(); sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { if (xhr.async) { if (typeof setTimeout.clock == "object") { this.clock = setTimeout.clock; } else { this.clock = sinon.useFakeTimers(); this.resetClock = true; } if (!this.longestTimeout) { var clockSetTimeout = this.clock.setTimeout; var clockSetInterval = this.clock.setInterval; var server = this; this.clock.setTimeout = function (fn, timeout) { server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); return clockSetTimeout.apply(this, arguments); }; this.clock.setInterval = function (fn, timeout) { server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); return clockSetInterval.apply(this, arguments); }; } } return sinon.fakeServer.addRequest.call(this, xhr); }; sinon.fakeServerWithClock.respond = function respond() { var returnVal = sinon.fakeServer.respond.apply(this, arguments); if (this.clock) { this.clock.tick(this.longestTimeout || 0); this.longestTimeout = 0; if (this.resetClock) { this.clock.restore(); this.resetClock = false; } } return returnVal; }; sinon.fakeServerWithClock.restore = function restore() { if (this.clock) { this.clock.restore(); } return sinon.fakeServer.restore.apply(this, arguments); }; }()); /** * @depend ../sinon.js * @depend collection.js * @depend util/fake_timers.js * @depend util/fake_server_with_clock.js */ /*jslint eqeqeq: false, onevar: false, plusplus: false*/ /*global require, module*/ /** * Manages fake collections as well as fake utilities such as Sinon's * timers and fake XHR implementation in one convenient object. * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ if (typeof module == "object" && typeof require == "function") { var sinon = require("../sinon"); sinon.extend(sinon, require("./util/fake_timers")); } (function () { var push = [].push; function exposeValue(sandbox, config, key, value) { if (!value) { return; } if (config.injectInto) { config.injectInto[key] = value; } else { push.call(sandbox.args, value); } } function prepareSandboxFromConfig(config) { var sandbox = sinon.create(sinon.sandbox); if (config.useFakeServer) { if (typeof config.useFakeServer == "object") { sandbox.serverPrototype = config.useFakeServer; } sandbox.useFakeServer(); } if (config.useFakeTimers) { if (typeof config.useFakeTimers == "object") { sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers); } else { sandbox.useFakeTimers(); } } return sandbox; } sinon.sandbox = sinon.extend(sinon.create(sinon.collection), { useFakeTimers: function useFakeTimers() { this.clock = sinon.useFakeTimers.apply(sinon, arguments); return this.add(this.clock); }, serverPrototype: sinon.fakeServer, useFakeServer: function useFakeServer() { var proto = this.serverPrototype || sinon.fakeServer; if (!proto || !proto.create) { return null; } this.server = proto.create(); return this.add(this.server); }, inject: function (obj) { sinon.collection.inject.call(this, obj); if (this.clock) { obj.clock = this.clock; } if (this.server) { obj.server = this.server; obj.requests = this.server.requests; } return obj; }, create: function (config) { if (!config) { return sinon.create(sinon.sandbox); } var sandbox = prepareSandboxFromConfig(config); sandbox.args = sandbox.args || []; var prop, value, exposed = sandbox.inject({}); if (config.properties) { for (var i = 0, l = config.properties.length; i < l; i++) { prop = config.properties[i]; value = exposed[prop] || prop == "sandbox" && sandbox; exposeValue(sandbox, config, prop, value); } } else { exposeValue(sandbox, config, "sandbox", value); } return sandbox; } }); sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer; if (typeof module == "object" && typeof require == "function") { module.exports = sinon.sandbox; } }()); /** * @depend ../sinon.js * @depend stub.js * @depend mock.js * @depend sandbox.js */ /*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/ /*global module, require, sinon*/ /** * Test function, sandboxes fakes * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function (sinon) { var commonJSModule = typeof module == "object" && typeof require == "function"; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon) { return; } function test(callback) { var type = typeof callback; if (type != "function") { throw new TypeError("sinon.test needs to wrap a test function, got " + type); } return function () { var config = sinon.getConfig(sinon.config); config.injectInto = config.injectIntoThis && this || config.injectInto; var sandbox = sinon.sandbox.create(config); var exception, result; var args = Array.prototype.slice.call(arguments).concat(sandbox.args); try { result = callback.apply(this, args); } catch (e) { exception = e; } if (typeof exception !== "undefined") { sandbox.restore(); throw exception; } else { sandbox.verifyAndRestore(); } return result; }; } test.config = { injectIntoThis: true, injectInto: null, properties: ["spy", "stub", "mock", "clock", "server", "requests"], useFakeTimers: true, useFakeServer: true }; if (commonJSModule) { module.exports = test; } else { sinon.test = test; } }(typeof sinon == "object" && sinon || null)); /** * @depend ../sinon.js * @depend test.js */ /*jslint eqeqeq: false, onevar: false, eqeqeq: false*/ /*global module, require, sinon*/ /** * Test case, sandboxes all test functions * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function (sinon) { var commonJSModule = typeof module == "object" && typeof require == "function"; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon || !Object.prototype.hasOwnProperty) { return; } function createTest(property, setUp, tearDown) { return function () { if (setUp) { setUp.apply(this, arguments); } var exception, result; try { result = property.apply(this, arguments); } catch (e) { exception = e; } if (tearDown) { tearDown.apply(this, arguments); } if (exception) { throw exception; } return result; }; } function testCase(tests, prefix) { /*jsl:ignore*/ if (!tests || typeof tests != "object") { throw new TypeError("sinon.testCase needs an object with test functions"); } /*jsl:end*/ prefix = prefix || "test"; var rPrefix = new RegExp("^" + prefix); var methods = {}, testName, property, method; var setUp = tests.setUp; var tearDown = tests.tearDown; for (testName in tests) { if (tests.hasOwnProperty(testName)) { property = tests[testName]; if (/^(setUp|tearDown)$/.test(testName)) { continue; } if (typeof property == "function" && rPrefix.test(testName)) { method = property; if (setUp || tearDown) { method = createTest(property, setUp, tearDown); } methods[testName] = sinon.test(method); } else { methods[testName] = tests[testName]; } } } return methods; } if (commonJSModule) { module.exports = testCase; } else { sinon.testCase = testCase; } }(typeof sinon == "object" && sinon || null)); /** * @depend ../sinon.js * @depend stub.js */ /*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/ /*global module, require, sinon*/ /** * Assertions matching the test spy retrieval interface. * * @author Christian Johansen (christian@cjohansen.no) * @license BSD * * Copyright (c) 2010-2013 Christian Johansen */ (function (sinon, global) { var commonJSModule = typeof module == "object" && typeof require == "function"; var slice = Array.prototype.slice; var assert; if (!sinon && commonJSModule) { sinon = require("../sinon"); } if (!sinon) { return; } function verifyIsStub() { var method; for (var i = 0, l = arguments.length; i < l; ++i) { method = arguments[i]; if (!method) { assert.fail("fake is not a spy"); } if (typeof method != "function") { assert.fail(method + " is not a function"); } if (typeof method.getCall != "function") { assert.fail(method + " is not stubbed"); } } } function failAssertion(object, msg) { object = object || global; var failMethod = object.fail || assert.fail; failMethod.call(object, msg); } function mirrorPropAsAssertion(name, method, message) { if (arguments.length == 2) { message = method; method = name; } assert[name] = function (fake) { verifyIsStub(fake); var args = slice.call(arguments, 1); var failed = false; if (typeof method == "function") { failed = !method(fake); } else { failed = typeof fake[method] == "function" ? !fake[method].apply(fake, args) : !fake[method]; } if (failed) { failAssertion(this, fake.printf.apply(fake, [message].concat(args))); } else { assert.pass(name); } }; } function exposedName(prefix, prop) { return !prefix || /^fail/.test(prop) ? prop : prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1); }; assert = { failException: "AssertError", fail: function fail(message) { var error = new Error(message); error.name = this.failException || assert.failException; throw error; }, pass: function pass(assertion) {}, callOrder: function assertCallOrder() { verifyIsStub.apply(null, arguments); var expected = "", actual = ""; if (!sinon.calledInOrder(arguments)) { try { expected = [].join.call(arguments, ", "); actual = sinon.orderByFirstCall(slice.call(arguments)).join(", "); } catch (e) { // If this fails, we'll just fall back to the blank string } failAssertion(this, "expected " + expected + " to be " + "called in order but were called as " + actual); } else { assert.pass("callOrder"); } }, callCount: function assertCallCount(method, count) { verifyIsStub(method); if (method.callCount != count) { var msg = "expected %n to be called " + sinon.timesInWords(count) + " but was called %c%C"; failAssertion(this, method.printf(msg)); } else { assert.pass("callCount"); } }, expose: function expose(target, options) { if (!target) { throw new TypeError("target is null or undefined"); } var o = options || {}; var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix; var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail; for (var method in this) { if (method != "export" && (includeFail || !/^(fail)/.test(method))) { target[exposedName(prefix, method)] = this[method]; } } return target; } }; mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called"); mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; }, "expected %n to not have been called but was called %c%C"); mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C"); mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C"); mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C"); mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t"); mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t"); mirrorPropAsAssertion("calledWithNew", "expected %n to be called with new"); mirrorPropAsAssertion("alwaysCalledWithNew", "expected %n to always be called with new"); mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C"); mirrorPropAsAssertion("calledWithMatch", "expected %n to be called with match %*%C"); mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C"); mirrorPropAsAssertion("alwaysCalledWithMatch", "expected %n to always be called with match %*%C"); mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C"); mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C"); mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C"); mirrorPropAsAssertion("neverCalledWithMatch", "expected %n to never be called with match %*%C"); mirrorPropAsAssertion("threw", "%n did not throw exception%C"); mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C"); if (commonJSModule) { module.exports = assert; } else { sinon.assert = assert; } }(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : global)); return sinon;}.call(typeof window != 'undefined' && window || {})); Leaflet.markercluster-0.5.0/spec/suites/000077500000000000000000000000001270301646500202245ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/spec/suites/AddLayer.MultipleSpec.js000066400000000000000000000067021270301646500246610ustar00rootroot00000000000000describe('addLayer adding multiple markers', function () { var map, div, clock; beforeEach(function () { clock = sinon.useFakeTimers(); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { clock.restore(); document.body.removeChild(div); }); it('creates a cluster when 2 overlapping markers are added before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayer(marker); group.addLayer(marker2); map.addLayer(group); expect(marker._icon).to.be(undefined); expect(marker2._icon).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); }); it('creates a cluster when 2 overlapping markers are added after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); group.addLayer(marker2); expect(marker._icon).to.be(null); //Null as was added and then removed expect(marker2._icon).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); }); it('creates a cluster with an animation when 2 overlapping markers are added after the group is added to the map', function () { var group = new L.MarkerClusterGroup({ animateAddingMarkers: true }); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); group.addLayer(marker2); expect(marker._icon.parentNode).to.be(map._panes.markerPane); expect(marker2._icon.parentNode).to.be(map._panes.markerPane); expect(map._panes.markerPane.childNodes.length).to.be(3); //Run the the animation clock.tick(1000); //Then markers should be removed from map expect(marker._icon).to.be(null); expect(marker2._icon).to.be(null); expect(map._panes.markerPane.childNodes.length).to.be(1); }); it('creates a cluster and marker when 2 overlapping markers and one non-overlapping are added before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); var marker3 = new L.Marker([3.0, 1.5]); group.addLayer(marker); group.addLayer(marker2); group.addLayer(marker3); map.addLayer(group); expect(marker._icon).to.be(undefined); expect(marker2._icon).to.be(undefined); expect(marker3._icon.parentNode).to.be(map._panes.markerPane); expect(map._panes.markerPane.childNodes.length).to.be(2); }); it('creates a cluster and marker when 2 overlapping markers and one non-overlapping are added after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); var marker3 = new L.Marker([3.0, 1.5]); map.addLayer(group); group.addLayer(marker); group.addLayer(marker2); group.addLayer(marker3); expect(marker._icon).to.be(null); //Null as was added and then removed expect(marker2._icon).to.be(undefined); expect(marker3._icon.parentNode).to.be(map._panes.markerPane); expect(map._panes.markerPane.childNodes.length).to.be(2); }); });Leaflet.markercluster-0.5.0/spec/suites/AddLayer.SingleSpec.js000066400000000000000000000037631270301646500243130ustar00rootroot00000000000000describe('addLayer adding a single marker', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('appears when added to the group before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); group.addLayer(marker); map.addLayer(group); expect(marker._icon).to.not.be(undefined); expect(marker._icon.parentNode).to.be(map._panes.markerPane); }); it('appears when added to the group after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); expect(marker._icon).to.not.be(undefined); expect(marker._icon.parentNode).to.be(map._panes.markerPane); }); it('appears (using animations) when added after the group is added to the map', function () { var group = new L.MarkerClusterGroup({ animateAddingMarkers: true }); var marker = new L.Marker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); expect(marker._icon).to.not.be(undefined); expect(marker._icon.parentNode).to.be(map._panes.markerPane); }); it('does not appear when too far away when added before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([3.5, 1.5]); group.addLayer(marker); map.addLayer(group); expect(marker._icon).to.be(undefined); }); it('does not appear when too far away when added after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([3.5, 1.5]); map.addLayer(group); group.addLayer(marker); expect(marker._icon).to.be(undefined); }); }); Leaflet.markercluster-0.5.0/spec/suites/AddLayersSpec.js000066400000000000000000000047321270301646500232530ustar00rootroot00000000000000describe('addLayers adding multiple markers', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('creates a cluster when 2 overlapping markers are added before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); expect(marker._icon).to.be(undefined); expect(marker2._icon).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); }); it('creates a cluster when 2 overlapping markers are added after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); map.addLayer(group); group.addLayers([marker, marker2]); expect(marker._icon).to.be(undefined); expect(marker2._icon).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); }); it('creates a cluster and marker when 2 overlapping markers and one non-overlapping are added before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); var marker3 = new L.Marker([3.0, 1.5]); group.addLayers([marker, marker2, marker3]); map.addLayer(group); expect(marker._icon).to.be(undefined); expect(marker2._icon).to.be(undefined); expect(marker3._icon.parentNode).to.be(map._panes.markerPane); expect(map._panes.markerPane.childNodes.length).to.be(2); }); it('creates a cluster and marker when 2 overlapping markers and one non-overlapping are added after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); var marker3 = new L.Marker([3.0, 1.5]); map.addLayer(group); group.addLayers([marker, marker2, marker3]); expect(marker._icon).to.be(undefined); expect(marker2._icon).to.be(undefined); expect(marker3._icon.parentNode).to.be(map._panes.markerPane); expect(map._panes.markerPane.childNodes.length).to.be(2); }); });Leaflet.markercluster-0.5.0/spec/suites/ChildChangingIconSupportSpec.js000066400000000000000000000023251270301646500262670ustar00rootroot00000000000000describe('support child markers changing icon', function () { var map, div, clock; beforeEach(function () { clock = sinon.useFakeTimers(); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { clock.restore(); document.body.removeChild(div); }); it('child markers end up with the right icon after becoming unclustered', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5], { icon: new L.DivIcon({html: 'Inner1Text' }) }); var marker2 = new L.Marker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); expect(marker._icon.parentNode).to.be(map._panes.markerPane); expect(marker._icon.innerHTML).to.contain('Inner1Text'); group.addLayer(marker2); expect(marker._icon).to.be(null); //Have been removed from the map marker.setIcon(new L.DivIcon({ html: 'Inner2Text' })); //Change the icon group.removeLayer(marker2); //Remove the other marker, so we'll become unclustered expect(marker._icon.innerHTML).to.contain('Inner2Text'); }); });Leaflet.markercluster-0.5.0/spec/suites/CircleMarkerSupportSpec.js000066400000000000000000000063111270301646500253360ustar00rootroot00000000000000describe('support for CircleMarker elements', function () { var map, div, clock; beforeEach(function () { clock = sinon.useFakeTimers(); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { clock.restore(); document.body.removeChild(div); }); it('appears when added to the group before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.CircleMarker([1.5, 1.5]); group.addLayer(marker); map.addLayer(group); expect(marker._container).to.not.be(undefined); expect(marker._container.parentNode).to.be(map._pathRoot); clock.tick(1000); }); it('appears when added to the group after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.CircleMarker([1.5, 1.5]); group.addLayer(marker); map.addLayer(group); expect(marker._container).to.not.be(undefined); expect(marker._container.parentNode).to.be(map._pathRoot); clock.tick(1000); }); it('appears animated when added to the group after the group is added to the map', function () { var group = new L.MarkerClusterGroup({ animateAddingMarkers: true }); var marker = new L.CircleMarker([1.5, 1.5]); var marker2 = new L.CircleMarker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); group.addLayer(marker2); expect(marker._container.parentNode).to.be(map._pathRoot); expect(marker2._container.parentNode).to.be(map._pathRoot); clock.tick(1000); expect(marker._container.parentNode).to.be(null); expect(marker2._container.parentNode).to.be(null); }); it('creates a cluster when 2 overlapping markers are added before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.CircleMarker([1.5, 1.5]); var marker2 = new L.CircleMarker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); expect(marker._container).to.be(undefined); expect(marker2._container).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); clock.tick(1000); }); it('creates a cluster when 2 overlapping markers are added after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.CircleMarker([1.5, 1.5]); var marker2 = new L.CircleMarker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); group.addLayer(marker2); expect(marker._container.parentNode).to.be(null); //Removed then re-added, so null expect(marker2._container).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); clock.tick(1000); }); it('disappears when removed from the group', function () { var group = new L.MarkerClusterGroup(); var marker = new L.CircleMarker([1.5, 1.5]); group.addLayer(marker); map.addLayer(group); expect(marker._container).to.not.be(undefined); expect(marker._container.parentNode).to.be(map._pathRoot); group.removeLayer(marker); expect(marker._container.parentNode).to.be(null); clock.tick(1000); }); });Leaflet.markercluster-0.5.0/spec/suites/CircleSupportSpec.js000066400000000000000000000061201270301646500241720ustar00rootroot00000000000000describe('support for Circle elements', function () { var map, div, clock; beforeEach(function () { clock = sinon.useFakeTimers(); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { clock.restore(); document.body.removeChild(div); }); it('appears when added to the group before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 200); group.addLayer(marker); map.addLayer(group); expect(marker._container).to.not.be(undefined); expect(marker._container.parentNode).to.be(map._pathRoot); clock.tick(1000); }); it('appears when added to the group after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 200); group.addLayer(marker); map.addLayer(group); expect(marker._container).to.not.be(undefined); expect(marker._container.parentNode).to.be(map._pathRoot); clock.tick(1000); }); it('appears animated when added to the group after the group is added to the map', function () { var group = new L.MarkerClusterGroup({ animateAddingMarkers: true }); var marker = new L.Circle([1.5, 1.5], 200); var marker2 = new L.Circle([1.5, 1.5], 200); map.addLayer(group); group.addLayer(marker); group.addLayer(marker2); expect(marker._container.parentNode).to.be(map._pathRoot); expect(marker2._container.parentNode).to.be(map._pathRoot); clock.tick(1000); }); it('creates a cluster when 2 overlapping markers are added before the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 200); var marker2 = new L.Circle([1.5, 1.5], 200); group.addLayers([marker, marker2]); map.addLayer(group); expect(marker._container).to.be(undefined); expect(marker2._container).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); clock.tick(1000); }); it('creates a cluster when 2 overlapping markers are added after the group is added to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 200); var marker2 = new L.Circle([1.5, 1.5], 200); map.addLayer(group); group.addLayer(marker); group.addLayer(marker2); expect(marker._container.parentNode).to.be(null); //Removed then re-added, so null expect(marker2._container).to.be(undefined); expect(map._panes.markerPane.childNodes.length).to.be(1); clock.tick(1000); }); it('disappears when removed from the group', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 200); group.addLayer(marker); map.addLayer(group); expect(marker._container).to.not.be(undefined); expect(marker._container.parentNode).to.be(map._pathRoot); group.removeLayer(marker); expect(marker._container.parentNode).to.be(null); clock.tick(1000); }); });Leaflet.markercluster-0.5.0/spec/suites/DistanceGridSpec.js000066400000000000000000000007701270301646500237410ustar00rootroot00000000000000describe('distance grid', function () { it('addObject', function () { var grid = new L.DistanceGrid(100), obj = {}; expect(grid.addObject(obj, { x: 0, y: 0 })).to.eql(undefined); expect(grid.removeObject(obj, { x: 0, y: 0 })).to.eql(true); }); it('eachObject', function (done) { var grid = new L.DistanceGrid(100), obj = {}; expect(grid.addObject(obj, { x: 0, y: 0 })).to.eql(undefined); grid.eachObject(function(o) { expect(o).to.eql(obj); done(); }); }); }); Leaflet.markercluster-0.5.0/spec/suites/LeafletSpec.js000066400000000000000000000002371270301646500227530ustar00rootroot00000000000000describe('L#noConflict', function() { it('restores the previous L value and returns Leaflet namespace', function(){ expect(L.version).to.be.ok(); }); }); Leaflet.markercluster-0.5.0/spec/suites/NonPointSpec.js000066400000000000000000000132711270301646500231450ustar00rootroot00000000000000describe('adding non point data works', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('Allows adding a polygon before via addLayer', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0,2.0], [1.5, 2.0]]); group.addLayer(polygon); map.addLayer(group); expect(polygon._container).to.not.be(undefined); expect(polygon._container.parentNode).to.be(map._pathRoot); expect(group.hasLayer(polygon)); }); it('Allows adding a polygon before via addLayers([])', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayers([polygon]); map.addLayer(group); expect(polygon._container).to.not.be(undefined); expect(polygon._container.parentNode).to.be(map._pathRoot); }); it('Removes polygons from map when removed', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); map.addLayer(group); map.removeLayer(group); expect(polygon._container.parentNode).to.be(null); }); describe('hasLayer', function () { it('returns false when not added', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); expect(group.hasLayer(polygon)).to.be(false); map.addLayer(group); expect(group.hasLayer(polygon)).to.be(false); map.addLayer(polygon); expect(group.hasLayer(polygon)).to.be(false); }); it('returns true before adding to map', function() { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayers([polygon]); expect(group.hasLayer(polygon)).to.be(true); }); it('returns true after adding to map after adding polygon', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); map.addLayer(group); expect(group.hasLayer(polygon)).to.be(true); }); it('returns true after adding to map before adding polygon', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); map.addLayer(group); group.addLayer(polygon); expect(group.hasLayer(polygon)).to.be(true); }); }); describe('removeLayer', function() { it('removes before adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); expect(group.hasLayer(polygon)).to.be(true); group.removeLayer(polygon); expect(group.hasLayer(polygon)).to.be(false); }); it('removes before adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayers([polygon]); expect(group.hasLayer(polygon)).to.be(true); group.removeLayer(polygon); expect(group.hasLayer(polygon)).to.be(false); }); it('removes after adding to map after adding polygon', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); map.addLayer(group); expect(group.hasLayer(polygon)).to.be(true); group.removeLayer(polygon); expect(group.hasLayer(polygon)).to.be(false); }); it('removes after adding to map before adding polygon', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); map.addLayer(group); group.addLayer(polygon); expect(group.hasLayer(polygon)).to.be(true); group.removeLayer(polygon); expect(group.hasLayer(polygon)).to.be(false); }); }); describe('removeLayers', function () { it('removes before adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); expect(group.hasLayer(polygon)).to.be(true); group.removeLayers([polygon]); expect(group.hasLayer(polygon)).to.be(false); }); it('removes before adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayers([polygon]); expect(group.hasLayer(polygon)).to.be(true); group.removeLayers([polygon]); expect(group.hasLayer(polygon)).to.be(false); }); it('removes after adding to map after adding polygon', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); map.addLayer(group); expect(group.hasLayer(polygon)).to.be(true); group.removeLayers([polygon]); expect(group.hasLayer(polygon)).to.be(false); }); it('removes after adding to map before adding polygon', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); map.addLayer(group); group.addLayer(polygon); expect(group.hasLayer(polygon)).to.be(true); group.removeLayers([polygon]); expect(group.hasLayer(polygon)).to.be(false); }); }); });Leaflet.markercluster-0.5.0/spec/suites/QuickHullSpec.js000066400000000000000000000026161270301646500233030ustar00rootroot00000000000000describe('quickhull', function () { describe('getDistant', function () { it('zero distance', function () { var bl = [ { lat: 0, lng: 0 }, { lat: 0, lng: 10 } ]; expect(L.QuickHull.getDistant({ lat: 0, lng: 0 }, bl)).to.eql(0); }); it('non-zero distance', function () { var bl = [ { lat: 0, lng: 0 }, { lat: 0, lng: 10 } ]; expect(L.QuickHull.getDistant({ lat: 5, lng: 5 }, bl)).to.eql(-50); }); }); describe('getConvexHull', function () { it('creates a hull', function () { expect(L.QuickHull.getConvexHull([ { lat: 0, lng: 0 }, { lat: 10, lng: 0 }, { lat: 10, lng: 10 }, { lat: 0, lng: 10 }, { lat: 5, lng: 5 } ])).to.eql([ { lat: 0, lng: 10 }, { lat: 10, lng: 10 }, { lat: 10, lng: 0 }, { lat: 0, lng: 0 } ]); }); it('creates a hull for vertically-aligned objects', function () { expect(L.QuickHull.getConvexHull([ { lat: 0, lng: 0 }, { lat: 5, lng: 0 }, { lat: 10, lng: 0 } ])).to.eql([ { lat: 0, lng: 0 }, { lat: 10, lng: 0 } ]); }); it('creates a hull for horizontally-aligned objects', function () { expect(L.QuickHull.getConvexHull([ { lat: 0, lng: 0 }, { lat: 0, lng: 5 }, { lat: 0, lng: 10 } ])).to.eql([ { lat: 0, lng: 0 }, { lat: 0, lng: 10 } ]); }); }); }); Leaflet.markercluster-0.5.0/spec/suites/RefreshSpec.js000066400000000000000000000335341270301646500230030ustar00rootroot00000000000000describe('refreshClusters', function () { /** * Avoid as much as possible creating and destroying objects for each test. * Instead, try re-using them, except for the ones under test of course. * PhantomJS does not perform garbage collection for the life of the page, * i.e. during the entire test process (Karma runs all tests in a single page). * http://stackoverflow.com/questions/27239708/how-to-get-around-memory-error-with-karma-phantomjs * * The `beforeEach` and `afterEach do not seem to cause much issue. * => they can still be used to initialize some setup between each test. * Using them keeps a readable spec/index. * * But refrain from re-creating div and map every time. Re-use those objects. */ ///////////////////////////// // SETUP FOR EACH TEST ///////////////////////////// beforeEach(function () { clock = sinon.useFakeTimers(); // Look away to avoid refreshing the display while adding markers. // By adding markers one by one (instead of using addLayers) we make // sure we never start an async process. map.fitBounds(new L.LatLngBounds([ [-11, -11], [-10, -10] ])) }); afterEach(function () { if (group instanceof L.MarkerClusterGroup) { group.removeLayers(group.getLayers()); map.removeLayer(group); } // group must be thrown away since we are testing it with a potentially // different configuration at each test. group = null; clock.restore(); clock = null; }); ///////////////////////////// // PREPARATION CODE ///////////////////////////// var div, map, group, clock; div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); // Corresponds to zoom level 8 for the above div dimensions. map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); function getClusterAtZoom(marker, zoom) { var parent = marker.__parent; while (parent && parent._zoom !== zoom) { parent = parent.__parent; } return parent; } function setMapView() { // Now look at the markers to force cluster icons drawing. // Corresponds to zoom level 8 for the above div dimensions. map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); } ///////////////////////////// // TESTS ///////////////////////////// it('flags all non-visible parent clusters of a given marker', function () { group = L.markerClusterGroup().addTo(map); var marker1 = L.marker([1.5, 1.5]).addTo(group), marker2 = L.marker([1.5, 1.5]).addTo(group); // Needed to force a cluster. setMapView(); var marker1cluster10 = getClusterAtZoom(marker1, 10), marker1cluster2 = getClusterAtZoom(marker1, 2), marker1cluster5 = getClusterAtZoom(marker1, 5); // First go to some zoom levels so that Leaflet initializes clusters icons. expect(marker1cluster10._iconNeedsUpdate).to.be.ok(); map.setZoom(10, {animate: false}); expect(marker1cluster10._iconNeedsUpdate).to.not.be.ok(); expect(marker1cluster2._iconNeedsUpdate).to.be.ok(); map.setZoom(2, {animate: false}); expect(marker1cluster2._iconNeedsUpdate).to.not.be.ok(); // Finish on an intermediate zoom level. expect(marker1cluster5._iconNeedsUpdate).to.be.ok(); map.setZoom(5, {animate: false}); expect(marker1cluster5._iconNeedsUpdate).to.not.be.ok(); // Run any animation. clock.tick(1000); // Then request clusters refresh. // No need to actually modify the marker. group.refreshClusters(marker1); // Now check that non-visible clusters are flagged as "dirty". expect(marker1cluster10._iconNeedsUpdate).to.be.ok(); expect(marker1cluster2._iconNeedsUpdate).to.be.ok(); // Also check that visible clusters are "un-flagged" since they should be re-drawn. expect(marker1cluster5._iconNeedsUpdate).to.not.be.ok(); }); it('re-draws visible clusters', function () { group = L.markerClusterGroup({ iconCreateFunction: function (cluster) { var markers = cluster.getAllChildMarkers(); for(var i in markers) { if (markers[i].changed) { return new L.DivIcon({ className: "changed" }); } } return new L.DivIcon({ className: "original" }); } }).addTo(map); var marker1 = L.marker([1.5, 1.5]).addTo(group), marker2 = L.marker([1.5, 1.5]).addTo(group); // Needed to force a cluster. setMapView(); var marker1cluster9 = getClusterAtZoom(marker1, 9); // First go to some zoom levels so that Leaflet initializes clusters icons. expect(marker1cluster9._iconNeedsUpdate).to.be.ok(); map.setZoom(9, {animate: false}); expect(marker1cluster9._iconNeedsUpdate).to.not.be.ok(); expect(marker1cluster9._icon.className).to.contain("original"); expect(marker1cluster9._icon.className).to.not.contain("changed"); // Run any animation. clock.tick(1000); // Alter the marker. marker1.changed = true; // Then request clusters refresh. group.refreshClusters(marker1); // Now check that visible clusters icon is re-drawn. expect(marker1cluster9._icon.className).to.contain("changed"); expect(marker1cluster9._icon.className).to.not.contain("original"); }); // Shared code for the 2 below tests function iconCreateFunction(cluster) { var markers = cluster.getAllChildMarkers(); for(var i in markers) { if (markers[i].changed) { return new L.DivIcon({ className: "changed" }); } } return new L.DivIcon({ className: "original" }); } it('re-draws markers in singleMarkerMode', function () { group = L.markerClusterGroup({ singleMarkerMode: true, iconCreateFunction: iconCreateFunction }).addTo(map); var marker1 = L.marker([1.5, 1.5]).addTo(group); setMapView(); expect(marker1._icon.className).to.contain("original"); // Alter the marker. marker1.changed = true; // Then request clusters refresh. group.refreshClusters(marker1); expect(marker1._icon.className).to.contain("changed"); expect(marker1._icon.className).to.not.contain("original"); }); it('does not modify markers that do not belong to the current group (in singleMarkerMode)', function () { group = L.markerClusterGroup({ singleMarkerMode: true, iconCreateFunction: iconCreateFunction }).addTo(map); var marker1 = L.marker([1.5, 1.5]).addTo(group), marker2 = L.marker([1.5, 1.5], { icon: iconCreateFunction({ getAllChildMarkers: function () { return marker2; } }) }).addTo(map); setMapView(); expect(marker1._icon.className).to.contain("original"); expect(marker2._icon.className).to.contain("original"); // Alter the markers. marker1.changed = true; marker2.changed = true; // Then request clusters refresh. group.refreshClusters([marker1, marker2]); expect(marker1._icon.className).to.contain("changed"); expect(marker1._icon.className).to.not.contain("original"); expect(marker2._icon.className).to.contain("original"); expect(marker2._icon.className).to.not.contain("changed"); }); // Shared code for below tests. var marker1 = L.marker([1.5, 1.5]), marker2 = L.marker([1.5, 1.5]), // Needed to force a cluster. marker3 = L.marker([1.1, 1.1]), marker4 = L.marker([1.1, 1.1]), // Needed to force a cluster. marker5 = L.marker([1.9, 1.9]), marker6 = L.marker([1.9, 1.9]), // Needed to force a cluster. marker1cluster8, marker1cluster3, marker1cluster5, marker3cluster8, marker3cluster3, marker3cluster5, marker5cluster8, marker5cluster3, marker5cluster5; function init3clusterBranches() { group = L.markerClusterGroup({ maxClusterRadius: 2 // Make sure we keep distinct clusters. }).addTo(map); // Populate Marker Cluster Group. marker1.addTo(group); marker2.addTo(group); marker3.addTo(group); marker4.addTo(group); marker5.addTo(group); marker6.addTo(group); setMapView(); marker1cluster8 = getClusterAtZoom(marker1, 8); marker1cluster3 = getClusterAtZoom(marker1, 3); marker1cluster5 = getClusterAtZoom(marker1, 5); marker3cluster8 = getClusterAtZoom(marker3, 8); marker3cluster3 = getClusterAtZoom(marker3, 3); marker3cluster5 = getClusterAtZoom(marker3, 5); marker5cluster8 = getClusterAtZoom(marker5, 8); marker5cluster3 = getClusterAtZoom(marker5, 3); marker5cluster5 = getClusterAtZoom(marker5, 5); // Make sure we have 3 distinct clusters up to zoom level Z (let's choose Z = 3) expect(marker1cluster3._childCount).to.equal(2); expect(marker3cluster3._childCount).to.equal(2); expect(marker5cluster3._childCount).to.equal(2); // First go to some zoom levels so that Leaflet initializes clusters icons. expect(marker1cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker5cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker1cluster3._iconNeedsUpdate).to.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.be.ok(); map.setZoom(3, {animate: false}); expect(marker1cluster3._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.not.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.not.be.ok(); // Finish on an intermediate zoom level. expect(marker1cluster5._iconNeedsUpdate).to.be.ok(); expect(marker3cluster5._iconNeedsUpdate).to.be.ok(); expect(marker5cluster5._iconNeedsUpdate).to.be.ok(); map.setZoom(5, {animate: false}); expect(marker1cluster5._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster5._iconNeedsUpdate).to.not.be.ok(); expect(marker5cluster5._iconNeedsUpdate).to.not.be.ok(); // Run any animation. clock.tick(1000); // Ready to refresh clusters with method of choice and assess result. } it('does not flag clusters of other markers', function () { init3clusterBranches(); // Then request clusters refresh. // No need to actually modify the marker. group.refreshClusters(marker1); // Now check that non-visible clusters are flagged as "dirty". expect(marker1cluster8._iconNeedsUpdate).to.be.ok(); expect(marker1cluster3._iconNeedsUpdate).to.be.ok(); // Finally check that non-involved clusters are not "dirty". expect(marker3cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.not.be.ok(); expect(marker5cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.not.be.ok(); }); it('processes itself when no argument is passed', function () { init3clusterBranches(); // Then request clusters refresh. // No need to actually modify the marker. group.refreshClusters(); // Now check that non-visible clusters are flagged as "dirty". expect(marker1cluster8._iconNeedsUpdate).to.be.ok(); expect(marker1cluster3._iconNeedsUpdate).to.be.ok(); expect(marker3cluster8._iconNeedsUpdate).to.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.be.ok(); expect(marker5cluster8._iconNeedsUpdate).to.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.be.ok(); }); it('accepts an array of markers', function () { init3clusterBranches(); // Then request clusters refresh. // No need to actually modify the markers. group.refreshClusters([marker1, marker5]); // Clusters of marker3 and 4 shall not be flagged. // Now check that non-visible clusters are flagged as "dirty". expect(marker1cluster8._iconNeedsUpdate).to.be.ok(); expect(marker1cluster3._iconNeedsUpdate).to.be.ok(); expect(marker5cluster8._iconNeedsUpdate).to.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.be.ok(); // Clusters of marker3 and 4 shall not be flagged. expect(marker3cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.not.be.ok(); }); it('accepts a mapping of markers', function () { init3clusterBranches(); // Then request clusters refresh. // No need to actually modify the markers. group.refreshClusters({ id1: marker1, id2: marker5 }); // Clusters of marker3 and 4 shall not be flagged. // Now check that non-visible clusters are flagged as "dirty". expect(marker1cluster8._iconNeedsUpdate).to.be.ok(); expect(marker1cluster3._iconNeedsUpdate).to.be.ok(); expect(marker5cluster8._iconNeedsUpdate).to.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.be.ok(); // Clusters of marker3 and 4 shall not be flagged. expect(marker3cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.not.be.ok(); }); it('accepts an L.LayerGroup', function () { init3clusterBranches(); // Then request clusters refresh. // No need to actually modify the markers. var layerGroup = new L.LayerGroup([marker1, marker5]); group.refreshClusters(layerGroup); // Clusters of marker3 and 4 shall not be flagged. // Now check that non-visible clusters are flagged as "dirty". expect(marker1cluster8._iconNeedsUpdate).to.be.ok(); expect(marker1cluster3._iconNeedsUpdate).to.be.ok(); expect(marker5cluster8._iconNeedsUpdate).to.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.be.ok(); // Clusters of marker3 and 4 shall not be flagged. expect(marker3cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.not.be.ok(); }); it('accepts an L.MarkerCluster', function () { init3clusterBranches(); // Then request clusters refresh. // No need to actually modify the markers. group.refreshClusters(marker1cluster8); // Clusters of marker3, 4, 5 and 6 shall not be flagged. // Now check that non-visible clusters are flagged as "dirty". expect(marker1cluster8._iconNeedsUpdate).to.be.ok(); expect(marker1cluster3._iconNeedsUpdate).to.be.ok(); // Clusters of marker3 and 4 shall not be flagged. expect(marker3cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker3cluster3._iconNeedsUpdate).to.not.be.ok(); expect(marker5cluster8._iconNeedsUpdate).to.not.be.ok(); expect(marker5cluster3._iconNeedsUpdate).to.not.be.ok(); }); ///////////////////////////// // CLEAN UP CODE ///////////////////////////// map.remove(); document.body.removeChild(div); }); Leaflet.markercluster-0.5.0/spec/suites/RememberOpacity.js000066400000000000000000000076531270301646500236640ustar00rootroot00000000000000describe('Remember opacity', function () { var map, div, clock, markers; var markerDefs = [ {latLng: [ 0, 0], opts: {opacity: 0.9}}, {latLng: [ 0, 1], opts: {opacity: 0.5}}, {latLng: [ 0,-1], opts: {opacity: 0.5}}, {latLng: [ 1, 0], opts: {opacity: 0.5}}, {latLng: [-1, 0], opts: {opacity: 0.5}}, {latLng: [ 1, 1], opts: {opacity: 0.2}}, {latLng: [ 1,-1], opts: {opacity: 0.2}}, {latLng: [-1, 1], opts: {opacity: 0.2}}, {latLng: [-1,-1], opts: {opacity: 0.2}} ]; var bounds = L.latLngBounds( L.latLng( -1.1, -1.1), L.latLng( 1.1, 1.1) ); beforeEach(function () { clock = sinon.useFakeTimers(); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); markers = []; for (var i=0; i>> 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; }; }Leaflet.markercluster-0.5.0/spec/suites/animateOptionSpec.js000066400000000000000000000111541270301646500242060ustar00rootroot00000000000000describe('animate option', function () { /** * Avoid as much as possible creating and destroying objects for each test. * Instead, try re-using them, except for the ones under test of course. * PhantomJS does not perform garbage collection for the life of the page, * i.e. during the entire test process (Karma runs all tests in a single page). * http://stackoverflow.com/questions/27239708/how-to-get-around-memory-error-with-karma-phantomjs * * The `beforeEach` and `afterEach do not seem to cause much issue. * => they can still be used to initialize some setup between each test. * Using them keeps a readable spec/index. * * But refrain from re-creating div and map every time. Re-use those objects. */ ///////////////////////////// // SETUP FOR EACH TEST ///////////////////////////// beforeEach(function () { previousTransitionSetting = L.DomUtil.TRANSITION; }); afterEach(function () { // Restore previous setting so that next tests are unaffected. L.DomUtil.TRANSITION = previousTransitionSetting; if (group instanceof L.MarkerClusterGroup) { group.removeLayers(group.getLayers()); map.removeLayer(group); } // Throw away group as it can be assigned with different configurations between tests. group = null; }); ///////////////////////////// // PREPARATION CODE ///////////////////////////// var div, map, group, previousTransitionSetting; div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); // Corresponds to zoom level 8 for the above div dimensions. map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); ///////////////////////////// // TESTS ///////////////////////////// it('hooks animated methods version by default', function () { // Need to add to map so that we have the top cluster level created. group = L.markerClusterGroup().addTo(map); var withAnimation = L.MarkerClusterGroup.prototype._withAnimation; // MCG animated methods. expect(group._animationStart).to.be(withAnimation._animationStart); expect(group._animationZoomIn).to.be(withAnimation._animationZoomIn); expect(group._animationZoomOut).to.be(withAnimation._animationZoomOut); expect(group._animationAddLayer).to.be(withAnimation._animationAddLayer); // MarkerCluster spiderfy animated methods var cluster = group._topClusterLevel; withAnimation = L.MarkerCluster.prototype; expect(cluster._animationSpiderfy).to.be(withAnimation._animationSpiderfy); expect(cluster._animationUnspiderfy).to.be(withAnimation._animationUnspiderfy); }); it('hooks non-animated methods version when set to false', function () { // Need to add to map so that we have the top cluster level created. group = L.markerClusterGroup({animate: false}).addTo(map); var noAnimation = L.MarkerClusterGroup.prototype._noAnimation; // MCG non-animated methods. expect(group._animationStart).to.be(noAnimation._animationStart); expect(group._animationZoomIn).to.be(noAnimation._animationZoomIn); expect(group._animationZoomOut).to.be(noAnimation._animationZoomOut); expect(group._animationAddLayer).to.be(noAnimation._animationAddLayer); // MarkerCluster spiderfy non-animated methods var cluster = group._topClusterLevel; noAnimation = L.MarkerClusterNonAnimated.prototype; expect(cluster._animationSpiderfy).to.be(noAnimation._animationSpiderfy); expect(cluster._animationUnspiderfy).to.be(noAnimation._animationUnspiderfy); }); it('always hooks non-animated methods version when L.DomUtil.TRANSITION is false', function () { // Fool Leaflet, make it think the browser does not support transitions. L.DomUtil.TRANSITION = false; // Need to add to map so that we have the top cluster level created. group = L.markerClusterGroup({animate: true}).addTo(map); var noAnimation = L.MarkerClusterGroup.prototype._noAnimation; // MCG non-animated methods. expect(group._animationStart).to.be(noAnimation._animationStart); expect(group._animationZoomIn).to.be(noAnimation._animationZoomIn); expect(group._animationZoomOut).to.be(noAnimation._animationZoomOut); expect(group._animationAddLayer).to.be(noAnimation._animationAddLayer); // MarkerCluster spiderfy non-animated methods var cluster = group._topClusterLevel; noAnimation = L.MarkerClusterNonAnimated.prototype; expect(cluster._animationSpiderfy).to.be(noAnimation._animationSpiderfy); expect(cluster._animationUnspiderfy).to.be(noAnimation._animationUnspiderfy); }); ///////////////////////////// // CLEAN UP CODE ///////////////////////////// map.remove(); document.body.removeChild(div); }); Leaflet.markercluster-0.5.0/spec/suites/clearLayersSpec.js000066400000000000000000000022531270301646500236450ustar00rootroot00000000000000describe('clearLayer', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('clears everything before adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); var marker = new L.Marker([1.5, 1.5]); group.addLayers([polygon, marker]); group.clearLayers(); expect(group.hasLayer(polygon)).to.be(false); expect(group.hasLayer(marker)).to.be(false); }); it('hits polygons and markers after adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); var marker = new L.Marker([1.5, 1.5]); group.addLayers([polygon, marker]); map.addLayer(group); group.clearLayers(); expect(group.hasLayer(polygon)).to.be(false); expect(group.hasLayer(marker)).to.be(false); }); });Leaflet.markercluster-0.5.0/spec/suites/eachLayerSpec.js000066400000000000000000000024741270301646500233010ustar00rootroot00000000000000describe('eachLayer', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('hits polygons and markers before adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); var marker = new L.Marker([1.5, 1.5]); group.addLayers([polygon, marker]); var layers = []; group.eachLayer(function (l) { layers.push(l); }); expect(layers.length).to.be(2); expect(layers).to.contain(marker); expect(layers).to.contain(polygon); }); it('hits polygons and markers after adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); var marker = new L.Marker([1.5, 1.5]); group.addLayers([polygon, marker]); map.addLayer(group); var layers = []; group.eachLayer(function (l) { layers.push(l); }); expect(layers.length).to.be(2); expect(layers).to.contain(marker); expect(layers).to.contain(polygon); }); });Leaflet.markercluster-0.5.0/spec/suites/eventsSpec.js000066400000000000000000000072521270301646500227070ustar00rootroot00000000000000describe('events', function() { var map, div; beforeEach(function() { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function() { document.body.removeChild(div); }); it('is fired for a single child marker', function () { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); group.on('click', callback); group.addLayer(marker); map.addLayer(group); marker.fire('click'); expect(callback.called).to.be(true); }); it('is fired for a child polygon', function () { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.on('click', callback); group.addLayer(polygon); map.addLayer(group); polygon.fire('click'); expect(callback.called).to.be(true); }); it('is fired for a cluster click', function () { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.on('clusterclick', callback); group.addLayers([marker, marker2]); map.addLayer(group); var cluster = group.getVisibleParent(marker); expect(cluster instanceof L.MarkerCluster).to.be(true); cluster.fire('click'); expect(callback.called).to.be(true); }); describe('after being added, removed, re-added from the map', function() { it('still fires events for nonpoint data', function() { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.on('click', callback); group.addLayer(polygon); map.addLayer(group); map.removeLayer(group); map.addLayer(group); polygon.fire('click'); expect(callback.called).to.be(true); }); it('still fires events for point data', function() { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); group.on('click', callback); group.addLayer(marker); map.addLayer(group); map.removeLayer(group); map.addLayer(group); marker.fire('click'); expect(callback.called).to.be(true); }); it('still fires cluster events', function() { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.on('clusterclick', callback); group.addLayers([marker, marker2]); map.addLayer(group); map.removeLayer(group); map.addLayer(group); var cluster = group.getVisibleParent(marker); expect(cluster instanceof L.MarkerCluster).to.be(true); cluster.fire('click'); expect(callback.called).to.be(true); }); it('doesnt break map events', function () { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); map.on('zoomend', callback); map.addLayer(group); map.removeLayer(group); map.addLayer(group); map.fire('zoomend'); expect(callback.called).to.be(true); }); }); /* //No normal events can be fired by a clustered marker, so probably don't need this. it('is fired for a clustered child marker', function() { var callback = sinon.spy(); var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.on('click', callback); group.addLayers([marker, marker2]); map.addLayer(group); marker.fire('click'); expect(callback.called).to.be(true); }); */ });Leaflet.markercluster-0.5.0/spec/suites/getBoundsSpec.js000066400000000000000000000072721270301646500233370ustar00rootroot00000000000000describe('getBounds', function() { var map, div; beforeEach(function() { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function() { document.body.removeChild(div); }); describe('polygon layer', function() { it('returns the correct bounds before adding to the map', function() { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); expect(group.getBounds().equals(polygon.getBounds())).to.be(true); }); it('returns the correct bounds after adding to the map after adding polygon', function() { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayer(polygon); map.addLayer(group); expect(group.getBounds().equals(polygon.getBounds())).to.be(true); }); it('returns the correct bounds after adding to the map before adding polygon', function() { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); map.addLayer(group); group.addLayer(polygon); expect(group.getBounds().equals(polygon.getBounds())).to.be(true); }); }); describe('marker layers', function () { it('returns the correct bounds before adding to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.0, 5.0]); var marker3 = new L.Marker([6.0, 2.0]); group.addLayers([marker, marker2, marker3]); expect(group.getBounds().equals(L.latLngBounds([1.0, 5.0], [6.0, 1.5]))).to.be(true); }); it('returns the correct bounds after adding to the map after adding markers', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.0, 5.0]); var marker3 = new L.Marker([6.0, 2.0]); group.addLayers([marker, marker2, marker3]); map.addLayer(group); expect(group.getBounds().equals(L.latLngBounds([1.0, 5.0], [6.0, 1.5]))).to.be(true); }); it('returns the correct bounds after adding to the map before adding markers', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.0, 5.0]); var marker3 = new L.Marker([6.0, 2.0]); map.addLayer(group); group.addLayers([marker, marker2, marker3]); expect(group.getBounds().equals(L.latLngBounds([1.0, 5.0], [6.0, 1.5]))).to.be(true); }); }); describe('marker and polygon layers', function() { it('returns the correct bounds before adding to the map', function() { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([6.0, 3.0]); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); group.addLayers([marker, polygon]); expect(group.getBounds().equals(L.latLngBounds([1.5, 1.5], [6.0, 3.0]))).to.be(true); }); it('returns the correct bounds after adding to the map', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([6.0, 3.0]); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); map.addLayer(group); group.addLayers([marker, polygon]); expect(group.getBounds().equals(L.latLngBounds([1.5, 1.5], [6.0, 3.0]))).to.be(true); }); }); describe('blank layer', function () { it('returns a blank bounds', function () { var group = new L.MarkerClusterGroup(); expect(group.getBounds().isValid()).to.be(false); }); }); });Leaflet.markercluster-0.5.0/spec/suites/getLayersSpec.js000066400000000000000000000023501270301646500233340ustar00rootroot00000000000000describe('getLayers', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('hits polygons and markers before adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); var marker = new L.Marker([1.5, 1.5]); group.addLayers([polygon, marker]); var layers = group.getLayers(); expect(layers.length).to.be(2); expect(layers).to.contain(marker); expect(layers).to.contain(polygon); }); it('hits polygons and markers after adding to map', function () { var group = new L.MarkerClusterGroup(); var polygon = new L.Polygon([[1.5, 1.5], [2.0, 1.5], [2.0, 2.0], [1.5, 2.0]]); var marker = new L.Marker([1.5, 1.5]); group.addLayers([polygon, marker]); map.addLayer(group); var layers = group.getLayers(); expect(layers.length).to.be(2); expect(layers).to.contain(marker); expect(layers).to.contain(polygon); }); });Leaflet.markercluster-0.5.0/spec/suites/getVisibleParentSpec.js000066400000000000000000000026251270301646500246510ustar00rootroot00000000000000describe('getVisibleParent', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('gets the marker if the marker is visible', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); group.addLayer(marker); map.addLayer(group); var vp = group.getVisibleParent(marker); expect(vp).to.be(marker); }); it('gets the visible cluster if it is clustered', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); var vp = group.getVisibleParent(marker); expect(vp).to.be.a(L.MarkerCluster); expect(vp._icon).to.not.be(null); expect(vp._icon).to.not.be(undefined); }); it('returns null if the marker and parents are all not visible', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([5.5, 1.5]); var marker2 = new L.Marker([5.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); var vp = group.getVisibleParent(marker); expect(vp).to.be(null); }); });Leaflet.markercluster-0.5.0/spec/suites/onAddSpec.js000066400000000000000000000021371270301646500224250ustar00rootroot00000000000000describe('onAdd', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('throws an error if maxZoom is not specified', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); group.addLayer(marker); var ex = null; try { map.addLayer(group); } catch (e) { ex = e; } expect(ex).to.not.be(null); }); it('successfully handles removing and re-adding a layer while not on the map', function () { map.options.maxZoom = 18; var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); map.addLayer(group); group.addLayer(marker); map.removeLayer(group); group.removeLayer(marker); group.addLayer(marker); map.addLayer(group); expect(map.hasLayer(group)).to.be(true); expect(group.hasLayer(marker)).to.be(true); }); });Leaflet.markercluster-0.5.0/spec/suites/onRemoveSpec.js000066400000000000000000000016051270301646500231710ustar00rootroot00000000000000describe('onRemove', function () { var map, div; beforeEach(function () { div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { document.body.removeChild(div); }); it('removes the shown coverage polygon', function () { var group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); var marker3 = new L.Marker([1.5, 1.5]); group.addLayer(marker); group.addLayer(marker2); group.addLayer(marker3); map.addLayer(group); group._showCoverage({ layer: group._topClusterLevel }); expect(group._shownPolygon).to.not.be(null); map.removeLayer(group); expect(group._shownPolygon).to.be(null); }); });Leaflet.markercluster-0.5.0/spec/suites/removeLayersSpec.js000066400000000000000000000042621270301646500240560ustar00rootroot00000000000000describe('removeLayers', function () { var map, div, clock; beforeEach(function () { clock = sinon.useFakeTimers(); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); }); afterEach(function () { clock.restore(); document.body.removeChild(div); }); it('removes all the layer given to it', function () { var group = new L.MarkerClusterGroup(); var markers = [ new L.Marker([1.5, 1.5]), new L.Marker([1.5, 1.5]), new L.Marker([1.5, 1.5]) ]; map.addLayer(group); group.addLayers(markers); group.removeLayers(markers); expect(group.hasLayer(markers[0])).to.be(false); expect(group.hasLayer(markers[1])).to.be(false); expect(group.hasLayer(markers[2])).to.be(false); expect(group.getLayers().length).to.be(0); }); it('removes all the layer given to it even if the group is not on the map', function () { var group = new L.MarkerClusterGroup(); var markers = [ new L.Marker([1.5, 1.5]), new L.Marker([1.5, 1.5]), new L.Marker([1.5, 1.5]) ]; map.addLayer(group); group.addLayers(markers); map.removeLayer(group); group.removeLayers(markers); map.addLayer(group); expect(group.hasLayer(markers[0])).to.be(false); expect(group.hasLayer(markers[1])).to.be(false); expect(group.hasLayer(markers[2])).to.be(false); expect(group.getLayers().length).to.be(0); }); it('doesnt break if we are spiderfied', function () { var group = new L.MarkerClusterGroup(); var markers = [ new L.Marker([1.5, 1.5]), new L.Marker([1.5, 1.5]), new L.Marker([1.5, 1.5]) ]; map.addLayer(group); group.addLayers(markers); markers[0].__parent.spiderfy(); // We must wait for the spiderfy animation to timeout clock.tick(200); group.removeLayers(markers); expect(group.hasLayer(markers[0])).to.be(false); expect(group.hasLayer(markers[1])).to.be(false); expect(group.hasLayer(markers[2])).to.be(false); expect(group.getLayers().length).to.be(0); group.on('spiderfied', function() { expect(group._spiderfied).to.be(null); }); }); }); Leaflet.markercluster-0.5.0/spec/suites/removeOutsideVisibleBoundsSpec.js000066400000000000000000000174761270301646500267370ustar00rootroot00000000000000describe('Option removeOutsideVisibleBounds', function () { /** * Avoid as much as possible creating and destroying objects for each test. * Instead, try re-using them, except for the ones under test of course. * PhantomJS does not perform garbage collection for the life of the page, * i.e. during the entire test process (Karma runs all tests in a single page). * http://stackoverflow.com/questions/27239708/how-to-get-around-memory-error-with-karma-phantomjs * * The `beforeEach` and `afterEach do not seem to cause much issue. * => they can still be used to initialize some setup between each test. * Using them keeps a readable spec/index. * * But refrain from re-creating div and map every time. Re-use those objects. */ ///////////////////////////// // SETUP FOR EACH TEST ///////////////////////////// beforeEach(function () { // Nothing for this test suite. }); afterEach(function () { // Restore the previous setting, so that even in case of test failure, next tests are not affected. L.Browser.mobile = previousMobileSetting; if (group instanceof L.MarkerClusterGroup) { group.removeLayers(group.getLayers()); map.removeLayer(group); } // group must be thrown away since we are testing it with a potentially // different configuration at each test. group = null; }); ///////////////////////////// // PREPARATION CODE ///////////////////////////// var marker1 = L.marker([1.5, -0.4]), // 2 screens width away. marker2 = L.marker([1.5, 0.6]), // 1 screen width away. marker3 = L.marker([1.5, 1.5]), // In view port. marker4 = L.marker([1.5, 2.4]), // 1 screen width away. marker5 = L.marker([1.5, 3.4]), // 2 screens width away. markers = [marker1, marker2, marker3, marker4, marker5], previousMobileSetting = L.Browser.mobile, div, map, group, i; div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); // Corresponds to zoom level 8 for the above div dimensions. map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); // Add all markers once to map then remove them immediately so that their icon is null (instead of undefined). for (i = 0; i < markers.length; i++) { map.removeLayer(markers[i].addTo(map)); } function prepareGroup() { // "group" should be assigned with a Marker Cluster Group before calling this function. group.addTo(map); group.addLayers(markers); } ///////////////////////////// // TESTS ///////////////////////////// it('removes objects more than 1 screen away from view port by default', function () { group = L.markerClusterGroup(); prepareGroup(); expect(marker1._icon).to.be(null); expect(map._panes.markerPane.childNodes.length).to.be(3); // markers 2, 3 and 4. expect(marker5._icon).to.be(null); }); it('removes objects out of view port by default for mobile device', function () { // Fool Leaflet, make it thinks it runs on a mobile device. L.Browser.mobile = true; group = L.markerClusterGroup(); prepareGroup(); expect(marker1._icon).to.be(null); expect(marker2._icon).to.be(null); expect(map._panes.markerPane.childNodes.length).to.be(1); // marker 3 only. expect(marker4._icon).to.be(null); expect(marker5._icon).to.be(null); }); it('leaves all objects on map when set to false', function () { group = L.markerClusterGroup({ removeOutsideVisibleBounds: false }); prepareGroup(); expect(map._panes.markerPane.childNodes.length).to.be(5); // All 5 markers. }); // Following tests need markers at very high latitude. // They test the _checkBoundsMaxLat method against the default Web/Spherical Mercator projection maximum latitude (85.0511287798). // The actual map view should be '-1.0986328125,84.92929204957956,1.0986328125,85.11983467698401' // The expdanded bounds without correction should be '-3.2958984375,84.7387494221751,3.2958984375,85.31037730438847' var latLngsMaxLatDefault = [ [100, 3], // Impossible in real world, but nothing prevents the user from entering such latitude, and Web/Spherical Mercator projection will still display it at 85.0511287798 [85.2, 1.5], // 1 "screen" heights away. [85, 0], // In center of view. [84.8, -1.5], // 1 "screen" height away. [84.6, -3] // 2 "screens" height away. ]; function moveMarkersAndMapToMaxLat(latLngs, isSouth) { for (i = 0; i < markers.length; i++) { if (isSouth) { markers[i].setLatLng([-latLngs[i][0], latLngs[i][1]]); } else { markers[i].setLatLng(latLngs[i]); } } map.fitBounds([ [isSouth ? -86 : 85, -1], [isSouth ? -85 : 86, 1] // The actual map view longitude span will be wider. '-1.0986328125,84.92929204957956,1.0986328125,85.11983467698401' ]); } function checkProjection(latLngs) { expect(map.options.crs).to.equal(L.CRS.EPSG3857); expect(L.CRS.EPSG3857.projection).to.equal(L.Projection.SphericalMercator); expect(L.Projection.SphericalMercator.MAX_LATITUDE).to.be.a('number'); var mapZoom = map.getZoom(); for (i = 0; i < markers.length; i++) { try { expect(markers[i].__parent._zoom).to.be.below(mapZoom); } catch (e) { console.log("Failed marker: " + (i + 1)); throw e; } } } it('includes objects above the Web Mercator projection maximum limit by default', function () { moveMarkersAndMapToMaxLat(latLngsMaxLatDefault); group = L.markerClusterGroup(); prepareGroup(); checkProjection(latLngsMaxLatDefault); expect(map._panes.markerPane.childNodes.length).to.be(4); // Markers 1, 2, 3 and 4. expect(marker5._icon).to.be(null); }); it('includes objects below the Web Mercator projection minimum limit by default', function () { moveMarkersAndMapToMaxLat(latLngsMaxLatDefault, true); // Make sure we are really in Southern hemisphere. expect(map.getBounds().getNorth()).to.be.below(-80); group = L.markerClusterGroup(); prepareGroup(); checkProjection(latLngsMaxLatDefault); expect(map._panes.markerPane.childNodes.length).to.be(4); // Markers 1, 2, 3 and 4. expect(marker5._icon).to.be(null); }); // The actual map view should be '-1.0986328125,84.92929204957956,1.0986328125,85.11983467698401' var latLngsMaxLatMobile = [ [100, 1], // Impossible in real world, but nothing prevents the user from entering such latitude, and Web/Spherical Mercator projection will still display it at 85.0511287798 [85.2, 0.5], // 1 "screen" heights away, but should be included by the correction. [85, 0], // In center of view. [84.9, -1], // 1 "screen" height away. [84.8, -1.5] // 2 "screens" height away. ]; it('includes objects above the Web Mercator projection maximum limit for mobile device', function () { // Fool Leaflet, make it thinks it runs on a mobile device. L.Browser.mobile = true; moveMarkersAndMapToMaxLat(latLngsMaxLatMobile); group = L.markerClusterGroup({ maxClusterRadius: 10 }); prepareGroup(); checkProjection(latLngsMaxLatMobile); expect(map._panes.markerPane.childNodes.length).to.be(3); // Markers 1, 2 and 3. expect(marker4._icon).to.be(null); expect(marker5._icon).to.be(null); }); it('includes objects below the Web Mercator projection minimum limit for mobile device', function () { // Fool Leaflet, make it thinks it runs on a mobile device. L.Browser.mobile = true; moveMarkersAndMapToMaxLat(latLngsMaxLatMobile, true); // Make sure we are really in Southern hemisphere. expect(map.getBounds().getNorth()).to.be.below(-80); group = L.markerClusterGroup({ maxClusterRadius: 10 }); prepareGroup(); checkProjection(latLngsMaxLatMobile); expect(map._panes.markerPane.childNodes.length).to.be(3); // Markers 1, 2 and 3. expect(marker4._icon).to.be(null); expect(marker5._icon).to.be(null); }); ///////////////////////////// // CLEAN UP CODE ///////////////////////////// map.remove(); document.body.removeChild(div); }); Leaflet.markercluster-0.5.0/spec/suites/singleMarkerModeSpec.js000066400000000000000000000050431270301646500246270ustar00rootroot00000000000000describe('singleMarkerMode option', function () { /** * Avoid as much as possible creating and destroying objects for each test. * Instead, try re-using them, except for the ones under test of course. * PhantomJS does not perform garbage collection for the life of the page, * i.e. during the entire test process (Karma runs all tests in a single page). * http://stackoverflow.com/questions/27239708/how-to-get-around-memory-error-with-karma-phantomjs * * The `beforeEach` and `afterEach do not seem to cause much issue. * => they can still be used to initialize some setup between each test. * Using them keeps a readable spec/index. * * But refrain from re-creating div and map every time. Re-use those objects. */ ///////////////////////////// // SETUP FOR EACH TEST ///////////////////////////// beforeEach(function () { // Reset the marker icon. marker.setIcon(defaultIcon); }); afterEach(function () { if (group instanceof L.MarkerClusterGroup) { group.removeLayers(group.getLayers()); map.removeLayer(group); } // Throw away group as it can be assigned with different configurations between tests. group = null; }); ///////////////////////////// // PREPARATION CODE ///////////////////////////// var div, map, group; var defaultIcon = new L.Icon.Default(), clusterIcon = new L.Icon.Default(), marker = L.marker([1.5, 1.5]); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); // Corresponds to zoom level 8 for the above div dimensions. map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); ///////////////////////////// // TESTS ///////////////////////////// it('overrides marker icons when set to true', function () { group = L.markerClusterGroup({ singleMarkerMode: true, iconCreateFunction: function (layer) { return clusterIcon; } }).addTo(map); expect(marker.options.icon).to.equal(defaultIcon); marker.addTo(group); expect(marker.options.icon).to.equal(clusterIcon); }); it('does not modify marker icons by default (or set to false)', function () { group = L.markerClusterGroup({ iconCreateFunction: function (layer) { return clusterIcon; } }).addTo(map); expect(marker.options.icon).to.equal(defaultIcon); marker.addTo(group); expect(marker.options.icon).to.equal(defaultIcon); }); ///////////////////////////// // CLEAN UP CODE ///////////////////////////// map.remove(); document.body.removeChild(div); }); Leaflet.markercluster-0.5.0/spec/suites/spiderfySpec.js000066400000000000000000000213361270301646500232270ustar00rootroot00000000000000describe('spiderfy', function () { /** * Avoid as much as possible creating and destroying objects for each test. * Instead, try re-using them, except for the ones under test of course. * PhantomJS does not perform garbage collection for the life of the page, * i.e. during the entire test process (Karma runs all tests in a single page). * http://stackoverflow.com/questions/27239708/how-to-get-around-memory-error-with-karma-phantomjs * * The `beforeEach` and `afterEach do not seem to cause much issue. * => they can still be used to initialize some setup between each test. * Using them keeps a readable spec/index. * * But refrain from re-creating div and map every time. Re-use those objects. */ ///////////////////////////// // SETUP FOR EACH TEST ///////////////////////////// beforeEach(function () { clock = sinon.useFakeTimers(); }); afterEach(function () { if (group instanceof L.MarkerClusterGroup) { group.clearLayers(); map.removeLayer(group); } // group must be thrown away since we are testing it with a potentially // different configuration at each test. group = null; clock.restore(); clock = null; }); ///////////////////////////// // PREPARATION CODE ///////////////////////////// var div, map, group, clock; div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); // Corresponds to zoom level 8 for the above div dimensions. map.fitBounds(new L.LatLngBounds([ [1, 1], [2, 2] ])); ///////////////////////////// // TESTS ///////////////////////////// it('Spiderfies 2 Markers', function () { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayer(marker); group.addLayer(marker2); map.addLayer(group); marker.__parent.spiderfy(); expect(marker._icon.parentNode).to.be(map._panes.markerPane); expect(marker2._icon.parentNode).to.be(map._panes.markerPane); }); it('Spiderfies 2 CircleMarkers', function () { group = new L.MarkerClusterGroup(); var marker = new L.CircleMarker([1.5, 1.5]); var marker2 = new L.CircleMarker([1.5, 1.5]); group.addLayer(marker); group.addLayer(marker2); map.addLayer(group); marker.__parent.spiderfy(); expect(marker._container.parentNode).to.be(map._pathRoot); expect(marker2._container.parentNode).to.be(map._pathRoot); }); it('Spiderfies 2 Circles', function () { group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 10); var marker2 = new L.Circle([1.5, 1.5], 10); group.addLayer(marker); group.addLayer(marker2); map.addLayer(group); marker.__parent.spiderfy(); expect(marker._container.parentNode).to.be(map._pathRoot); expect(marker2._container.parentNode).to.be(map._pathRoot); }); it('Spiderfies at current zoom if all child markers are at the exact same position', function () { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); // Get the appropriate cluster. var cluster = marker.__parent, zoom = map.getZoom(); while (cluster._zoom !== zoom) { cluster = cluster.__parent; } expect(zoom).to.be.lessThan(10); cluster.fireEvent('click'); clock.tick(1000); expect(map.getZoom()).to.equal(zoom); expect(marker._icon.parentNode).to.be(map._panes.markerPane); expect(marker2._icon.parentNode).to.be(map._panes.markerPane); }); it('Spiderfies at current zoom if all child markers are still within a single cluster at map maxZoom', function () { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.50001]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); expect(marker.__parent._zoom).to.equal(18); // Get the appropriate cluster. var cluster = marker.__parent, zoom = map.getZoom(); while (cluster._zoom !== zoom) { cluster = cluster.__parent; } expect(zoom).to.be.lessThan(10); cluster.fireEvent('click'); clock.tick(1000); expect(map.getZoom()).to.equal(zoom); expect(marker._icon.parentNode).to.be(map._panes.markerPane); expect(marker2._icon.parentNode).to.be(map._panes.markerPane); }); it('removes all markers and spider legs when group is removed from map', function () { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); marker.__parent.spiderfy(); expect(map._panes.markerPane.childNodes.length).to.be(3); // The 2 markers + semi-transparent cluster. expect(map._pathRoot.childNodes.length).to.be(2); // The 2 spider legs. }); it('adds then removes class "leaflet-cluster-anim" from mapPane on spiderfy', function () { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); marker.__parent.spiderfy(); expect(map._panes.mapPane.className).to.contain('leaflet-cluster-anim'); clock.tick(1000); expect(map._panes.mapPane.className).to.not.contain('leaflet-cluster-anim'); }); it('adds then removes class "leaflet-cluster-anim" from mapPane on unspiderfy', function () { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); marker.__parent.spiderfy(); clock.tick(1000); marker.__parent.unspiderfy(); expect(map._panes.mapPane.className).to.contain('leaflet-cluster-anim'); clock.tick(1000); expect(map._panes.mapPane.className).to.not.contain('leaflet-cluster-anim'); }); it('fires unspiderfied event on unspiderfy', function (done) { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); marker.__parent.spiderfy(); clock.tick(1000); // Add event listener group.on('unspiderfied', function (event) { expect(event.target).to.be(group); expect(event.cluster).to.be.a(L.Marker); expect(event.markers[1]).to.be(marker); expect(event.markers[0]).to.be(marker2); done(); }); marker.__parent.unspiderfy(); clock.tick(1000); }); it('does not leave class "leaflet-cluster-anim" on mapPane when group is removed while spiderfied', function () { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayers([marker, marker2]); map.addLayer(group); marker.__parent.spiderfy(); clock.tick(1000); map.removeLayer(group); expect(map._panes.mapPane.className).to.not.contain('leaflet-cluster-anim'); }); describe('zoomend event listener', function () { it('unspiderfies correctly', function () { group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 10); var marker2 = new L.Circle([1.5, 1.5], 10); group.addLayer(marker); group.addLayer(marker2); map.addLayer(group); marker.__parent.spiderfy(); expect(group._spiderfied).to.not.be(null); map.fire('zoomend'); //We should unspiderfy with no animation, so this should be null expect(group._spiderfied).to.be(null); }); }); describe('spiderfied event listener', function () { it('Spiderfies 2 Markers', function (done) { group = new L.MarkerClusterGroup(); var marker = new L.Marker([1.5, 1.5]); var marker2 = new L.Marker([1.5, 1.5]); group.addLayer(marker); group.addLayer(marker2); map.addLayer(group); // Add event listener group.on('spiderfied', function (event) { expect(event.target).to.be(group); expect(event.cluster).to.be.a(L.Marker); expect(event.markers[1]).to.be(marker); expect(event.markers[0]).to.be(marker2); done(); }); marker.__parent.spiderfy(); clock.tick(200); }); it('Spiderfies 2 Circles', function (done) { group = new L.MarkerClusterGroup(); var marker = new L.Circle([1.5, 1.5], 10); var marker2 = new L.Circle([1.5, 1.5], 10); group.addLayer(marker); group.addLayer(marker2); map.addLayer(group); // Add event listener group.on('spiderfied', function (event) { expect(event.target).to.be(group); expect(event.cluster).to.be.a(L.Marker); expect(event.markers[1]).to.be(marker); expect(event.markers[0]).to.be(marker2); done(); }); marker.__parent.spiderfy(); clock.tick(200); }); }); ///////////////////////////// // CLEAN UP CODE ///////////////////////////// map.remove(); document.body.removeChild(div); }); Leaflet.markercluster-0.5.0/spec/suites/zoomAnimationSpec.js000066400000000000000000000060371270301646500242270ustar00rootroot00000000000000describe('zoomAnimation', function () { var map, div, clock; beforeEach(function () { clock = sinon.useFakeTimers(); div = document.createElement('div'); div.style.width = '200px'; div.style.height = '200px'; document.body.appendChild(div); map = L.map(div, { maxZoom: 18 }); }); afterEach(function () { clock.restore(); document.body.removeChild(div); }); it('adds the visible marker to the map when zooming in', function () { map.setView(new L.LatLng(-37.36142550190516, 174.254150390625), 7); var markers = new L.MarkerClusterGroup({ showCoverageOnHover: true, maxClusterRadius: 20, disableClusteringAtZoom: 15 }); var marker = new L.Marker([-37.77852090603777, 175.3103667497635]); markers.addLayer(marker); //The one we zoom in on markers.addLayer(new L.Marker([-37.711800591811055, 174.50034790039062])); //Marker that we cluster with at the top zoom level, but not 1 level down map.addLayer(markers); clock.tick(1000); map.setView([-37.77852090603777, 175.3103667497635], 15); //Run the the animation clock.tick(1000); expect(marker._icon).to.not.be(undefined); expect(marker._icon).to.not.be(null); }); it('adds the visible marker to the map when jumping around', function () { var markers = new L.MarkerClusterGroup(); var marker1 = new L.Marker([48.858280181884766, 2.2945759296417236]); var marker2 = new L.Marker([16.02359962463379, -61.70280075073242]); markers.addLayer(marker1); //The one we zoom in on first markers.addLayer(marker2); //Marker that we cluster with at the top zoom level, but not 1 level down map.addLayer(markers); //show the first map.fitBounds(new L.LatLngBounds(new L.LatLng(41.371582, -5.142222), new L.LatLng(51.092804, 9.561556))); clock.tick(1000); map.fitBounds(new L.LatLngBounds(new L.LatLng(15.830972671508789, -61.807167053222656), new L.LatLng(16.516849517822266, -61.0))); //Run the the animation clock.tick(1000); //Now the second one should be visible on the map expect(marker2._icon).to.not.be(undefined); expect(marker2._icon).to.not.be(null); }); it('adds the visible markers to the map, but not parent clusters when jumping around', function () { var markers = new L.MarkerClusterGroup(), marker1 = new L.Marker([59.9520, 30.3307]), marker2 = new L.Marker([59.9516, 30.3308]), marker3 = new L.Marker([59.9513, 30.3312]); markers.addLayer(marker1); markers.addLayer(marker2); markers.addLayer(marker3); map.addLayer(markers); //Show none of them map.setView([53.0676, 170.6835], 16); clock.tick(1000); //Zoom so that all the markers will be visible (Same as zoomToShowLayer) map.setView(marker1.getLatLng(), 18); //Run the the animation clock.tick(1000); //Now the markers should all be visible, and there should be no visible clusters expect(marker1._icon.parentNode).to.be(map._panes.markerPane); expect(marker2._icon.parentNode).to.be(map._panes.markerPane); expect(marker3._icon.parentNode).to.be(map._panes.markerPane); expect(map._panes.markerPane.childNodes.length).to.be(3); }); });Leaflet.markercluster-0.5.0/src/000077500000000000000000000000001270301646500165455ustar00rootroot00000000000000Leaflet.markercluster-0.5.0/src/DistanceGrid.js000066400000000000000000000043721270301646500214510ustar00rootroot00000000000000 L.DistanceGrid = function (cellSize) { this._cellSize = cellSize; this._sqCellSize = cellSize * cellSize; this._grid = {}; this._objectPoint = { }; }; L.DistanceGrid.prototype = { addObject: function (obj, point) { var x = this._getCoord(point.x), y = this._getCoord(point.y), grid = this._grid, row = grid[y] = grid[y] || {}, cell = row[x] = row[x] || [], stamp = L.Util.stamp(obj); this._objectPoint[stamp] = point; cell.push(obj); }, updateObject: function (obj, point) { this.removeObject(obj); this.addObject(obj, point); }, //Returns true if the object was found removeObject: function (obj, point) { var x = this._getCoord(point.x), y = this._getCoord(point.y), grid = this._grid, row = grid[y] = grid[y] || {}, cell = row[x] = row[x] || [], i, len; delete this._objectPoint[L.Util.stamp(obj)]; for (i = 0, len = cell.length; i < len; i++) { if (cell[i] === obj) { cell.splice(i, 1); if (len === 1) { delete row[x]; } return true; } } }, eachObject: function (fn, context) { var i, j, k, len, row, cell, removed, grid = this._grid; for (i in grid) { row = grid[i]; for (j in row) { cell = row[j]; for (k = 0, len = cell.length; k < len; k++) { removed = fn.call(context, cell[k]); if (removed) { k--; len--; } } } } }, getNearObject: function (point) { var x = this._getCoord(point.x), y = this._getCoord(point.y), i, j, k, row, cell, len, obj, dist, objectPoint = this._objectPoint, closestDistSq = this._sqCellSize, closest = null; for (i = y - 1; i <= y + 1; i++) { row = this._grid[i]; if (row) { for (j = x - 1; j <= x + 1; j++) { cell = row[j]; if (cell) { for (k = 0, len = cell.length; k < len; k++) { obj = cell[k]; dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point); if (dist < closestDistSq) { closestDistSq = dist; closest = obj; } } } } } } return closest; }, _getCoord: function (x) { return Math.floor(x / this._cellSize); }, _sqDist: function (p, p2) { var dx = p2.x - p.x, dy = p2.y - p.y; return dx * dx + dy * dy; } }; Leaflet.markercluster-0.5.0/src/MarkerCluster.QuickHull.js000066400000000000000000000112651270301646500235730ustar00rootroot00000000000000/* Copyright (c) 2012 the authors listed at the following URL, and/or the authors of referenced articles or incorporated external code: http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434 */ (function () { L.QuickHull = { /* * @param {Object} cpt a point to be measured from the baseline * @param {Array} bl the baseline, as represented by a two-element * array of latlng objects. * @returns {Number} an approximate distance measure */ getDistant: function (cpt, bl) { var vY = bl[1].lat - bl[0].lat, vX = bl[0].lng - bl[1].lng; return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng)); }, /* * @param {Array} baseLine a two-element array of latlng objects * representing the baseline to project from * @param {Array} latLngs an array of latlng objects * @returns {Object} the maximum point and all new points to stay * in consideration for the hull. */ findMostDistantPointFromBaseLine: function (baseLine, latLngs) { var maxD = 0, maxPt = null, newPoints = [], i, pt, d; for (i = latLngs.length - 1; i >= 0; i--) { pt = latLngs[i]; d = this.getDistant(pt, baseLine); if (d > 0) { newPoints.push(pt); } else { continue; } if (d > maxD) { maxD = d; maxPt = pt; } } return { maxPoint: maxPt, newPoints: newPoints }; }, /* * Given a baseline, compute the convex hull of latLngs as an array * of latLngs. * * @param {Array} latLngs * @returns {Array} */ buildConvexHull: function (baseLine, latLngs) { var convexHullBaseLines = [], t = this.findMostDistantPointFromBaseLine(baseLine, latLngs); if (t.maxPoint) { // if there is still a point "outside" the base line convexHullBaseLines = convexHullBaseLines.concat( this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints) ); convexHullBaseLines = convexHullBaseLines.concat( this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints) ); return convexHullBaseLines; } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull return [baseLine[0]]; } }, /* * Given an array of latlngs, compute a convex hull as an array * of latlngs * * @param {Array} latLngs * @returns {Array} */ getConvexHull: function (latLngs) { // find first baseline var maxLat = false, minLat = false, maxLng = false, minLng = false, maxLatPt = null, minLatPt = null, maxLngPt = null, minLngPt = null, maxPt = null, minPt = null, i; for (i = latLngs.length - 1; i >= 0; i--) { var pt = latLngs[i]; if (maxLat === false || pt.lat > maxLat) { maxLatPt = pt; maxLat = pt.lat; } if (minLat === false || pt.lat < minLat) { minLatPt = pt; minLat = pt.lat; } if (maxLng === false || pt.lng > maxLng) { maxLngPt = pt; maxLng = pt.lng; } if (minLng === false || pt.lng < minLng) { minLngPt = pt; minLng = pt.lng; } } if (minLat !== maxLat) { minPt = minLatPt; maxPt = maxLatPt; } else { minPt = minLngPt; maxPt = maxLngPt; } var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs), this.buildConvexHull([maxPt, minPt], latLngs)); return ch; } }; }()); L.MarkerCluster.include({ getConvexHull: function () { var childMarkers = this.getAllChildMarkers(), points = [], p, i; for (i = childMarkers.length - 1; i >= 0; i--) { p = childMarkers[i].getLatLng(); points.push(p); } return L.QuickHull.getConvexHull(points); } }); Leaflet.markercluster-0.5.0/src/MarkerCluster.Spiderfier.js000066400000000000000000000311561270301646500237670ustar00rootroot00000000000000//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet //Huge thanks to jawj for implementing it first to make my job easy :-) L.MarkerCluster.include({ _2PI: Math.PI * 2, _circleFootSeparation: 25, //related to circumference of circle _circleStartAngle: Math.PI / 6, _spiralFootSeparation: 28, //related to size of spiral (experiment!) _spiralLengthStart: 11, _spiralLengthFactor: 5, _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards. // 0 -> always spiral; Infinity -> always circle spiderfy: function () { if (this._group._spiderfied === this || this._group._inZoomAnimation) { return; } var childMarkers = this.getAllChildMarkers(), group = this._group, map = group._map, center = map.latLngToLayerPoint(this._latlng), positions; this._group._unspiderfy(); this._group._spiderfied = this; //TODO Maybe: childMarkers order by distance to center if (childMarkers.length >= this._circleSpiralSwitchover) { positions = this._generatePointsSpiral(childMarkers.length, center); } else { center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons. positions = this._generatePointsCircle(childMarkers.length, center); } this._animationSpiderfy(childMarkers, positions); }, unspiderfy: function (zoomDetails) { /// Argument from zoomanim if being called in a zoom animation or null otherwise if (this._group._inZoomAnimation) { return; } this._animationUnspiderfy(zoomDetails); this._group._spiderfied = null; }, _generatePointsCircle: function (count, centerPt) { var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), legLength = circumference / this._2PI, //radius from circumference angleStep = this._2PI / count, res = [], i, angle; res.length = count; for (i = count - 1; i >= 0; i--) { angle = this._circleStartAngle + i * angleStep; res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); } return res; }, _generatePointsSpiral: function (count, centerPt) { var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier, legLength = spiderfyDistanceMultiplier * this._spiralLengthStart, separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, angle = 0, res = [], i; res.length = count; // Higher index, closer position to cluster center. for (i = count - 1; i >= 0; i--) { angle += separation / legLength + i * 0.0005; res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); legLength += lengthFactor / angle; } return res; }, _noanimationUnspiderfy: function () { var group = this._group, map = group._map, fg = group._featureGroup, childMarkers = this.getAllChildMarkers(), m, i; this.setOpacity(1); for (i = childMarkers.length - 1; i >= 0; i--) { m = childMarkers[i]; fg.removeLayer(m); if (m._preSpiderfyLatlng) { m.setLatLng(m._preSpiderfyLatlng); delete m._preSpiderfyLatlng; } if (m.setZIndexOffset) { m.setZIndexOffset(0); } if (m._spiderLeg) { map.removeLayer(m._spiderLeg); delete m._spiderLeg; } } group.fire('unspiderfied', { cluster: this, markers: childMarkers }); group._spiderfied = null; } }); //Non Animated versions of everything L.MarkerClusterNonAnimated = L.MarkerCluster.extend({ _animationSpiderfy: function (childMarkers, positions) { var group = this._group, map = group._map, fg = group._featureGroup, legOptions = this._group.options.spiderLegPolylineOptions, i, m, leg, newPos; // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. // The reverse order trick no longer improves performance on modern browsers. for (i = 0; i < childMarkers.length; i++) { newPos = map.layerPointToLatLng(positions[i]); m = childMarkers[i]; // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. leg = new L.Polyline([this._latlng, newPos], legOptions); map.addLayer(leg); m._spiderLeg = leg; // Now add the marker. m._preSpiderfyLatlng = m._latlng; m.setLatLng(newPos); if (m.setZIndexOffset) { m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING } fg.addLayer(m); } this.setOpacity(0.3); group.fire('spiderfied', { cluster: this, markers: childMarkers }); }, _animationUnspiderfy: function () { this._noanimationUnspiderfy(); } }); //Animated versions here L.MarkerCluster.include({ _animationSpiderfy: function (childMarkers, positions) { var me = this, group = this._group, map = group._map, fg = group._featureGroup, thisLayerLatLng = this._latlng, thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng), svg = L.Path.SVG, legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation. finalLegOpacity = legOptions.opacity, i, m, leg, legPath, legLength, newPos; if (finalLegOpacity === undefined) { finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity; } if (svg) { // If the initial opacity of the spider leg is not 0 then it appears before the animation starts. legOptions.opacity = 0; // Add the class for CSS transitions. legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg'; } else { // Make sure we have a defined opacity. legOptions.opacity = finalLegOpacity; } // Add markers and spider legs to map, hidden at our center point. // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. // The reverse order trick no longer improves performance on modern browsers. for (i = 0; i < childMarkers.length; i++) { m = childMarkers[i]; newPos = map.layerPointToLatLng(positions[i]); // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. leg = new L.Polyline([thisLayerLatLng, newPos], legOptions); map.addLayer(leg); m._spiderLeg = leg; // Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/ // In our case the transition property is declared in the CSS file. if (svg) { legPath = leg._path; legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox. legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated. legPath.style.strokeDashoffset = legLength; } // If it is a marker, add it now and we'll animate it out if (m.setZIndexOffset) { m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING } if (m.clusterHide) { m.clusterHide(); } // Vectors just get immediately added fg.addLayer(m); if (m._setPos) { m._setPos(thisLayerPos); } } group._forceLayout(); group._animationStart(); // Reveal markers and spider legs. for (i = childMarkers.length - 1; i >= 0; i--) { newPos = map.layerPointToLatLng(positions[i]); m = childMarkers[i]; //Move marker to new position m._preSpiderfyLatlng = m._latlng; m.setLatLng(newPos); if (m.clusterShow) { m.clusterShow(); } // Animate leg (animation is actually delegated to CSS transition). if (svg) { leg = m._spiderLeg; legPath = leg._path; legPath.style.strokeDashoffset = 0; //legPath.style.strokeOpacity = finalLegOpacity; leg.setStyle({opacity: finalLegOpacity}); } } this.setOpacity(0.3); setTimeout(function () { group._animationEnd(); group.fire('spiderfied', { cluster: me, markers: childMarkers }); }, 200); }, _animationUnspiderfy: function (zoomDetails) { var me = this, group = this._group, map = group._map, fg = group._featureGroup, thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng), childMarkers = this.getAllChildMarkers(), svg = L.Path.SVG, m, i, leg, legPath, legLength, nonAnimatable; group._animationStart(); //Make us visible and bring the child markers back in this.setOpacity(1); for (i = childMarkers.length - 1; i >= 0; i--) { m = childMarkers[i]; //Marker was added to us after we were spiderfied if (!m._preSpiderfyLatlng) { continue; } //Fix up the location to the real one m.setLatLng(m._preSpiderfyLatlng); delete m._preSpiderfyLatlng; //Hack override the location to be our center nonAnimatable = true; if (m._setPos) { m._setPos(thisLayerPos); nonAnimatable = false; } if (m.clusterHide) { m.clusterHide(); nonAnimatable = false; } if (nonAnimatable) { fg.removeLayer(m); } // Animate the spider leg back in (animation is actually delegated to CSS transition). if (svg) { leg = m._spiderLeg; legPath = leg._path; legLength = legPath.getTotalLength() + 0.1; legPath.style.strokeDashoffset = legLength; leg.setStyle({opacity: 0}); } } setTimeout(function () { //If we have only <= one child left then that marker will be shown on the map so don't remove it! var stillThereChildCount = 0; for (i = childMarkers.length - 1; i >= 0; i--) { m = childMarkers[i]; if (m._spiderLeg) { stillThereChildCount++; } } for (i = childMarkers.length - 1; i >= 0; i--) { m = childMarkers[i]; if (!m._spiderLeg) { //Has already been unspiderfied continue; } if (m.clusterShow) { m.clusterShow(); } if (m.setZIndexOffset) { m.setZIndexOffset(0); } if (stillThereChildCount > 1) { fg.removeLayer(m); } map.removeLayer(m._spiderLeg); delete m._spiderLeg; } group._animationEnd(); group.fire('unspiderfied', { cluster: me, markers: childMarkers }); }, 200); } }); L.MarkerClusterGroup.include({ //The MarkerCluster currently spiderfied (if any) _spiderfied: null, _spiderfierOnAdd: function () { this._map.on('click', this._unspiderfyWrapper, this); if (this._map.options.zoomAnimation) { this._map.on('zoomstart', this._unspiderfyZoomStart, this); } //Browsers without zoomAnimation or a big zoom don't fire zoomstart this._map.on('zoomend', this._noanimationUnspiderfy, this); }, _spiderfierOnRemove: function () { this._map.off('click', this._unspiderfyWrapper, this); this._map.off('zoomstart', this._unspiderfyZoomStart, this); this._map.off('zoomanim', this._unspiderfyZoomAnim, this); this._map.off('zoomend', this._noanimationUnspiderfy, this); //Ensure that markers are back where they should be // Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane this._noanimationUnspiderfy(); }, //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated) //This means we can define the animation they do rather than Markers doing an animation to their actual location _unspiderfyZoomStart: function () { if (!this._map) { //May have been removed from the map by a zoomEnd handler return; } this._map.on('zoomanim', this._unspiderfyZoomAnim, this); }, _unspiderfyZoomAnim: function (zoomDetails) { //Wait until the first zoomanim after the user has finished touch-zooming before running the animation if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) { return; } this._map.off('zoomanim', this._unspiderfyZoomAnim, this); this._unspiderfy(zoomDetails); }, _unspiderfyWrapper: function () { /// _unspiderfy but passes no arguments this._unspiderfy(); }, _unspiderfy: function (zoomDetails) { if (this._spiderfied) { this._spiderfied.unspiderfy(zoomDetails); } }, _noanimationUnspiderfy: function () { if (this._spiderfied) { this._spiderfied._noanimationUnspiderfy(); } }, //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc _unspiderfyLayer: function (layer) { if (layer._spiderLeg) { this._featureGroup.removeLayer(layer); if (layer.clusterShow) { layer.clusterShow(); } //Position will be fixed up immediately in _animationUnspiderfy if (layer.setZIndexOffset) { layer.setZIndexOffset(0); } this._map.removeLayer(layer._spiderLeg); delete layer._spiderLeg; } } }); Leaflet.markercluster-0.5.0/src/MarkerCluster.js000066400000000000000000000264001270301646500216700ustar00rootroot00000000000000L.MarkerCluster = L.Marker.extend({ initialize: function (group, zoom, a, b) { L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this }); this._group = group; this._zoom = zoom; this._markers = []; this._childClusters = []; this._childCount = 0; this._iconNeedsUpdate = true; this._boundsNeedUpdate = true; this._bounds = new L.LatLngBounds(); if (a) { this._addChild(a); } if (b) { this._addChild(b); } }, //Recursively retrieve all child markers of this cluster getAllChildMarkers: function (storageArray) { storageArray = storageArray || []; for (var i = this._childClusters.length - 1; i >= 0; i--) { this._childClusters[i].getAllChildMarkers(storageArray); } for (var j = this._markers.length - 1; j >= 0; j--) { storageArray.push(this._markers[j]); } return storageArray; }, //Returns the count of how many child markers we have getChildCount: function () { return this._childCount; }, //Zoom to the minimum of showing all of the child markers, or the extents of this cluster zoomToBounds: function () { var childClusters = this._childClusters.slice(), map = this._group._map, boundsZoom = map.getBoundsZoom(this._bounds), zoom = this._zoom + 1, mapZoom = map.getZoom(), i; //calculate how far we need to zoom down to see all of the markers while (childClusters.length > 0 && boundsZoom > zoom) { zoom++; var newClusters = []; for (i = 0; i < childClusters.length; i++) { newClusters = newClusters.concat(childClusters[i]._childClusters); } childClusters = newClusters; } if (boundsZoom > zoom) { this._group._map.setView(this._latlng, zoom); } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead this._group._map.setView(this._latlng, mapZoom + 1); } else { this._group._map.fitBounds(this._bounds); } }, getBounds: function () { var bounds = new L.LatLngBounds(); bounds.extend(this._bounds); return bounds; }, _updateIcon: function () { this._iconNeedsUpdate = true; if (this._icon) { this.setIcon(this); } }, //Cludge for Icon, we pretend to be an icon for performance createIcon: function () { if (this._iconNeedsUpdate) { this._iconObj = this._group.options.iconCreateFunction(this); this._iconNeedsUpdate = false; } return this._iconObj.createIcon(); }, createShadow: function () { return this._iconObj.createShadow(); }, _addChild: function (new1, isNotificationFromChild) { this._iconNeedsUpdate = true; this._boundsNeedUpdate = true; this._setClusterCenter(new1); if (new1 instanceof L.MarkerCluster) { if (!isNotificationFromChild) { this._childClusters.push(new1); new1.__parent = this; } this._childCount += new1._childCount; } else { if (!isNotificationFromChild) { this._markers.push(new1); } this._childCount++; } if (this.__parent) { this.__parent._addChild(new1, true); } }, /** * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position. * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet. * @private */ _setClusterCenter: function (child) { if (!this._cLatLng) { // when clustering, take position of the first point as the cluster center this._cLatLng = child._cLatLng || child._latlng; } }, /** * Assigns impossible bounding values so that the next extend entirely determines the new bounds. * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class. * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended. * @private */ _resetBounds: function () { var bounds = this._bounds; if (bounds._southWest) { bounds._southWest.lat = Infinity; bounds._southWest.lng = Infinity; } if (bounds._northEast) { bounds._northEast.lat = -Infinity; bounds._northEast.lng = -Infinity; } }, _recalculateBounds: function () { var markers = this._markers, childClusters = this._childClusters, latSum = 0, lngSum = 0, totalCount = this._childCount, i, child, childLatLng, childCount; // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel. if (totalCount === 0) { return; } // Reset rather than creating a new object, for performance. this._resetBounds(); // Child markers. for (i = 0; i < markers.length; i++) { childLatLng = markers[i]._latlng; this._bounds.extend(childLatLng); latSum += childLatLng.lat; lngSum += childLatLng.lng; } // Child clusters. for (i = 0; i < childClusters.length; i++) { child = childClusters[i]; // Re-compute child bounds and weighted position first if necessary. if (child._boundsNeedUpdate) { child._recalculateBounds(); } this._bounds.extend(child._bounds); childLatLng = child._wLatLng; childCount = child._childCount; latSum += childLatLng.lat * childCount; lngSum += childLatLng.lng * childCount; } this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount); // Reset dirty flag. this._boundsNeedUpdate = false; }, //Set our markers position as given and add it to the map _addToMap: function (startPos) { if (startPos) { this._backupLatlng = this._latlng; this.setLatLng(startPos); } this._group._featureGroup.addLayer(this); }, _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) { this._recursively(bounds, 0, maxZoom - 1, function (c) { var markers = c._markers, i, m; for (i = markers.length - 1; i >= 0; i--) { m = markers[i]; //Only do it if the icon is still on the map if (m._icon) { m._setPos(center); m.clusterHide(); } } }, function (c) { var childClusters = c._childClusters, j, cm; for (j = childClusters.length - 1; j >= 0; j--) { cm = childClusters[j]; if (cm._icon) { cm._setPos(center); cm.clusterHide(); } } } ); }, _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) { this._recursively(bounds, newZoomLevel, 0, function (c) { c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel); //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be. //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) { c.clusterShow(); c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds } else { c.clusterHide(); } c._addToMap(); } ); }, _recursivelyBecomeVisible: function (bounds, zoomLevel) { this._recursively(bounds, 0, zoomLevel, null, function (c) { c.clusterShow(); }); }, _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { this._recursively(bounds, -1, zoomLevel, function (c) { if (zoomLevel === c._zoom) { return; } //Add our child markers at startPos (so they can be animated out) for (var i = c._markers.length - 1; i >= 0; i--) { var nm = c._markers[i]; if (!bounds.contains(nm._latlng)) { continue; } if (startPos) { nm._backupLatlng = nm.getLatLng(); nm.setLatLng(startPos); if (nm.clusterHide) { nm.clusterHide(); } } c._group._featureGroup.addLayer(nm); } }, function (c) { c._addToMap(startPos); } ); }, _recursivelyRestoreChildPositions: function (zoomLevel) { //Fix positions of child markers for (var i = this._markers.length - 1; i >= 0; i--) { var nm = this._markers[i]; if (nm._backupLatlng) { nm.setLatLng(nm._backupLatlng); delete nm._backupLatlng; } } if (zoomLevel - 1 === this._zoom) { //Reposition child clusters for (var j = this._childClusters.length - 1; j >= 0; j--) { this._childClusters[j]._restorePosition(); } } else { for (var k = this._childClusters.length - 1; k >= 0; k--) { this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel); } } }, _restorePosition: function () { if (this._backupLatlng) { this.setLatLng(this._backupLatlng); delete this._backupLatlng; } }, //exceptBounds: If set, don't remove any markers/clusters in it _recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) { var m, i; this._recursively(previousBounds, -1, zoomLevel - 1, function (c) { //Remove markers at every level for (i = c._markers.length - 1; i >= 0; i--) { m = c._markers[i]; if (!exceptBounds || !exceptBounds.contains(m._latlng)) { c._group._featureGroup.removeLayer(m); if (m.clusterShow) { m.clusterShow(); } } } }, function (c) { //Remove child clusters at just the bottom level for (i = c._childClusters.length - 1; i >= 0; i--) { m = c._childClusters[i]; if (!exceptBounds || !exceptBounds.contains(m._latlng)) { c._group._featureGroup.removeLayer(m); if (m.clusterShow) { m.clusterShow(); } } } } ); }, //Run the given functions recursively to this and child clusters // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to // zoomLevelToStart: zoom level to start running functions (inclusive) // zoomLevelToStop: zoom level to stop running functions (inclusive) // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) { var childClusters = this._childClusters, zoom = this._zoom, i, c; if (zoomLevelToStart > zoom) { //Still going down to required depth, just recurse to child clusters for (i = childClusters.length - 1; i >= 0; i--) { c = childClusters[i]; if (boundsToApplyTo.intersects(c._bounds)) { c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); } } } else { //In required depth if (runAtEveryLevel) { runAtEveryLevel(this); } if (runAtBottomLevel && this._zoom === zoomLevelToStop) { runAtBottomLevel(this); } //TODO: This loop is almost the same as above if (zoomLevelToStop > zoom) { for (i = childClusters.length - 1; i >= 0; i--) { c = childClusters[i]; if (boundsToApplyTo.intersects(c._bounds)) { c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); } } } } }, //Returns true if we are the parent of only one cluster and that cluster is the same as us _isSingleParent: function () { //Don't need to check this._markers as the rest won't work if there are any return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount; } }); Leaflet.markercluster-0.5.0/src/MarkerClusterGroup.Refresh.js000066400000000000000000000076351270301646500243130ustar00rootroot00000000000000/** * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing * markers' icon options and refreshing their icon and their parent clusters * accordingly (case where their iconCreateFunction uses data of childMarkers * to make up the cluster icon). */ L.MarkerClusterGroup.include({ /** * Updates the icon of all clusters which are parents of the given marker(s). * In singleMarkerMode, also updates the given marker(s) icon. * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)| * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent * clusters need to be updated. If not provided, retrieves all child markers of this. * @returns {L.MarkerClusterGroup} */ refreshClusters: function (layers) { if (!layers) { layers = this._topClusterLevel.getAllChildMarkers(); } else if (layers instanceof L.MarkerClusterGroup) { layers = layers._topClusterLevel.getAllChildMarkers(); } else if (layers instanceof L.LayerGroup) { layers = layers._layers; } else if (layers instanceof L.MarkerCluster) { layers = layers.getAllChildMarkers(); } else if (layers instanceof L.Marker) { layers = [layers]; } // else: must be an Array(L.Marker)|Map(L.Marker) this._flagParentsIconsNeedUpdate(layers); this._refreshClustersIcons(); // In case of singleMarkerMode, also re-draw the markers. if (this.options.singleMarkerMode) { this._refreshSingleMarkerModeMarkers(layers); } return this; }, /** * Simply flags all parent clusters of the given markers as having a "dirty" icon. * @param layers Array(L.Marker)|Map(L.Marker) list of markers. * @private */ _flagParentsIconsNeedUpdate: function (layers) { var id, parent; // Assumes layers is an Array or an Object whose prototype is non-enumerable. for (id in layers) { // Flag parent clusters' icon as "dirty", all the way up. // Dumb process that flags multiple times upper parents, but still // much more efficient than trying to be smart and make short lists, // at least in the case of a hierarchy following a power law: // http://jsperf.com/flag-nodes-in-power-hierarchy/2 parent = layers[id].__parent; while (parent) { parent._iconNeedsUpdate = true; parent = parent.__parent; } } }, /** * Refreshes the icon of all "dirty" visible clusters. * Non-visible "dirty" clusters will be updated when they are added to the map. * @private */ _refreshClustersIcons: function () { this._featureGroup.eachLayer(function (c) { if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { c._updateIcon(); } }); }, /** * Re-draws the icon of the supplied markers. * To be used in singleMarkerMode only. * @param layers Array(L.Marker)|Map(L.Marker) list of markers. * @private */ _refreshSingleMarkerModeMarkers: function (layers) { var id, layer; for (id in layers) { layer = layers[id]; // Make sure we do not override markers that do not belong to THIS group. if (this.hasLayer(layer)) { // Need to re-create the icon first, then re-draw the marker. layer.setIcon(this._overrideMarkerIcon(layer)); } } } }); L.Marker.include({ /** * Updates the given options in the marker's icon and refreshes the marker. * @param options map object of icon options. * @param directlyRefreshClusters boolean (optional) true to trigger * MCG.refreshClustersOf() right away with this single marker. * @returns {L.Marker} */ refreshIconOptions: function (options, directlyRefreshClusters) { var icon = this.options.icon; L.setOptions(icon, options); this.setIcon(icon); // Shortcut to refresh the associated MCG clusters right away. // To be used when refreshing a single marker. // Otherwise, better use MCG.refreshClusters() once at the end with // the list of modified markers. if (directlyRefreshClusters && this.__parent) { this.__parent._group.refreshClusters(this); } return this; } }); Leaflet.markercluster-0.5.0/src/MarkerClusterGroup.js000066400000000000000000001034741270301646500227140ustar00rootroot00000000000000/* * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within */ L.MarkerClusterGroup = L.FeatureGroup.extend({ options: { maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center iconCreateFunction: null, spiderfyOnMaxZoom: true, showCoverageOnHover: true, zoomToBoundsOnClick: true, singleMarkerMode: false, disableClusteringAtZoom: null, // Setting this to false prevents the removal of any clusters outside of the viewpoint, which // is the default behaviour for performance reasons. removeOutsideVisibleBounds: true, // Set to false to disable all animations (zoom and spiderfy). // If false, option animateAddingMarkers below has no effect. // If L.DomUtil.TRANSITION is falsy, this option has no effect. animate: true, //Whether to animate adding markers after adding the MarkerClusterGroup to the map // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. animateAddingMarkers: false, //Increase to increase the distance away that spiderfied markers appear from the center spiderfyDistanceMultiplier: 1, // Make it possible to specify a polyline options on a spider leg spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 }, // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts chunkedLoading: false, chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) //Options to pass to the L.Polygon constructor polygonOptions: {} }, initialize: function (options) { L.Util.setOptions(this, options); if (!this.options.iconCreateFunction) { this.options.iconCreateFunction = this._defaultIconCreateFunction; } this._featureGroup = L.featureGroup(); this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); this._nonPointGroup = L.featureGroup(); this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); this._inZoomAnimation = 0; this._needsClustering = []; this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move this._currentShownBounds = null; this._queue = []; // Hook the appropriate animation methods. var animate = L.DomUtil.TRANSITION && this.options.animate; L.extend(this, animate ? this._withAnimation : this._noAnimation); // Remember which MarkerCluster class to instantiate (animated or not). this._markerCluster = animate ? L.MarkerCluster : L.MarkerClusterNonAnimated; }, addLayer: function (layer) { if (layer instanceof L.LayerGroup) { var array = []; for (var i in layer._layers) { array.push(layer._layers[i]); } return this.addLayers(array); } //Don't cluster non point data if (!layer.getLatLng) { this._nonPointGroup.addLayer(layer); return this; } if (!this._map) { this._needsClustering.push(layer); return this; } if (this.hasLayer(layer)) { return this; } //If we have already clustered we'll need to add this one to a cluster if (this._unspiderfy) { this._unspiderfy(); } this._addLayer(layer, this._maxZoom); // Refresh bounds and weighted positions. this._topClusterLevel._recalculateBounds(); //Work out what is visible var visibleLayer = layer, currentZoom = this._map.getZoom(); if (layer.__parent) { while (visibleLayer.__parent._zoom >= currentZoom) { visibleLayer = visibleLayer.__parent; } } if (this._currentShownBounds.contains(visibleLayer.getLatLng())) { if (this.options.animateAddingMarkers) { this._animationAddLayer(layer, visibleLayer); } else { this._animationAddLayerNonAnimated(layer, visibleLayer); } } return this; }, removeLayer: function (layer) { if (layer instanceof L.LayerGroup) { var array = []; for (var i in layer._layers) { array.push(layer._layers[i]); } return this.removeLayers(array); } //Non point layers if (!layer.getLatLng) { this._nonPointGroup.removeLayer(layer); return this; } if (!this._map) { if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { this._needsRemoving.push(layer); } return this; } if (!layer.__parent) { return this; } if (this._unspiderfy) { this._unspiderfy(); this._unspiderfyLayer(layer); } //Remove the marker from clusters this._removeLayer(layer, true); // Refresh bounds and weighted positions. this._topClusterLevel._recalculateBounds(); if (this._featureGroup.hasLayer(layer)) { this._featureGroup.removeLayer(layer); if (layer.clusterShow) { layer.clusterShow(); } } return this; }, //Takes an array of markers and adds them in bulk addLayers: function (layersArray) { var fg = this._featureGroup, npg = this._nonPointGroup, chunked = this.options.chunkedLoading, chunkInterval = this.options.chunkInterval, chunkProgress = this.options.chunkProgress, newMarkers, i, l, m; if (this._map) { var offset = 0, started = (new Date()).getTime(); var process = L.bind(function () { var start = (new Date()).getTime(); for (; offset < layersArray.length; offset++) { if (chunked && offset % 200 === 0) { // every couple hundred markers, instrument the time elapsed since processing started: var elapsed = (new Date()).getTime() - start; if (elapsed > chunkInterval) { break; // been working too hard, time to take a break :-) } } m = layersArray[offset]; //Not point data, can't be clustered if (!m.getLatLng) { npg.addLayer(m); continue; } if (this.hasLayer(m)) { continue; } this._addLayer(m, this._maxZoom); //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will if (m.__parent) { if (m.__parent.getChildCount() === 2) { var markers = m.__parent.getAllChildMarkers(), otherMarker = markers[0] === m ? markers[1] : markers[0]; fg.removeLayer(otherMarker); } } } if (chunkProgress) { // report progress and time elapsed: chunkProgress(offset, layersArray.length, (new Date()).getTime() - started); } // Completed processing all markers. if (offset === layersArray.length) { // Refresh bounds and weighted positions. this._topClusterLevel._recalculateBounds(); //Update the icons of all those visible clusters that were affected this._featureGroup.eachLayer(function (c) { if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { c._updateIcon(); } }); this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); } else { setTimeout(process, this.options.chunkDelay); } }, this); process(); } else { newMarkers = []; for (i = 0, l = layersArray.length; i < l; i++) { m = layersArray[i]; //Not point data, can't be clustered if (!m.getLatLng) { npg.addLayer(m); continue; } if (this.hasLayer(m)) { continue; } newMarkers.push(m); } this._needsClustering = this._needsClustering.concat(newMarkers); } return this; }, //Takes an array of markers and removes them in bulk removeLayers: function (layersArray) { var i, l, m, fg = this._featureGroup, npg = this._nonPointGroup; if (!this._map) { for (i = 0, l = layersArray.length; i < l; i++) { m = layersArray[i]; this._arraySplice(this._needsClustering, m); npg.removeLayer(m); if (this.hasLayer(m)) { this._needsRemoving.push(m); } } return this; } if (this._unspiderfy) { this._unspiderfy(); for (i = 0, l = layersArray.length; i < l; i++) { m = layersArray[i]; this._unspiderfyLayer(m); } } for (i = 0, l = layersArray.length; i < l; i++) { m = layersArray[i]; if (!m.__parent) { npg.removeLayer(m); continue; } this._removeLayer(m, true, true); if (fg.hasLayer(m)) { fg.removeLayer(m); if (m.clusterShow) { m.clusterShow(); } } } // Refresh bounds and weighted positions. this._topClusterLevel._recalculateBounds(); //Fix up the clusters and markers on the map this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); fg.eachLayer(function (c) { if (c instanceof L.MarkerCluster) { c._updateIcon(); } }); return this; }, //Removes all layers from the MarkerClusterGroup clearLayers: function () { //Need our own special implementation as the LayerGroup one doesn't work for us //If we aren't on the map (yet), blow away the markers we know of if (!this._map) { this._needsClustering = []; delete this._gridClusters; delete this._gridUnclustered; } if (this._noanimationUnspiderfy) { this._noanimationUnspiderfy(); } //Remove all the visible layers this._featureGroup.clearLayers(); this._nonPointGroup.clearLayers(); this.eachLayer(function (marker) { delete marker.__parent; }); if (this._map) { //Reset _topClusterLevel and the DistanceGrids this._generateInitialClusters(); } return this; }, //Override FeatureGroup.getBounds as it doesn't work getBounds: function () { var bounds = new L.LatLngBounds(); if (this._topClusterLevel) { bounds.extend(this._topClusterLevel._bounds); } for (var i = this._needsClustering.length - 1; i >= 0; i--) { bounds.extend(this._needsClustering[i].getLatLng()); } bounds.extend(this._nonPointGroup.getBounds()); return bounds; }, //Overrides LayerGroup.eachLayer eachLayer: function (method, context) { var markers = this._needsClustering.slice(), i; if (this._topClusterLevel) { this._topClusterLevel.getAllChildMarkers(markers); } for (i = markers.length - 1; i >= 0; i--) { method.call(context, markers[i]); } this._nonPointGroup.eachLayer(method, context); }, //Overrides LayerGroup.getLayers getLayers: function () { var layers = []; this.eachLayer(function (l) { layers.push(l); }); return layers; }, //Overrides LayerGroup.getLayer, WARNING: Really bad performance getLayer: function (id) { var result = null; id = parseInt(id, 10); this.eachLayer(function (l) { if (L.stamp(l) === id) { result = l; } }); return result; }, //Returns true if the given layer is in this MarkerClusterGroup hasLayer: function (layer) { if (!layer) { return false; } var i, anArray = this._needsClustering; for (i = anArray.length - 1; i >= 0; i--) { if (anArray[i] === layer) { return true; } } anArray = this._needsRemoving; for (i = anArray.length - 1; i >= 0; i--) { if (anArray[i] === layer) { return false; } } return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer); }, //Zoom down to show the given layer (spiderfying if necessary) then calls the callback zoomToShowLayer: function (layer, callback) { if (typeof callback !== 'function') { callback = function () {}; } var showMarker = function () { if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) { this._map.off('moveend', showMarker, this); this.off('animationend', showMarker, this); if (layer._icon) { callback(); } else if (layer.__parent._icon) { this.once('spiderfied', callback, this); layer.__parent.spiderfy(); } } }; if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) { //Layer is visible ond on screen, immediate return callback(); } else if (layer.__parent._zoom < this._map.getZoom()) { //Layer should be visible at this zoom level. It must not be on screen so just pan over to it this._map.on('moveend', showMarker, this); this._map.panTo(layer.getLatLng()); } else { var moveStart = function () { this._map.off('movestart', moveStart, this); moveStart = null; }; this._map.on('movestart', moveStart, this); this._map.on('moveend', showMarker, this); this.on('animationend', showMarker, this); layer.__parent.zoomToBounds(); if (moveStart) { //Never started moving, must already be there, probably need clustering however showMarker.call(this); } } }, //Overrides FeatureGroup.onAdd onAdd: function (map) { this._map = map; var i, l, layer; if (!isFinite(this._map.getMaxZoom())) { throw "Map has no maxZoom specified"; } this._featureGroup.onAdd(map); this._nonPointGroup.onAdd(map); if (!this._gridClusters) { this._generateInitialClusters(); } this._maxLat = map.options.crs.projection.MAX_LATITUDE; for (i = 0, l = this._needsRemoving.length; i < l; i++) { layer = this._needsRemoving[i]; this._removeLayer(layer, true); } this._needsRemoving = []; //Remember the current zoom level and bounds this._zoom = this._map.getZoom(); this._currentShownBounds = this._getExpandedVisibleBounds(); this._map.on('zoomend', this._zoomEnd, this); this._map.on('moveend', this._moveEnd, this); if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely this._spiderfierOnAdd(); } this._bindEvents(); //Actually add our markers to the map: l = this._needsClustering; this._needsClustering = []; this.addLayers(l); }, //Overrides FeatureGroup.onRemove onRemove: function (map) { map.off('zoomend', this._zoomEnd, this); map.off('moveend', this._moveEnd, this); this._unbindEvents(); //In case we are in a cluster animation this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely this._spiderfierOnRemove(); } delete this._maxLat; //Clean up all the layers we added to the map this._hideCoverage(); this._featureGroup.onRemove(map); this._nonPointGroup.onRemove(map); this._featureGroup.clearLayers(); this._map = null; }, getVisibleParent: function (marker) { var vMarker = marker; while (vMarker && !vMarker._icon) { vMarker = vMarker.__parent; } return vMarker || null; }, //Remove the given object from the given array _arraySplice: function (anArray, obj) { for (var i = anArray.length - 1; i >= 0; i--) { if (anArray[i] === obj) { anArray.splice(i, 1); return true; } } }, /** * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom. * @param marker to be removed from _gridUnclustered. * @param z integer bottom start zoom level (included) * @private */ _removeFromGridUnclustered: function (marker, z) { var map = this._map, gridUnclustered = this._gridUnclustered; for (; z >= 0; z--) { if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { break; } } }, //Internal function for removing a marker from everything. //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions) _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) { var gridClusters = this._gridClusters, gridUnclustered = this._gridUnclustered, fg = this._featureGroup, map = this._map; //Remove the marker from distance clusters it might be in if (removeFromDistanceGrid) { this._removeFromGridUnclustered(marker, this._maxZoom); } //Work our way up the clusters removing them as we go if required var cluster = marker.__parent, markers = cluster._markers, otherMarker; //Remove the marker from the immediate parents marker list this._arraySplice(markers, marker); while (cluster) { cluster._childCount--; cluster._boundsNeedUpdate = true; if (cluster._zoom < 0) { //Top level, do nothing break; } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required //We need to push the other marker up to the parent otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0]; //Update distance grid gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom)); gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom)); //Move otherMarker up to parent this._arraySplice(cluster.__parent._childClusters, cluster); cluster.__parent._markers.push(otherMarker); otherMarker.__parent = cluster.__parent; if (cluster._icon) { //Cluster is currently on the map, need to put the marker on the map instead fg.removeLayer(cluster); if (!dontUpdateMap) { fg.addLayer(otherMarker); } } } else { if (!dontUpdateMap || !cluster._icon) { cluster._updateIcon(); } } cluster = cluster.__parent; } delete marker.__parent; }, _isOrIsParent: function (el, oel) { while (oel) { if (el === oel) { return true; } oel = oel.parentNode; } return false; }, _propagateEvent: function (e) { if (e.layer instanceof L.MarkerCluster) { //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget) if (e.originalEvent && this._isOrIsParent(e.layer._icon, e.originalEvent.relatedTarget)) { return; } e.type = 'cluster' + e.type; } this.fire(e.type, e); }, //Default functionality _defaultIconCreateFunction: function (cluster) { var childCount = cluster.getChildCount(); var c = ' marker-cluster-'; if (childCount < 10) { c += 'small'; } else if (childCount < 100) { c += 'medium'; } else { c += 'large'; } return new L.DivIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); }, _bindEvents: function () { var map = this._map, spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, showCoverageOnHover = this.options.showCoverageOnHover, zoomToBoundsOnClick = this.options.zoomToBoundsOnClick; //Zoom on cluster click or spiderfy if we are at the lowest level if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { this.on('clusterclick', this._zoomOrSpiderfy, this); } //Show convex hull (boundary) polygon on mouse over if (showCoverageOnHover) { this.on('clustermouseover', this._showCoverage, this); this.on('clustermouseout', this._hideCoverage, this); map.on('zoomend', this._hideCoverage, this); } }, _zoomOrSpiderfy: function (e) { var cluster = e.layer, bottomCluster = cluster; while (bottomCluster._childClusters.length === 1) { bottomCluster = bottomCluster._childClusters[0]; } if (bottomCluster._zoom === this._maxZoom && bottomCluster._childCount === cluster._childCount) { // All child markers are contained in a single cluster from this._maxZoom to this cluster. if (this.options.spiderfyOnMaxZoom) { cluster.spiderfy(); } } else if (this.options.zoomToBoundsOnClick) { cluster.zoomToBounds(); } // Focus the map again for keyboard users. if (e.originalEvent && e.originalEvent.keyCode === 13) { this._map._container.focus(); } }, _showCoverage: function (e) { var map = this._map; if (this._inZoomAnimation) { return; } if (this._shownPolygon) { map.removeLayer(this._shownPolygon); } if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) { this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions); map.addLayer(this._shownPolygon); } }, _hideCoverage: function () { if (this._shownPolygon) { this._map.removeLayer(this._shownPolygon); this._shownPolygon = null; } }, _unbindEvents: function () { var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, showCoverageOnHover = this.options.showCoverageOnHover, zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, map = this._map; if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { this.off('clusterclick', this._zoomOrSpiderfy, this); } if (showCoverageOnHover) { this.off('clustermouseover', this._showCoverage, this); this.off('clustermouseout', this._hideCoverage, this); map.off('zoomend', this._hideCoverage, this); } }, _zoomEnd: function () { if (!this._map) { //May have been removed from the map by a zoomEnd handler return; } this._mergeSplitClusters(); this._zoom = this._map._zoom; this._currentShownBounds = this._getExpandedVisibleBounds(); }, _moveEnd: function () { if (this._inZoomAnimation) { return; } var newBounds = this._getExpandedVisibleBounds(); this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds); this._topClusterLevel._recursivelyAddChildrenToMap(null, this._map._zoom, newBounds); this._currentShownBounds = newBounds; return; }, _generateInitialClusters: function () { var maxZoom = this._map.getMaxZoom(), radius = this.options.maxClusterRadius, radiusFn = radius; //If we just set maxClusterRadius to a single number, we need to create //a simple function to return that number. Otherwise, we just have to //use the function we've passed in. if (typeof radius !== "function") { radiusFn = function () { return radius; }; } if (this.options.disableClusteringAtZoom) { maxZoom = this.options.disableClusteringAtZoom - 1; } this._maxZoom = maxZoom; this._gridClusters = {}; this._gridUnclustered = {}; //Set up DistanceGrids for each zoom for (var zoom = maxZoom; zoom >= 0; zoom--) { this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom)); this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom)); } // Instantiate the appropriate L.MarkerCluster class (animated or not). this._topClusterLevel = new this._markerCluster(this, -1); }, //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom) _addLayer: function (layer, zoom) { var gridClusters = this._gridClusters, gridUnclustered = this._gridUnclustered, markerPoint, z; if (this.options.singleMarkerMode) { this._overrideMarkerIcon(layer); } //Find the lowest zoom level to slot this one in for (; zoom >= 0; zoom--) { markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position //Try find a cluster close by var closest = gridClusters[zoom].getNearObject(markerPoint); if (closest) { closest._addChild(layer); layer.__parent = closest; return; } //Try find a marker close by to form a new cluster with closest = gridUnclustered[zoom].getNearObject(markerPoint); if (closest) { var parent = closest.__parent; if (parent) { this._removeLayer(closest, false); } //Create new cluster with these 2 in it var newCluster = new this._markerCluster(this, zoom, closest, layer); gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom)); closest.__parent = newCluster; layer.__parent = newCluster; //First create any new intermediate parent clusters that don't exist var lastParent = newCluster; for (z = zoom - 1; z > parent._zoom; z--) { lastParent = new this._markerCluster(this, z, lastParent); gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z)); } parent._addChild(lastParent); //Remove closest from this zoom level and any above that it is in, replace with newCluster this._removeFromGridUnclustered(closest, zoom); return; } //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards gridUnclustered[zoom].addObject(layer, markerPoint); } //Didn't get in anything, add us to the top this._topClusterLevel._addChild(layer); layer.__parent = this._topClusterLevel; return; }, //Enqueue code to fire after the marker expand/contract has happened _enqueue: function (fn) { this._queue.push(fn); if (!this._queueTimeout) { this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300); } }, _processQueue: function () { for (var i = 0; i < this._queue.length; i++) { this._queue[i].call(this); } this._queue.length = 0; clearTimeout(this._queueTimeout); this._queueTimeout = null; }, //Merge and split any existing clusters that are too big or small _mergeSplitClusters: function () { //Incase we are starting to split before the animation finished this._processQueue(); if (this._zoom < this._map._zoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split this._animationStart(); //Remove clusters now off screen this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds()); this._animationZoomIn(this._zoom, this._map._zoom); } else if (this._zoom > this._map._zoom) { //Zoom out, merge this._animationStart(); this._animationZoomOut(this._zoom, this._map._zoom); } else { this._moveEnd(); } }, //Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan) _getExpandedVisibleBounds: function () { if (!this.options.removeOutsideVisibleBounds) { return this._mapBoundsInfinite; } else if (L.Browser.mobile) { return this._checkBoundsMaxLat(this._map.getBounds()); } return this._checkBoundsMaxLat(this._map.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor. }, /** * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas). * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit, * making the user think that MCG "eats" them and never displays them again. * @param bounds L.LatLngBounds * @returns {L.LatLngBounds} * @private */ _checkBoundsMaxLat: function (bounds) { var maxLat = this._maxLat; if (maxLat !== undefined) { if (bounds.getNorth() >= maxLat) { bounds._northEast.lat = Infinity; } if (bounds.getSouth() <= -maxLat) { bounds._southWest.lat = -Infinity; } } return bounds; }, //Shared animation code _animationAddLayerNonAnimated: function (layer, newCluster) { if (newCluster === layer) { this._featureGroup.addLayer(layer); } else if (newCluster._childCount === 2) { newCluster._addToMap(); var markers = newCluster.getAllChildMarkers(); this._featureGroup.removeLayer(markers[0]); this._featureGroup.removeLayer(markers[1]); } else { newCluster._updateIcon(); } }, /** * Implements the singleMarkerMode option. * @param layer Marker to re-style using the Clusters iconCreateFunction. * @returns {L.Icon} The newly created icon. * @private */ _overrideMarkerIcon: function (layer) { var icon = layer.options.icon = this.options.iconCreateFunction({ getChildCount: function () { return 1; }, getAllChildMarkers: function () { return [layer]; } }); return icon; } }); // Constant bounds used in case option "removeOutsideVisibleBounds" is set to false. L.MarkerClusterGroup.include({ _mapBoundsInfinite: new L.LatLngBounds(new L.LatLng(-Infinity, -Infinity), new L.LatLng(Infinity, Infinity)) }); L.MarkerClusterGroup.include({ _noAnimation: { //Non Animated versions of everything _animationStart: function () { //Do nothing... }, _animationZoomIn: function (previousZoomLevel, newZoomLevel) { this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel); this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); //We didn't actually animate, but we use this event to mean "clustering animations have finished" this.fire('animationend'); }, _animationZoomOut: function (previousZoomLevel, newZoomLevel) { this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel); this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); //We didn't actually animate, but we use this event to mean "clustering animations have finished" this.fire('animationend'); }, _animationAddLayer: function (layer, newCluster) { this._animationAddLayerNonAnimated(layer, newCluster); } }, _withAnimation: { //Animated versions here _animationStart: function () { this._map._mapPane.className += ' leaflet-cluster-anim'; this._inZoomAnimation++; }, _animationZoomIn: function (previousZoomLevel, newZoomLevel) { var bounds = this._getExpandedVisibleBounds(), fg = this._featureGroup, i; //Add all children of current clusters to map and remove those clusters from map this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) { var startPos = c._latlng, markers = c._markers, m; if (!bounds.contains(startPos)) { startPos = null; } if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us fg.removeLayer(c); c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds); } else { //Fade out old cluster c.clusterHide(); c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds); } //Remove all markers that aren't visible any more //TODO: Do we actually need to do this on the higher levels too? for (i = markers.length - 1; i >= 0; i--) { m = markers[i]; if (!bounds.contains(m._latlng)) { fg.removeLayer(m); } } }); this._forceLayout(); //Update opacities this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel); //TODO Maybe? Update markers in _recursivelyBecomeVisible fg.eachLayer(function (n) { if (!(n instanceof L.MarkerCluster) && n._icon) { n.clusterShow(); } }); //update the positions of the just added clusters/markers this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) { c._recursivelyRestoreChildPositions(newZoomLevel); }); //Remove the old clusters and close the zoom animation this._enqueue(function () { //update the positions of the just added clusters/markers this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) { fg.removeLayer(c); c.clusterShow(); }); this._animationEnd(); }); }, _animationZoomOut: function (previousZoomLevel, newZoomLevel) { this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel); //Need to add markers for those that weren't on the map before but are now this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); //Remove markers that were on the map before but won't be now this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds()); }, _animationAddLayer: function (layer, newCluster) { var me = this, fg = this._featureGroup; fg.addLayer(layer); if (newCluster !== layer) { if (newCluster._childCount > 2) { //Was already a cluster newCluster._updateIcon(); this._forceLayout(); this._animationStart(); layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng())); layer.clusterHide(); this._enqueue(function () { fg.removeLayer(layer); layer.clusterShow(); me._animationEnd(); }); } else { //Just became a cluster this._forceLayout(); me._animationStart(); me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom()); } } } }, // Private methods for animated versions. _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { var bounds = this._getExpandedVisibleBounds(); //Animate all of the markers in the clusters to move to their cluster center point cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel); var me = this; //Update the opacity (If we immediately set it they won't animate) this._forceLayout(); cluster._recursivelyBecomeVisible(bounds, newZoomLevel); //TODO: Maybe use the transition timing stuff to make this more reliable //When the animations are done, tidy up this._enqueue(function () { //This cluster stopped being a cluster before the timeout fired if (cluster._childCount === 1) { var m = cluster._markers[0]; //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it m.setLatLng(m.getLatLng()); if (m.clusterShow) { m.clusterShow(); } } else { cluster._recursively(bounds, newZoomLevel, 0, function (c) { c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1); }); } me._animationEnd(); }); }, _animationEnd: function () { if (this._map) { this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); } this._inZoomAnimation--; this.fire('animationend'); }, //Force a browser layout of stuff in the map // Should apply the current opacity and location to all elements so we can update them again for an animation _forceLayout: function () { //In my testing this works, infact offsetWidth of any element seems to work. //Could loop all this._layers and do this for each _icon if it stops working L.Util.falseFn(document.body.offsetWidth); } }); L.markerClusterGroup = function (options) { return new L.MarkerClusterGroup(options); }; Leaflet.markercluster-0.5.0/src/MarkerOpacity.js000066400000000000000000000010761270301646500216610ustar00rootroot00000000000000 /* * Extends L.Marker to include two extra methods: clusterHide and clusterShow. * * They work as setOpacity(0) and setOpacity(1) respectively, but * they will remember the marker's opacity when hiding and showing it again. * */ L.Marker.include({ clusterHide: function () { this.options.opacityWhenUnclustered = this.options.opacity || 1; return this.setOpacity(0); }, clusterShow: function () { var ret = this.setOpacity(this.options.opacity || this.options.opacityWhenUnclustered); delete this.options.opacityWhenUnclustered; return ret; } }); Leaflet.markercluster-0.5.0/src/copyright.js000066400000000000000000000003421270301646500211120ustar00rootroot00000000000000/* Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. https://github.com/Leaflet/Leaflet.markercluster (c) 2012-2013, Dave Leaver, smartrak */