From 81256ab8cd18b3a586376253de13dffb25af12f6 Mon Sep 17 00:00:00 2001 From: thomas-topway-it Date: Fri, 24 Jan 2025 13:54:32 +0400 Subject: [PATCH 1/4] Update libraries --- formats/filtered/package.json | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/formats/filtered/package.json b/formats/filtered/package.json index 052ac757a..22ffff7e0 100644 --- a/formats/filtered/package.json +++ b/formats/filtered/package.json @@ -1,6 +1,6 @@ { "name": "filtered", - "version": "2.0.0", + "version": "2.1.0", "description": "Displays SMW query results in switchable views and offers client-side (JavaScript based) filtering", "main": "ext.srf.filtered.js", "scripts": { @@ -9,15 +9,16 @@ }, "author": "Stephan Gambke", "license": "GPL-2.0-or-later", - "dependencies": {}, + "dependencies": { + }, "devDependencies": { "@types/fullcalendar": "2.7.42", "@types/ion.rangeslider": "^2.0.29", "@types/jquery": "^2.0.51", "@types/jqueryui": "^1.12.5", - "@types/leaflet": "~1.0.60", - "@types/leaflet-providers": "^1.1.0", - "@types/leaflet.markercluster": "1.0.0", + "@types/leaflet": "^1.9.16", + "@types/leaflet-providers": "^1.2.4", + "@types/leaflet.markercluster": "^1.5.5", "@types/qunit": "^1.16.31", "@types/select2": "^4.0.47", "browserify": "^16.2.3", @@ -28,14 +29,14 @@ "gulp-sourcemaps": "^2.4.1", "gulp-uglify": "^2.1.2", "ion-rangeslider": "^2.2.0", - "leaflet": "^1.3.4", - "leaflet-providers": "^1.4.0", - "leaflet.markercluster": "~1.2", + "leaflet": "^1.9.4", + "leaflet-providers": "^2.0.0", + "leaflet.markercluster": "^1.5.3", "nouislider": "^10.1.0", "select2": "4.0.3", "tsify": "^3.0.1", - "typescript": "^3.1.3", "vinyl-buffer": "^1.0.0", - "vinyl-source-stream": "^2.0.0" + "vinyl-source-stream": "^2.0.0", + "typescript": "^5.7.3" } } From 2f154c32fa6f9e07f95768fb534df0afb261f15e Mon Sep 17 00:00:00 2001 From: thomas-topway-it Date: Fri, 24 Jan 2025 13:55:37 +0400 Subject: [PATCH 2/4] update target --- formats/filtered/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/formats/filtered/tsconfig.json b/formats/filtered/tsconfig.json index 6b699b86c..c5036ee89 100644 --- a/formats/filtered/tsconfig.json +++ b/formats/filtered/tsconfig.json @@ -4,9 +4,9 @@ ], "compilerOptions": { "noImplicitAny": true, - "target": "es3" + "target": "es5" }, "exclude": [ "node_modules" ] -} \ No newline at end of file +} From 551e8cc52be831f198aa5e50d93a5b57e4906bd6 Mon Sep 17 00:00:00 2001 From: thomas-topway-it Date: Fri, 24 Jan 2025 13:56:34 +0400 Subject: [PATCH 3/4] add imports --- formats/filtered/resources/ts/Filtered/View/MapView.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/formats/filtered/resources/ts/Filtered/View/MapView.ts b/formats/filtered/resources/ts/Filtered/View/MapView.ts index d659058d7..d1b584e64 100644 --- a/formats/filtered/resources/ts/Filtered/View/MapView.ts +++ b/formats/filtered/resources/ts/Filtered/View/MapView.ts @@ -1,4 +1,7 @@ /// +import * as L from 'leaflet'; +import 'leaflet.markercluster'; +import 'leaflet-providers'; import { View } from "./View"; import { Options } from "../../types" From 949023980f65d9d639842a11443ea9a457be51a3 Mon Sep 17 00:00:00 2001 From: thomas-topway-it Date: Fri, 24 Jan 2025 13:58:50 +0400 Subject: [PATCH 4/4] update builds --- .../filtered/resources/js/ext.srf.filtered.js | 18623 ++++++++- .../resources/js/ext.srf.filtered.js.map | 2 +- .../resources/js/ext.srf.filtered.leaflet.js | 33551 ++++++++-------- 3 files changed, 35819 insertions(+), 16357 deletions(-) diff --git a/formats/filtered/resources/js/ext.srf.filtered.js b/formats/filtered/resources/js/ext.srf.filtered.js index 39fea7a01..7f7fa52f6 100644 --- a/formats/filtered/resources/js/ext.srf.filtered.js +++ b/formats/filtered/resources/js/ext.srf.filtered.js @@ -1,7 +1,18422 @@ (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;iOpenStreetMap contributors' + }, + variants: { + Mapnik: {}, + DE: { + url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png', + options: { + maxZoom: 18 + } + }, + CH: { + url: 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png', + options: { + maxZoom: 18, + bounds: [[45, 5], [48, 11]] + } + }, + France: { + url: 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', + options: { + maxZoom: 20, + attribution: '© OpenStreetMap France | {attribution.OpenStreetMap}' + } + }, + HOT: { + url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + options: { + attribution: + '{attribution.OpenStreetMap}, ' + + 'Tiles style by Humanitarian OpenStreetMap Team ' + + 'hosted by OpenStreetMap France' + } + }, + BZH: { + url: 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png', + options: { + attribution: '{attribution.OpenStreetMap}, Tiles courtesy of Breton OpenStreetMap Team', + bounds: [[46.2, -5.5], [50, 0.7]] + } + } + } + }, + MapTilesAPI: { + url: 'https://maptiles.p.rapidapi.com/{variant}/{z}/{x}/{y}.png?rapidapi-key={apikey}', + options: { + attribution: + '© MapTiles API, {attribution.OpenStreetMap}', + variant: 'en/map/v1', + // Get your own MapTiles API access token here : https://www.maptilesapi.com/ + // NB : this is a demonstration key that comes with no guarantee and not to be used in production + apikey: '', + maxZoom: 19 + }, + variants: { + OSMEnglish: { + options: { + variant: 'en/map/v1' + } + }, + OSMFrancais: { + options: { + variant: 'fr/map/v1' + } + }, + OSMEspagnol: { + options: { + variant: 'es/map/v1' + } + } + } + }, + OpenSeaMap: { + url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', + options: { + attribution: 'Map data: © OpenSeaMap contributors' + } + }, + OPNVKarte: { + url: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', + options: { + maxZoom: 18, + attribution: 'Map memomaps.de CC-BY-SA, map data {attribution.OpenStreetMap}' + } + }, + OpenTopoMap: { + url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', + options: { + maxZoom: 17, + attribution: 'Map data: {attribution.OpenStreetMap}, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' + } + }, + OpenRailwayMap: { + url: 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenRailwayMap (CC-BY-SA)' + } + }, + OpenFireMap: { + url: 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenFireMap (CC-BY-SA)' + } + }, + SafeCast: { + url: 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png', + options: { + maxZoom: 16, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © SafeCast (CC-BY-SA)' + } + }, + Stadia: { + url: 'https://tiles.stadiamaps.com/tiles/{variant}/{z}/{x}/{y}{r}.{ext}', + options: { + minZoom: 0, + maxZoom: 20, + attribution: + '© Stadia Maps ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'alidade_smooth', + ext: 'png' + }, + variants: { + AlidadeSmooth: 'alidade_smooth', + AlidadeSmoothDark: 'alidade_smooth_dark', + OSMBright: 'osm_bright', + Outdoors: 'outdoors', + StamenToner: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner' + } + }, + StamenTonerBackground: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_background' + } + }, + StamenTonerLines: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_lines' + } + }, + StamenTonerLabels: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_labels' + } + }, + StamenTonerLite: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_lite' + } + }, + StamenWatercolor: { + url: 'https://tiles.stadiamaps.com/tiles/{variant}/{z}/{x}/{y}.{ext}', + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_watercolor', + ext: 'jpg', + minZoom: 1, + maxZoom: 16 + } + }, + StamenTerrain: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain', + minZoom: 0, + maxZoom: 18 + } + }, + StamenTerrainBackground: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain_background', + minZoom: 0, + maxZoom: 18 + } + }, + StamenTerrainLabels: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain_labels', + minZoom: 0, + maxZoom: 18 + } + }, + StamenTerrainLines: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain_lines', + minZoom: 0, + maxZoom: 18 + } + } + } + }, + Thunderforest: { + url: 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}', + options: { + attribution: + '© Thunderforest, {attribution.OpenStreetMap}', + variant: 'cycle', + apikey: '', + maxZoom: 22 + }, + variants: { + OpenCycleMap: 'cycle', + Transport: { + options: { + variant: 'transport' + } + }, + TransportDark: { + options: { + variant: 'transport-dark' + } + }, + SpinalMap: { + options: { + variant: 'spinal-map' + } + }, + Landscape: 'landscape', + Outdoors: 'outdoors', + Pioneer: 'pioneer', + MobileAtlas: 'mobile-atlas', + Neighbourhood: 'neighbourhood' + } + }, + CyclOSM: { + url: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + options: { + maxZoom: 20, + attribution: 'CyclOSM | Map data: {attribution.OpenStreetMap}' + } + }, + Jawg: { + url: 'https://{s}.tile.jawg.io/{variant}/{z}/{x}/{y}{r}.png?access-token={accessToken}', + options: { + attribution: + '© JawgMaps ' + + '{attribution.OpenStreetMap}', + minZoom: 0, + maxZoom: 22, + subdomains: 'abcd', + variant: 'jawg-terrain', + // Get your own Jawg access token here : https://www.jawg.io/lab/ + // NB : this is a demonstration key that comes with no guarantee + accessToken: '', + }, + variants: { + Streets: 'jawg-streets', + Terrain: 'jawg-terrain', + Sunny: 'jawg-sunny', + Dark: 'jawg-dark', + Light: 'jawg-light', + Matrix: 'jawg-matrix' + } + }, + MapBox: { + url: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}', + options: { + attribution: + '© Mapbox ' + + '{attribution.OpenStreetMap} ' + + 'Improve this map', + tileSize: 512, + maxZoom: 18, + zoomOffset: -1, + id: 'mapbox/streets-v11', + accessToken: '', + } + }, + MapTiler: { + url: 'https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}', + options: { + attribution: + '© MapTiler © OpenStreetMap contributors', + variant: 'streets', + ext: 'png', + key: '', + tileSize: 512, + zoomOffset: -1, + minZoom: 0, + maxZoom: 21 + }, + variants: { + Streets: 'streets', + Basic: 'basic', + Bright: 'bright', + Pastel: 'pastel', + Positron: 'positron', + Hybrid: { + options: { + variant: 'hybrid', + ext: 'jpg' + } + }, + Toner: 'toner', + Topo: 'topo', + Voyager: 'voyager' + } + }, + TomTom: { + url: 'https://{s}.api.tomtom.com/map/1/tile/{variant}/{style}/{z}/{x}/{y}.{ext}?key={apikey}', + options: { + variant: 'basic', + maxZoom: 22, + attribution: + '© 1992 - ' + new Date().getFullYear() + ' TomTom. ', + subdomains: 'abcd', + style: 'main', + ext: 'png', + apikey: '', + }, + variants: { + Basic: 'basic', + Hybrid: 'hybrid', + Labels: 'labels' + } + }, + Esri: { + url: 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}', + options: { + variant: 'World_Street_Map', + attribution: 'Tiles © Esri' + }, + variants: { + WorldStreetMap: { + options: { + attribution: + '{attribution.Esri} — ' + + 'Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012' + } + }, + DeLorme: { + options: { + variant: 'Specialty/DeLorme_World_Base_Map', + minZoom: 1, + maxZoom: 11, + attribution: '{attribution.Esri} — Copyright: ©2012 DeLorme' + } + }, + WorldTopoMap: { + options: { + variant: 'World_Topo_Map', + attribution: + '{attribution.Esri} — ' + + 'Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' + } + }, + WorldImagery: { + options: { + variant: 'World_Imagery', + attribution: + '{attribution.Esri} — ' + + 'Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' + } + }, + WorldTerrain: { + options: { + variant: 'World_Terrain_Base', + maxZoom: 13, + attribution: + '{attribution.Esri} — ' + + 'Source: USGS, Esri, TANA, DeLorme, and NPS' + } + }, + WorldShadedRelief: { + options: { + variant: 'World_Shaded_Relief', + maxZoom: 13, + attribution: '{attribution.Esri} — Source: Esri' + } + }, + WorldPhysical: { + options: { + variant: 'World_Physical_Map', + maxZoom: 8, + attribution: '{attribution.Esri} — Source: US National Park Service' + } + }, + OceanBasemap: { + options: { + variant: 'Ocean/World_Ocean_Base', + maxZoom: 13, + attribution: '{attribution.Esri} — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri' + } + }, + NatGeoWorldMap: { + options: { + variant: 'NatGeo_World_Map', + maxZoom: 16, + attribution: '{attribution.Esri} — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC' + } + }, + WorldGrayCanvas: { + options: { + variant: 'Canvas/World_Light_Gray_Base', + maxZoom: 16, + attribution: '{attribution.Esri} — Esri, DeLorme, NAVTEQ' + } + } + } + }, + OpenWeatherMap: { + url: 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}', + options: { + maxZoom: 19, + attribution: 'Map data © OpenWeatherMap', + apiKey: '', + opacity: 0.5 + }, + variants: { + Clouds: 'clouds', + CloudsClassic: 'clouds_cls', + Precipitation: 'precipitation', + PrecipitationClassic: 'precipitation_cls', + Rain: 'rain', + RainClassic: 'rain_cls', + Pressure: 'pressure', + PressureContour: 'pressure_cntr', + Wind: 'wind', + Temperature: 'temp', + Snow: 'snow' + } + }, + HERE: { + /* + * HERE maps, formerly Nokia maps. + * These basemaps are free, but you need an api id and app key. Please sign up at + * https://developer.here.com/plans + */ + url: + 'https://{s}.{base}.maps.api.here.com/maptile/2.1/' + + '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + + 'app_id={app_id}&app_code={app_code}&lg={language}', + options: { + attribution: + 'Map © 1987-' + new Date().getFullYear() + ' HERE', + subdomains: '1234', + mapID: 'newest', + 'app_id': '', + 'app_code': '', + base: 'base', + variant: 'normal.day', + maxZoom: 20, + type: 'maptile', + language: 'eng', + format: 'png8', + size: '256' + }, + variants: { + normalDay: 'normal.day', + normalDayCustom: 'normal.day.custom', + normalDayGrey: 'normal.day.grey', + normalDayMobile: 'normal.day.mobile', + normalDayGreyMobile: 'normal.day.grey.mobile', + normalDayTransit: 'normal.day.transit', + normalDayTransitMobile: 'normal.day.transit.mobile', + normalDayTraffic: { + options: { + variant: 'normal.traffic.day', + base: 'traffic', + type: 'traffictile' + } + }, + normalNight: 'normal.night', + normalNightMobile: 'normal.night.mobile', + normalNightGrey: 'normal.night.grey', + normalNightGreyMobile: 'normal.night.grey.mobile', + normalNightTransit: 'normal.night.transit', + normalNightTransitMobile: 'normal.night.transit.mobile', + reducedDay: 'reduced.day', + reducedNight: 'reduced.night', + basicMap: { + options: { + type: 'basetile' + } + }, + mapLabels: { + options: { + type: 'labeltile', + format: 'png' + } + }, + trafficFlow: { + options: { + base: 'traffic', + type: 'flowtile' + } + }, + carnavDayGrey: 'carnav.day.grey', + hybridDay: { + options: { + base: 'aerial', + variant: 'hybrid.day' + } + }, + hybridDayMobile: { + options: { + base: 'aerial', + variant: 'hybrid.day.mobile' + } + }, + hybridDayTransit: { + options: { + base: 'aerial', + variant: 'hybrid.day.transit' + } + }, + hybridDayGrey: { + options: { + base: 'aerial', + variant: 'hybrid.grey.day' + } + }, + hybridDayTraffic: { + options: { + variant: 'hybrid.traffic.day', + base: 'traffic', + type: 'traffictile' + } + }, + pedestrianDay: 'pedestrian.day', + pedestrianNight: 'pedestrian.night', + satelliteDay: { + options: { + base: 'aerial', + variant: 'satellite.day' + } + }, + terrainDay: { + options: { + base: 'aerial', + variant: 'terrain.day' + } + }, + terrainDayMobile: { + options: { + base: 'aerial', + variant: 'terrain.day.mobile' + } + } + } + }, + HEREv3: { + /* + * HERE maps API Version 3. + * These basemaps are free, but you need an API key. Please sign up at + * https://developer.here.com/plans + * Version 3 deprecates the app_id and app_code access in favor of apiKey + * + * Supported access methods as of 2019/12/21: + * @see https://developer.here.com/faqs#access-control-1--how-do-you-control-access-to-here-location-services + */ + url: + 'https://{s}.{base}.maps.ls.hereapi.com/maptile/2.1/' + + '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + + 'apiKey={apiKey}&lg={language}', + options: { + attribution: + 'Map © 1987-' + new Date().getFullYear() + ' HERE', + subdomains: '1234', + mapID: 'newest', + apiKey: '', + base: 'base', + variant: 'normal.day', + maxZoom: 20, + type: 'maptile', + language: 'eng', + format: 'png8', + size: '256' + }, + variants: { + normalDay: 'normal.day', + normalDayCustom: 'normal.day.custom', + normalDayGrey: 'normal.day.grey', + normalDayMobile: 'normal.day.mobile', + normalDayGreyMobile: 'normal.day.grey.mobile', + normalDayTransit: 'normal.day.transit', + normalDayTransitMobile: 'normal.day.transit.mobile', + normalNight: 'normal.night', + normalNightMobile: 'normal.night.mobile', + normalNightGrey: 'normal.night.grey', + normalNightGreyMobile: 'normal.night.grey.mobile', + normalNightTransit: 'normal.night.transit', + normalNightTransitMobile: 'normal.night.transit.mobile', + reducedDay: 'reduced.day', + reducedNight: 'reduced.night', + basicMap: { + options: { + type: 'basetile' + } + }, + mapLabels: { + options: { + type: 'labeltile', + format: 'png' + } + }, + trafficFlow: { + options: { + base: 'traffic', + type: 'flowtile' + } + }, + carnavDayGrey: 'carnav.day.grey', + hybridDay: { + options: { + base: 'aerial', + variant: 'hybrid.day' + } + }, + hybridDayMobile: { + options: { + base: 'aerial', + variant: 'hybrid.day.mobile' + } + }, + hybridDayTransit: { + options: { + base: 'aerial', + variant: 'hybrid.day.transit' + } + }, + hybridDayGrey: { + options: { + base: 'aerial', + variant: 'hybrid.grey.day' + } + }, + pedestrianDay: 'pedestrian.day', + pedestrianNight: 'pedestrian.night', + satelliteDay: { + options: { + base: 'aerial', + variant: 'satellite.day' + } + }, + terrainDay: { + options: { + base: 'aerial', + variant: 'terrain.day' + } + }, + terrainDayMobile: { + options: { + base: 'aerial', + variant: 'terrain.day.mobile' + } + } + } + }, + FreeMapSK: { + url: 'https://{s}.freemap.sk/T/{z}/{x}/{y}.jpeg', + options: { + minZoom: 8, + maxZoom: 16, + subdomains: 'abcd', + bounds: [[47.204642, 15.996093], [49.830896, 22.576904]], + attribution: + '{attribution.OpenStreetMap}, visualization CC-By-SA 2.0 Freemap.sk' + } + }, + MtbMap: { + url: 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', + options: { + attribution: + '{attribution.OpenStreetMap} & USGS' + } + }, + CartoDB: { + url: 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png', + options: { + attribution: '{attribution.OpenStreetMap} © CARTO', + subdomains: 'abcd', + maxZoom: 20, + variant: 'light_all' + }, + variants: { + Positron: 'light_all', + PositronNoLabels: 'light_nolabels', + PositronOnlyLabels: 'light_only_labels', + DarkMatter: 'dark_all', + DarkMatterNoLabels: 'dark_nolabels', + DarkMatterOnlyLabels: 'dark_only_labels', + Voyager: 'rastertiles/voyager', + VoyagerNoLabels: 'rastertiles/voyager_nolabels', + VoyagerOnlyLabels: 'rastertiles/voyager_only_labels', + VoyagerLabelsUnder: 'rastertiles/voyager_labels_under' + } + }, + HikeBike: { + url: 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: '{attribution.OpenStreetMap}', + variant: 'hikebike' + }, + variants: { + HikeBike: {}, + HillShading: { + options: { + maxZoom: 15, + variant: 'hillshading' + } + } + } + }, + BasemapAT: { + url: 'https://mapsneu.wien.gv.at/basemap/{variant}/{type}/google3857/{z}/{y}/{x}.{format}', + options: { + maxZoom: 19, + attribution: 'Datenquelle: basemap.at', + type: 'normal', + format: 'png', + bounds: [[46.358770, 8.782379], [49.037872, 17.189532]], + variant: 'geolandbasemap' + }, + variants: { + basemap: { + options: { + maxZoom: 20, // currently only in Vienna + variant: 'geolandbasemap' + } + }, + grau: 'bmapgrau', + overlay: 'bmapoverlay', + terrain: { + options: { + variant: 'bmapgelaende', + type: 'grau', + format: 'jpeg' + } + }, + surface: { + options: { + variant: 'bmapoberflaeche', + type: 'grau', + format: 'jpeg' + } + }, + highdpi: { + options: { + variant: 'bmaphidpi', + format: 'jpeg' + } + }, + orthofoto: { + options: { + maxZoom: 20, // currently only in Vienna + variant: 'bmaporthofoto30cm', + format: 'jpeg' + } + } + } + }, + nlmaps: { + url: 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/{variant}/EPSG:3857/{z}/{x}/{y}.png', + options: { + minZoom: 6, + maxZoom: 19, + bounds: [[50.5, 3.25], [54, 7.6]], + attribution: 'Kaartgegevens © Kadaster' + }, + variants: { + 'standaard': 'standaard', + 'pastel': 'pastel', + 'grijs': 'grijs', + 'water': 'water', + 'luchtfoto': { + 'url': 'https://service.pdok.nl/hwh/luchtfotorgb/wmts/v1_0/Actueel_ortho25/EPSG:3857/{z}/{x}/{y}.jpeg', + } + } + }, + NASAGIBS: { + url: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', + options: { + attribution: + 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ' + + '(ESDIS) with funding provided by NASA/HQ.', + bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]], + minZoom: 1, + maxZoom: 9, + format: 'jpg', + time: '', + tilematrixset: 'GoogleMapsCompatible_Level' + }, + variants: { + ModisTerraTrueColorCR: 'MODIS_Terra_CorrectedReflectance_TrueColor', + ModisTerraBands367CR: 'MODIS_Terra_CorrectedReflectance_Bands367', + ViirsEarthAtNight2012: { + options: { + variant: 'VIIRS_CityLights_2012', + maxZoom: 8 + } + }, + ModisTerraLSTDay: { + options: { + variant: 'MODIS_Terra_Land_Surface_Temp_Day', + format: 'png', + maxZoom: 7, + opacity: 0.75 + } + }, + ModisTerraSnowCover: { + options: { + variant: 'MODIS_Terra_NDSI_Snow_Cover', + format: 'png', + maxZoom: 8, + opacity: 0.75 + } + }, + ModisTerraAOD: { + options: { + variant: 'MODIS_Terra_Aerosol', + format: 'png', + maxZoom: 6, + opacity: 0.75 + } + }, + ModisTerraChlorophyll: { + options: { + variant: 'MODIS_Terra_Chlorophyll_A', + format: 'png', + maxZoom: 7, + opacity: 0.75 + } + } + } + }, + NLS: { + // NLS maps are copyright National library of Scotland. + // http://maps.nls.uk/projects/api/index.html + // Please contact NLS for anything other than non-commercial low volume usage + // + // Map sources: Ordnance Survey 1:1m to 1:63K, 1920s-1940s + // z0-9 - 1:1m + // z10-11 - quarter inch (1:253440) + // z12-18 - one inch (1:63360) + url: 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg', + options: { + attribution: 'National Library of Scotland Historic Maps', + bounds: [[49.6, -12], [61.7, 3]], + minZoom: 1, + maxZoom: 18, + subdomains: '0123', + } + }, + JusticeMap: { + // Justice Map (http://www.justicemap.org/) + // Visualize race and income data for your community, county and country. + // Includes tools for data journalists, bloggers and community activists. + url: 'https://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png', + options: { + attribution: 'Justice Map', + // one of 'county', 'tract', 'block' + size: 'county', + // Bounds for USA, including Alaska and Hawaii + bounds: [[14, -180], [72, -56]] + }, + variants: { + income: 'income', + americanIndian: 'indian', + asian: 'asian', + black: 'black', + hispanic: 'hispanic', + multi: 'multi', + nonWhite: 'nonwhite', + white: 'white', + plurality: 'plural' + } + }, + GeoportailFrance: { + url: 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', + options: { + attribution: 'Geoportail France', + bounds: [[-75, -180], [81, 180]], + minZoom: 2, + maxZoom: 18, + // Get your own geoportail apikey here : http://professionnels.ign.fr/ign/contrats/ + // NB : 'choisirgeoportail' is a demonstration key that comes with no guarantee + apikey: 'choisirgeoportail', + format: 'image/png', + style: 'normal', + variant: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2' + }, + variants: { + plan: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2', + parcels: { + options: { + variant: 'CADASTRALPARCELS.PARCELLAIRE_EXPRESS', + style: 'PCI vecteur', + maxZoom: 20 + } + }, + orthos: { + options: { + maxZoom: 19, + format: 'image/jpeg', + variant: 'ORTHOIMAGERY.ORTHOPHOTOS' + } + } + } + }, + OneMapSG: { + url: 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png', + options: { + variant: 'Default', + minZoom: 11, + maxZoom: 18, + bounds: [[1.56073, 104.11475], [1.16, 103.502]], + attribution: ' New OneMap | Map data © contributors, Singapore Land Authority' + }, + variants: { + Default: 'Default', + Night: 'Night', + Original: 'Original', + Grey: 'Grey', + LandLot: 'LandLot' + } + }, + USGS: { + url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', + options: { + maxZoom: 20, + attribution: 'Tiles courtesy of the U.S. Geological Survey' + }, + variants: { + USTopo: {}, + USImagery: { + url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}' + }, + USImageryTopo: { + url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}' + } + } + }, + WaymarkedTrails: { + url: 'https://tile.waymarkedtrails.org/{variant}/{z}/{x}/{y}.png', + options: { + maxZoom: 18, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © waymarkedtrails.org (CC-BY-SA)' + }, + variants: { + hiking: 'hiking', + cycling: 'cycling', + mtb: 'mtb', + slopes: 'slopes', + riding: 'riding', + skating: 'skating' + } + }, + OpenAIP: { + url: 'https://{s}.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{y}.{ext}', + options: { + attribution: 'openAIP Data (CC-BY-NC-SA)', + ext: 'png', + minZoom: 4, + maxZoom: 14, + tms: true, + detectRetina: true, + subdomains: '12' + } + }, + OpenSnowMap: { + url: 'https://tiles.opensnowmap.org/{variant}/{z}/{x}/{y}.png', + options: { + minZoom: 9, + maxZoom: 18, + attribution: 'Map data: {attribution.OpenStreetMap} & ODbL, © www.opensnowmap.org CC-BY-SA' + }, + variants: { + pistes: 'pistes', + } + }, + AzureMaps: { + url: + 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+ + '&tilesetId={variant}&x={x}&y={y}&zoom={z}&language={language}'+ + '&subscription-key={subscriptionKey}', + options: { + attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile for details.', + apiVersion: '2.0', + variant: 'microsoft.imagery', + subscriptionKey: '', + language: 'en-US', + }, + variants: { + MicrosoftImagery: 'microsoft.imagery', + MicrosoftBaseDarkGrey: 'microsoft.base.darkgrey', + MicrosoftBaseRoad: 'microsoft.base.road', + MicrosoftBaseHybridRoad: 'microsoft.base.hybrid.road', + MicrosoftTerraMain: 'microsoft.terra.main', + MicrosoftWeatherInfraredMain: { + url: + 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+ + '&tilesetId={variant}&x={x}&y={y}&zoom={z}'+ + '&timeStamp={timeStamp}&language={language}' + + '&subscription-key={subscriptionKey}', + options: { + timeStamp: '2021-05-08T09:03:00Z', + attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile#uri-parameters for details.', + variant: 'microsoft.weather.infrared.main', + }, + }, + MicrosoftWeatherRadarMain: { + url: + 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+ + '&tilesetId={variant}&x={x}&y={y}&zoom={z}'+ + '&timeStamp={timeStamp}&language={language}' + + '&subscription-key={subscriptionKey}', + options: { + timeStamp: '2021-05-08T09:03:00Z', + attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile#uri-parameters for details.', + variant: 'microsoft.weather.radar.main', + }, + } + }, + }, + SwissFederalGeoportal: { + url: 'https://wmts.geo.admin.ch/1.0.0/{variant}/default/current/3857/{z}/{x}/{y}.jpeg', + options: { + attribution: '© swisstopo', + minZoom: 2, + maxZoom: 18, + bounds: [[45.398181, 5.140242], [48.230651, 11.47757]] + }, + variants: { + NationalMapColor: 'ch.swisstopo.pixelkarte-farbe', + NationalMapGrey: 'ch.swisstopo.pixelkarte-grau', + SWISSIMAGE: { + options: { + variant: 'ch.swisstopo.swissimage', + maxZoom: 19 + } + } + } + } + }; + + L.tileLayer.provider = function (provider, options) { + return new L.TileLayer.Provider(provider, options); + }; + + return L; +})); + +},{"leaflet":3}],2:[function(require,module,exports){ +/* + * Leaflet.markercluster 1.5.3+master.e5124b2, + * Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. + * https://github.com/Leaflet/Leaflet.markercluster + * (c) 2012-2017, Dave Leaver, smartrak + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = global || self, factory((global.Leaflet = global.Leaflet || {}, global.Leaflet.markercluster = {}))); +}(this, function (exports) { 'use strict'; + + /* + * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within + */ + + var MarkerClusterGroup = L.MarkerClusterGroup = L.FeatureGroup.extend({ + + options: { + maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center + iconCreateFunction: null, + clusterPane: L.Marker.prototype.options.pane, + + spiderfyOnEveryZoom: false, + 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, + + // Make it possible to provide custom function to calculate spiderfy shape positions + spiderfyShapePositions: null, + + //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.addEventParent(this); + + this._nonPointGroup = L.featureGroup(); + this._nonPointGroup.addEventParent(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 = []; + + this._childMarkerEventHandlers = { + 'dragstart': this._childMarkerDragStart, + 'move': this._childMarkerMoved, + 'dragend': this._childMarkerDragEnd, + }; + + // 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) { + return this.addLayers([layer]); + } + + //Don't cluster non point data + if (!layer.getLatLng) { + this._nonPointGroup.addLayer(layer); + this.fire('layeradd', { layer: layer }); + return this; + } + + if (!this._map) { + this._needsClustering.push(layer); + this.fire('layeradd', { layer: 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); + this.fire('layeradd', { layer: layer }); + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + //Work out what is visible + var visibleLayer = layer, + currentZoom = this._zoom; + 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) { + return this.removeLayers([layer]); + } + + //Non point layers + if (!layer.getLatLng) { + this._nonPointGroup.removeLayer(layer); + this.fire('layerremove', { layer: layer }); + return this; + } + + if (!this._map) { + if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { + this._needsRemoving.push({ layer: layer, latlng: layer._latlng }); + } + this.fire('layerremove', { layer: 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); + this.fire('layerremove', { layer: layer }); + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + layer.off(this._childMarkerEventHandlers, this); + + 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, skipLayerAddEvent) { + if (!L.Util.isArray(layersArray)) { + return this.addLayer(layersArray); + } + + var fg = this._featureGroup, + npg = this._nonPointGroup, + chunked = this.options.chunkedLoading, + chunkInterval = this.options.chunkInterval, + chunkProgress = this.options.chunkProgress, + l = layersArray.length, + offset = 0, + originalArray = true, + m; + + if (this._map) { + var started = (new Date()).getTime(); + var process = L.bind(function () { + var start = (new Date()).getTime(); + + // Make sure to unspiderfy before starting to add some layers + if (this._map && this._unspiderfy) { + this._unspiderfy(); + } + + for (; offset < l; 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]; + + // Group of layers, append children to layersArray and skip. + // Side effects: + // - Total increases, so chunkProgress ratio jumps backward. + // - Groups are not included in this group, only their non-group child layers (hasLayer). + // Changing array length while looping does not affect performance in current browsers: + // http://jsperf.com/for-loop-changing-length/6 + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + if (!skipLayerAddEvent) { + this.fire('layeradd', { layer: m }); + } + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + this._addLayer(m, this._maxZoom); + if (!skipLayerAddEvent) { + this.fire('layeradd', { layer: m }); + } + + //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, l, (new Date()).getTime() - started); + } + + // Completed processing all markers. + if (offset === l) { + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + } else { + setTimeout(process, this.options.chunkDelay); + } + }, this); + + process(); + } else { + var needsClustering = this._needsClustering; + + for (; offset < l; offset++) { + m = layersArray[offset]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + needsClustering.push(m); + } + } + return this; + }, + + //Takes an array of markers and removes them in bulk + removeLayers: function (layersArray) { + var i, m, + l = layersArray.length, + fg = this._featureGroup, + npg = this._nonPointGroup, + originalArray = true; + + if (!this._map) { + for (i = 0; i < l; i++) { + m = layersArray[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + this._arraySplice(this._needsClustering, m); + npg.removeLayer(m); + if (this.hasLayer(m)) { + this._needsRemoving.push({ layer: m, latlng: m._latlng }); + } + this.fire('layerremove', { layer: m }); + } + return this; + } + + if (this._unspiderfy) { + this._unspiderfy(); + + // Work on a copy of the array, so that next loop is not affected. + var layersArray2 = layersArray.slice(), + l2 = l; + for (i = 0; i < l2; i++) { + m = layersArray2[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + this._extractNonGroupLayers(m, layersArray2); + l2 = layersArray2.length; + continue; + } + + this._unspiderfyLayer(m); + } + } + + for (i = 0; i < l; i++) { + m = layersArray[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + if (!m.__parent) { + npg.removeLayer(m); + this.fire('layerremove', { layer: m }); + continue; + } + + this._removeLayer(m, true, true); + this.fire('layerremove', { layer: m }); + + if (fg.hasLayer(m)) { + fg.removeLayer(m); + if (m.clusterShow) { + m.clusterShow(); + } + } + } + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + //Fix up the clusters and markers on the map + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + + 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 = []; + this._needsRemoving = []; + 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) { + marker.off(this._childMarkerEventHandlers, this); + delete marker.__parent; + }, this); + + 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(), + needsRemoving = this._needsRemoving, + thisNeedsRemoving, i, j; + + if (this._topClusterLevel) { + this._topClusterLevel.getAllChildMarkers(markers); + } + + for (i = markers.length - 1; i >= 0; i--) { + thisNeedsRemoving = true; + + for (j = needsRemoving.length - 1; j >= 0; j--) { + if (needsRemoving[j].layer === markers[i]) { + thisNeedsRemoving = false; + break; + } + } + + if (thisNeedsRemoving) { + 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 === 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) { + + var map = this._map; + + if (typeof callback !== 'function') { + callback = function () {}; + } + + var showMarker = function () { + // Assumes that map.hasLayer checks for direct appearance on map, not recursively calling + // hasLayer on Layer Groups that are on map (typically not calling this MarkerClusterGroup.hasLayer, which would always return true) + if ((map.hasLayer(layer) || map.hasLayer(layer.__parent)) && !this._inZoomAnimation) { + this._map.off('moveend', showMarker, this); + this.off('animationend', showMarker, this); + + if (map.hasLayer(layer)) { + 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 < Math.round(this._map._zoom)) { + //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 { + this._map.on('moveend', showMarker, this); + this.on('animationend', showMarker, this); + layer.__parent.zoomToBounds(); + } + }, + + //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.addTo(map); + this._nonPointGroup.addTo(map); + + if (!this._gridClusters) { + this._generateInitialClusters(); + } + + this._maxLat = map.options.crs.projection.MAX_LATITUDE; + + //Restore all the positions as they are in the MCG before removing them + for (i = 0, l = this._needsRemoving.length; i < l; i++) { + layer = this._needsRemoving[i]; + layer.newlatlng = layer.layer._latlng; + layer.layer._latlng = layer.latlng; + } + //Remove them, then restore their new positions + for (i = 0, l = this._needsRemoving.length; i < l; i++) { + layer = this._needsRemoving[i]; + this._removeLayer(layer.layer, true); + layer.layer._latlng = layer.newlatlng; + } + this._needsRemoving = []; + + //Remember the current zoom level and bounds + this._zoom = Math.round(this._map._zoom); + 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, true); + }, + + //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.remove(); + this._nonPointGroup.remove(); + + 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, + minZoom = Math.floor(this._map.getMinZoom()); + + for (; z >= minZoom; z--) { + if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { + break; + } + } + }, + + _childMarkerDragStart: function (e) { + e.target.__dragStart = e.target._latlng; + }, + + _childMarkerMoved: function (e) { + if (!this._ignoreMove && !e.target.__dragStart) { + var isPopupOpen = e.target._popup && e.target._popup.isOpen(); + + this._moveChild(e.target, e.oldLatLng, e.latlng); + + if (isPopupOpen) { + e.target.openPopup(); + } + } + }, + + _moveChild: function (layer, from, to) { + layer._latlng = from; + this.removeLayer(layer); + + layer._latlng = to; + this.addLayer(layer); + }, + + _childMarkerDragEnd: function (e) { + var dragStart = e.target.__dragStart; + delete e.target.__dragStart; + if (dragStart) { + this._moveChild(e.target, dragStart, e.target._latlng); + } + }, + + + //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, + minZoom = Math.floor(this._map.getMinZoom()); + + //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 < minZoom) { + //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 { + cluster._iconNeedsUpdate = true; + } + + cluster = cluster.__parent; + } + + delete marker.__parent; + }, + + _isOrIsParent: function (el, oel) { + while (oel) { + if (el === oel) { + return true; + } + oel = oel.parentNode; + } + return false; + }, + + //Override L.Evented.fire + fire: function (type, data, propagate) { + if (data && data.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 (data.originalEvent && this._isOrIsParent(data.layer._icon, data.originalEvent.relatedTarget)) { + return; + } + type = 'cluster' + type; + } + + L.FeatureGroup.prototype.fire.call(this, type, data, propagate); + }, + + //Override L.Evented.listens + listens: function (type, propagate) { + return L.FeatureGroup.prototype.listens.call(this, type, propagate) || L.FeatureGroup.prototype.listens.call(this, 'cluster' + type, propagate); + }, + + //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, + spiderfyOnEveryZoom = this.options.spiderfyOnEveryZoom; + + //Zoom on cluster click or spiderfy if we are at the lowest level + if (spiderfyOnMaxZoom || zoomToBoundsOnClick || spiderfyOnEveryZoom) { + this.on('clusterclick clusterkeypress', 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; + + if (e.type === 'clusterkeypress' && e.originalEvent && e.originalEvent.keyCode !== 13) { + return; + } + + while (bottomCluster._childClusters.length === 1) { + bottomCluster = bottomCluster._childClusters[0]; + } + + if (bottomCluster._zoom === this._maxZoom && + bottomCluster._childCount === cluster._childCount && + this.options.spiderfyOnMaxZoom) { + + // All child markers are contained in a single cluster from this._maxZoom to this cluster. + cluster.spiderfy(); + } else if (this.options.zoomToBoundsOnClick) { + cluster.zoomToBounds(); + } + + if (this.options.spiderfyOnEveryZoom) { + cluster.spiderfy(); + } + + // 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, + spiderfyOnEveryZoom = this.options.spiderfyOnEveryZoom, + map = this._map; + + if (spiderfyOnMaxZoom || zoomToBoundsOnClick || spiderfyOnEveryZoom) { + this.off('clusterclick clusterkeypress', 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 = Math.round(this._map._zoom); + this._currentShownBounds = this._getExpandedVisibleBounds(); + }, + + _moveEnd: function () { + if (this._inZoomAnimation) { + return; + } + + var newBounds = this._getExpandedVisibleBounds(); + + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, newBounds); + this._topClusterLevel._recursivelyAddChildrenToMap(null, Math.round(this._map._zoom), newBounds); + + this._currentShownBounds = newBounds; + return; + }, + + _generateInitialClusters: function () { + var maxZoom = Math.ceil(this._map.getMaxZoom()), + minZoom = Math.floor(this._map.getMinZoom()), + 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 !== null) { + maxZoom = this.options.disableClusteringAtZoom - 1; + } + this._maxZoom = maxZoom; + this._gridClusters = {}; + this._gridUnclustered = {}; + + //Set up DistanceGrids for each zoom + for (var zoom = maxZoom; zoom >= minZoom; 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, minZoom - 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, + minZoom = Math.floor(this._map.getMinZoom()), + markerPoint, z; + + if (this.options.singleMarkerMode) { + this._overrideMarkerIcon(layer); + } + + layer.on(this._childMarkerEventHandlers, this); + + //Find the lowest zoom level to slot this one in + for (; zoom >= minZoom; 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; + }, + + /** + * 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(); + } + }); + }, + + //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 () { + var mapZoom = Math.round(this._map._zoom); + + //In case we are starting to split before the animation finished + this._processQueue(); + + if (this._zoom < mapZoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split + this._animationStart(); + //Remove clusters now off screen + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, this._getExpandedVisibleBounds()); + + this._animationZoomIn(this._zoom, mapZoom); + + } else if (this._zoom > mapZoom) { //Zoom out, merge + this._animationStart(); + + this._animationZoomOut(this._zoom, mapZoom); + } 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(); + } + }, + + /** + * Extracts individual (i.e. non-group) layers from a Layer Group. + * @param group to extract layers from. + * @param output {Array} in which to store the extracted layers. + * @returns {*|Array} + * @private + */ + _extractNonGroupLayers: function (group, output) { + var layers = group.getLayers(), + i = 0, + layer; + + output = output || []; + + for (; i < layers.length; i++) { + layer = layers[i]; + + if (layer instanceof L.LayerGroup) { + this._extractNonGroupLayers(layer, output); + continue; + } + + output.push(layer); + } + + return output; + }, + + /** + * 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, Math.floor(this._map.getMinZoom()), 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, Math.floor(this._map.getMinZoom()), 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, + minZoom = Math.floor(this._map.getMinZoom()), + i; + + this._ignoreMove = true; + + //Add all children of current clusters to map and remove those clusters from map + this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, 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); + }); + + this._ignoreMove = false; + + //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, minZoom, 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, Math.floor(this._map.getMinZoom()), 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._zoom); + } + } + } + }, + + // Private methods for animated versions. + _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { + var bounds = this._getExpandedVisibleBounds(), + minZoom = Math.floor(this._map.getMinZoom()); + + //Animate all of the markers in the clusters to move to their cluster center point + cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, minZoom, 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 + this._ignoreMove = true; + m.setLatLng(m.getLatLng()); + this._ignoreMove = false; + if (m.clusterShow) { + m.clusterShow(); + } + } else { + cluster._recursively(bounds, newZoomLevel, minZoom, function (c) { + c._recursivelyRemoveChildrenFromMap(bounds, minZoom, 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); + }; + + var MarkerCluster = L.MarkerCluster = L.Marker.extend({ + options: L.Icon.prototype.options, + + initialize: function (group, zoom, a, b) { + + L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), + { icon: this, pane: group.options.clusterPane }); + + 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, ignoreDraggedMarker) { + storageArray = storageArray || []; + + for (var i = this._childClusters.length - 1; i >= 0; i--) { + this._childClusters[i].getAllChildMarkers(storageArray, ignoreDraggedMarker); + } + + for (var j = this._markers.length - 1; j >= 0; j--) { + if (ignoreDraggedMarker && this._markers[j].__dragStart) { + continue; + } + 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 (fitBoundsOptions) { + 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, fitBoundsOptions); + } + }, + + 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, this._group._map.getMinZoom(), 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, mapMinZoom, previousZoomLevel, newZoomLevel) { + this._recursively(bounds, newZoomLevel, mapMinZoom, + 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, mapMinZoom, 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, this._group._map.getMinZoom(), zoomLevel, null, function (c) { + c.clusterShow(); + }); + }, + + _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { + this._recursively(bounds, this._group._map.getMinZoom() - 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, mapMinZoom, zoomLevel, exceptBounds) { + var m, i; + this._recursively(previousBounds, mapMinZoom - 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) { + if (runAtEveryLevel) { + runAtEveryLevel(this); + } + if (runAtBottomLevel && zoom === zoomLevelToStop) { + runAtBottomLevel(this); + } + } + + if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) { + for (i = childClusters.length - 1; i >= 0; i--) { + c = childClusters[i]; + if (c._boundsNeedUpdate) { + c._recalculateBounds(); + } + 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; + } + }); + + /* + * Extends L.Marker to include two extra methods: clusterHide and clusterShow. + * + * They work as setOpacity(0) and setOpacity(1) respectively, but + * don't overwrite the options.opacity + * + */ + + L.Marker.include({ + clusterHide: function () { + var backup = this.options.opacity; + this.setOpacity(0); + this.options.opacity = backup; + return this; + }, + + clusterShow: function () { + return this.setOpacity(this.options.opacity); + } + }); + + 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 || + dist <= closestDistSq && closest === null) { + closestDistSq = dist; + closest = obj; + } + } + } + } + } + } + return closest; + }, + + _getCoord: function (x) { + var coord = Math.floor(x / this._cellSize); + return isFinite(coord) ? coord : x; + }, + + _sqDist: function (p, p2) { + var dx = p2.x - p.x, + dy = p2.y - p.y; + return dx * dx + dy * dy; + } + }; + + /* 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); + } + }); + + //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: 0, + + _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(null, true), + 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 (this._group.options.spiderfyShapePositions) { + positions = this._group.options.spiderfyShapePositions(childMarkers.length, center); + } else 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; + + legLength = Math.max(legLength, 35); // Minimum distance to get outside the cluster icon. + + res.length = count; + + for (i = 0; i < count; i++) { // Clockwise, like spiral. + 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; i >= 0; i--) { + // Skip the first position, so that we are already farther from center and we avoid + // being under the default cluster icon (especially important for Circle Markers). + if (i < count) { + res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); + } + angle += separation / legLength + i * 0.0005; + legLength += lengthFactor / angle; + } + return res; + }, + + _noanimationUnspiderfy: function () { + var group = this._group, + map = group._map, + fg = group._featureGroup, + childMarkers = this.getAllChildMarkers(null, true), + m, i; + + group._ignoreMove = true; + + 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._ignoreMove = false; + 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; + + group._ignoreMove = true; + + // 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._ignoreMove = false; + 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; + } + + group._ignoreMove = true; + + // 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); + + group._ignoreMove = false; + + 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(null, true), + svg = L.Path.SVG, + m, i, leg, legPath, legLength, nonAnimatable; + + group._ignoreMove = true; + 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; + } + + //Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll + m.closePopup(); + + //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}); + } + } + + group._ignoreMove = false; + + 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, + + unspiderfy: function () { + this._unspiderfy.apply(this, arguments); + }, + + _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); + + if (!L.Browser.touch) { + this._map.getRenderer(this); + //Needs to happen in the pageload, not after, or animations don't work in webkit + // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements + //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable + } + }, + + _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; + } + } + }); + + /** + * 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; + } + } + }, + + /** + * 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; + } + }); + + exports.MarkerClusterGroup = MarkerClusterGroup; + exports.MarkerCluster = MarkerCluster; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); + + +},{}],3:[function(require,module,exports){ +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.leaflet = {})); +})(this, (function (exports) { 'use strict'; + + var version = "1.9.4"; + + /* + * @namespace Util + * + * Various utility functions, used by Leaflet internally. + */ + + // @function extend(dest: Object, src?: Object): Object + // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut. + function extend(dest) { + var i, j, len, src; + + for (j = 1, len = arguments.length; j < len; j++) { + src = arguments[j]; + for (i in src) { + dest[i] = src[i]; + } + } + return dest; + } + + // @function create(proto: Object, properties?: Object): Object + // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create) + var create$2 = Object.create || (function () { + function F() {} + return function (proto) { + F.prototype = proto; + return new F(); + }; + })(); + + // @function bind(fn: Function, …): Function + // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). + // Has a `L.bind()` shortcut. + function bind(fn, obj) { + var slice = Array.prototype.slice; + + if (fn.bind) { + return fn.bind.apply(fn, slice.call(arguments, 1)); + } + + var args = slice.call(arguments, 2); + + return function () { + return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); + }; + } + + // @property lastId: Number + // Last unique ID used by [`stamp()`](#util-stamp) + var lastId = 0; + + // @function stamp(obj: Object): Number + // Returns the unique ID of an object, assigning it one if it doesn't have it. + function stamp(obj) { + if (!('_leaflet_id' in obj)) { + obj['_leaflet_id'] = ++lastId; + } + return obj._leaflet_id; + } + + // @function throttle(fn: Function, time: Number, context: Object): Function + // Returns a function which executes function `fn` with the given scope `context` + // (so that the `this` keyword refers to `context` inside `fn`'s code). The function + // `fn` will be called no more than one time per given amount of `time`. The arguments + // received by the bound function will be any arguments passed when binding the + // function, followed by any arguments passed when invoking the bound function. + // Has an `L.throttle` shortcut. + function throttle(fn, time, context) { + var lock, args, wrapperFn, later; + + later = function () { + // reset lock and call if queued + lock = false; + if (args) { + wrapperFn.apply(context, args); + args = false; + } + }; + + wrapperFn = function () { + if (lock) { + // called too soon, queue to call later + args = arguments; + + } else { + // call and lock until later + fn.apply(context, arguments); + setTimeout(later, time); + lock = true; + } + }; + + return wrapperFn; + } + + // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number + // Returns the number `num` modulo `range` in such a way so it lies within + // `range[0]` and `range[1]`. The returned value will be always smaller than + // `range[1]` unless `includeMax` is set to `true`. + function wrapNum(x, range, includeMax) { + var max = range[1], + min = range[0], + d = max - min; + return x === max && includeMax ? x : ((x - min) % d + d) % d + min; + } + + // @function falseFn(): Function + // Returns a function which always returns `false`. + function falseFn() { return false; } + + // @function formatNum(num: Number, precision?: Number|false): Number + // Returns the number `num` rounded with specified `precision`. + // The default `precision` value is 6 decimal places. + // `false` can be passed to skip any processing (can be useful to avoid round-off errors). + function formatNum(num, precision) { + if (precision === false) { return num; } + var pow = Math.pow(10, precision === undefined ? 6 : precision); + return Math.round(num * pow) / pow; + } + + // @function trim(str: String): String + // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) + function trim(str) { + return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); + } + + // @function splitWords(str: String): String[] + // Trims and splits the string on whitespace and returns the array of parts. + function splitWords(str) { + return trim(str).split(/\s+/); + } + + // @function setOptions(obj: Object, options: Object): Object + // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut. + function setOptions(obj, options) { + if (!Object.prototype.hasOwnProperty.call(obj, 'options')) { + obj.options = obj.options ? create$2(obj.options) : {}; + } + for (var i in options) { + obj.options[i] = options[i]; + } + return obj.options; + } + + // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String + // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}` + // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will + // be appended at the end. If `uppercase` is `true`, the parameter names will + // be uppercased (e.g. `'?A=foo&B=bar'`) + function getParamString(obj, existingUrl, uppercase) { + var params = []; + for (var i in obj) { + params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i])); + } + return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); + } + + var templateRe = /\{ *([\w_ -]+) *\}/g; + + // @function template(str: String, data: Object): String + // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'` + // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string + // `('Hello foo, bar')`. You can also specify functions instead of strings for + // data values — they will be evaluated passing `data` as an argument. + function template(str, data) { + return str.replace(templateRe, function (str, key) { + var value = data[key]; + + if (value === undefined) { + throw new Error('No value provided for variable ' + str); + + } else if (typeof value === 'function') { + value = value(data); + } + return value; + }); + } + + // @function isArray(obj): Boolean + // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) + var isArray = Array.isArray || function (obj) { + return (Object.prototype.toString.call(obj) === '[object Array]'); + }; + + // @function indexOf(array: Array, el: Object): Number + // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) + function indexOf(array, el) { + for (var i = 0; i < array.length; i++) { + if (array[i] === el) { return i; } + } + return -1; + } + + // @property emptyImageUrl: String + // Data URI string containing a base64-encoded empty GIF image. + // Used as a hack to free memory from unused images on WebKit-powered + // mobile devices (by setting image `src` to this string). + var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + + // inspired by https://paulirish.com/2011/requestanimationframe-for-smart-animating/ + + function getPrefixed(name) { + return window['webkit' + name] || window['moz' + name] || window['ms' + name]; + } + + var lastTime = 0; + + // fallback for IE 7-8 + function timeoutDefer(fn) { + var time = +new Date(), + timeToCall = Math.max(0, 16 - (time - lastTime)); + + lastTime = time + timeToCall; + return window.setTimeout(fn, timeToCall); + } + + var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer; + var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || + getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; + + // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number + // Schedules `fn` to be executed when the browser repaints. `fn` is bound to + // `context` if given. When `immediate` is set, `fn` is called immediately if + // the browser doesn't have native support for + // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame), + // otherwise it's delayed. Returns a request ID that can be used to cancel the request. + function requestAnimFrame(fn, context, immediate) { + if (immediate && requestFn === timeoutDefer) { + fn.call(context); + } else { + return requestFn.call(window, bind(fn, context)); + } + } + + // @function cancelAnimFrame(id: Number): undefined + // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame). + function cancelAnimFrame(id) { + if (id) { + cancelFn.call(window, id); + } + } + + var Util = { + __proto__: null, + extend: extend, + create: create$2, + bind: bind, + get lastId () { return lastId; }, + stamp: stamp, + throttle: throttle, + wrapNum: wrapNum, + falseFn: falseFn, + formatNum: formatNum, + trim: trim, + splitWords: splitWords, + setOptions: setOptions, + getParamString: getParamString, + template: template, + isArray: isArray, + indexOf: indexOf, + emptyImageUrl: emptyImageUrl, + requestFn: requestFn, + cancelFn: cancelFn, + requestAnimFrame: requestAnimFrame, + cancelAnimFrame: cancelAnimFrame + }; + + // @class Class + // @aka L.Class + + // @section + // @uninheritable + + // Thanks to John Resig and Dean Edwards for inspiration! + + function Class() {} + + Class.extend = function (props) { + + // @function extend(props: Object): Function + // [Extends the current class](#class-inheritance) given the properties to be included. + // Returns a Javascript function that is a class constructor (to be called with `new`). + var NewClass = function () { + + setOptions(this); + + // call the constructor + if (this.initialize) { + this.initialize.apply(this, arguments); + } + + // call all constructor hooks + this.callInitHooks(); + }; + + var parentProto = NewClass.__super__ = this.prototype; + + var proto = create$2(parentProto); + proto.constructor = NewClass; + + NewClass.prototype = proto; + + // inherit parent's statics + for (var i in this) { + if (Object.prototype.hasOwnProperty.call(this, i) && i !== 'prototype' && i !== '__super__') { + NewClass[i] = this[i]; + } + } + + // mix static properties into the class + if (props.statics) { + extend(NewClass, props.statics); + } + + // mix includes into the prototype + if (props.includes) { + checkDeprecatedMixinEvents(props.includes); + extend.apply(null, [proto].concat(props.includes)); + } + + // mix given properties into the prototype + extend(proto, props); + delete proto.statics; + delete proto.includes; + + // merge options + if (proto.options) { + proto.options = parentProto.options ? create$2(parentProto.options) : {}; + extend(proto.options, props.options); + } + + proto._initHooks = []; + + // add method for calling all hooks + proto.callInitHooks = function () { + + if (this._initHooksCalled) { return; } + + if (parentProto.callInitHooks) { + parentProto.callInitHooks.call(this); + } + + this._initHooksCalled = true; + + for (var i = 0, len = proto._initHooks.length; i < len; i++) { + proto._initHooks[i].call(this); + } + }; + + return NewClass; + }; + + + // @function include(properties: Object): this + // [Includes a mixin](#class-includes) into the current class. + Class.include = function (props) { + var parentOptions = this.prototype.options; + extend(this.prototype, props); + if (props.options) { + this.prototype.options = parentOptions; + this.mergeOptions(props.options); + } + return this; + }; + + // @function mergeOptions(options: Object): this + // [Merges `options`](#class-options) into the defaults of the class. + Class.mergeOptions = function (options) { + extend(this.prototype.options, options); + return this; + }; + + // @function addInitHook(fn: Function): this + // Adds a [constructor hook](#class-constructor-hooks) to the class. + Class.addInitHook = function (fn) { // (Function) || (String, args...) + var args = Array.prototype.slice.call(arguments, 1); + + var init = typeof fn === 'function' ? fn : function () { + this[fn].apply(this, args); + }; + + this.prototype._initHooks = this.prototype._initHooks || []; + this.prototype._initHooks.push(init); + return this; + }; + + function checkDeprecatedMixinEvents(includes) { + /* global L: true */ + if (typeof L === 'undefined' || !L || !L.Mixin) { return; } + + includes = isArray(includes) ? includes : [includes]; + + for (var i = 0; i < includes.length; i++) { + if (includes[i] === L.Mixin.Events) { + console.warn('Deprecated include of L.Mixin.Events: ' + + 'this property will be removed in future releases, ' + + 'please inherit from L.Evented instead.', new Error().stack); + } + } + } + + /* + * @class Evented + * @aka L.Evented + * @inherits Class + * + * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event). + * + * @example + * + * ```js + * map.on('click', function(e) { + * alert(e.latlng); + * } ); + * ``` + * + * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function: + * + * ```js + * function onClick(e) { ... } + * + * map.on('click', onClick); + * map.off('click', onClick); + * ``` + */ + + var Events = { + /* @method on(type: String, fn: Function, context?: Object): this + * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`). + * + * @alternative + * @method on(eventMap: Object): this + * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` + */ + on: function (types, fn, context) { + + // types can be a map of types/handlers + if (typeof types === 'object') { + for (var type in types) { + // we don't process space-separated events here for performance; + // it's a hot path since Layer uses the on(obj) syntax + this._on(type, types[type], fn); + } + + } else { + // types can be a string of space-separated words + types = splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._on(types[i], fn, context); + } + } + + return this; + }, + + /* @method off(type: String, fn?: Function, context?: Object): this + * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener. + * + * @alternative + * @method off(eventMap: Object): this + * Removes a set of type/listener pairs. + * + * @alternative + * @method off: this + * Removes all listeners to all events on the object. This includes implicitly attached events. + */ + off: function (types, fn, context) { + + if (!arguments.length) { + // clear all listeners if called without arguments + delete this._events; + + } else if (typeof types === 'object') { + for (var type in types) { + this._off(type, types[type], fn); + } + + } else { + types = splitWords(types); + + var removeAll = arguments.length === 1; + for (var i = 0, len = types.length; i < len; i++) { + if (removeAll) { + this._off(types[i]); + } else { + this._off(types[i], fn, context); + } + } + } + + return this; + }, + + // attach listener (without syntactic sugar now) + _on: function (type, fn, context, _once) { + if (typeof fn !== 'function') { + console.warn('wrong listener type: ' + typeof fn); + return; + } + + // check if fn already there + if (this._listens(type, fn, context) !== false) { + return; + } + + if (context === this) { + // Less memory footprint. + context = undefined; + } + + var newListener = {fn: fn, ctx: context}; + if (_once) { + newListener.once = true; + } + + this._events = this._events || {}; + this._events[type] = this._events[type] || []; + this._events[type].push(newListener); + }, + + _off: function (type, fn, context) { + var listeners, + i, + len; + + if (!this._events) { + return; + } + + listeners = this._events[type]; + if (!listeners) { + return; + } + + if (arguments.length === 1) { // remove all + if (this._firingCount) { + // Set all removed listeners to noop + // so they are not called if remove happens in fire + for (i = 0, len = listeners.length; i < len; i++) { + listeners[i].fn = falseFn; + } + } + // clear all listeners for a type if function isn't specified + delete this._events[type]; + return; + } + + if (typeof fn !== 'function') { + console.warn('wrong listener type: ' + typeof fn); + return; + } + + // find fn and remove it + var index = this._listens(type, fn, context); + if (index !== false) { + var listener = listeners[index]; + if (this._firingCount) { + // set the removed listener to noop so that's not called if remove happens in fire + listener.fn = falseFn; + + /* copy array in case events are being fired */ + this._events[type] = listeners = listeners.slice(); + } + listeners.splice(index, 1); + } + }, + + // @method fire(type: String, data?: Object, propagate?: Boolean): this + // Fires an event of the specified type. You can optionally provide a data + // object — the first argument of the listener function will contain its + // properties. The event can optionally be propagated to event parents. + fire: function (type, data, propagate) { + if (!this.listens(type, propagate)) { return this; } + + var event = extend({}, data, { + type: type, + target: this, + sourceTarget: data && data.sourceTarget || this + }); + + if (this._events) { + var listeners = this._events[type]; + if (listeners) { + this._firingCount = (this._firingCount + 1) || 1; + for (var i = 0, len = listeners.length; i < len; i++) { + var l = listeners[i]; + // off overwrites l.fn, so we need to copy fn to a var + var fn = l.fn; + if (l.once) { + this.off(type, fn, l.ctx); + } + fn.call(l.ctx || this, event); + } + + this._firingCount--; + } + } + + if (propagate) { + // propagate the event to parents (set with addEventParent) + this._propagateEvent(event); + } + + return this; + }, + + // @method listens(type: String, propagate?: Boolean): Boolean + // @method listens(type: String, fn: Function, context?: Object, propagate?: Boolean): Boolean + // Returns `true` if a particular event type has any listeners attached to it. + // The verification can optionally be propagated, it will return `true` if parents have the listener attached to it. + listens: function (type, fn, context, propagate) { + if (typeof type !== 'string') { + console.warn('"string" type argument expected'); + } + + // we don't overwrite the input `fn` value, because we need to use it for propagation + var _fn = fn; + if (typeof fn !== 'function') { + propagate = !!fn; + _fn = undefined; + context = undefined; + } + + var listeners = this._events && this._events[type]; + if (listeners && listeners.length) { + if (this._listens(type, _fn, context) !== false) { + return true; + } + } + + if (propagate) { + // also check parents for listeners if event propagates + for (var id in this._eventParents) { + if (this._eventParents[id].listens(type, fn, context, propagate)) { return true; } + } + } + return false; + }, + + // returns the index (number) or false + _listens: function (type, fn, context) { + if (!this._events) { + return false; + } + + var listeners = this._events[type] || []; + if (!fn) { + return !!listeners.length; + } + + if (context === this) { + // Less memory footprint. + context = undefined; + } + + for (var i = 0, len = listeners.length; i < len; i++) { + if (listeners[i].fn === fn && listeners[i].ctx === context) { + return i; + } + } + return false; + + }, + + // @method once(…): this + // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed. + once: function (types, fn, context) { + + // types can be a map of types/handlers + if (typeof types === 'object') { + for (var type in types) { + // we don't process space-separated events here for performance; + // it's a hot path since Layer uses the on(obj) syntax + this._on(type, types[type], fn, true); + } + + } else { + // types can be a string of space-separated words + types = splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._on(types[i], fn, context, true); + } + } + + return this; + }, + + // @method addEventParent(obj: Evented): this + // Adds an event parent - an `Evented` that will receive propagated events + addEventParent: function (obj) { + this._eventParents = this._eventParents || {}; + this._eventParents[stamp(obj)] = obj; + return this; + }, + + // @method removeEventParent(obj: Evented): this + // Removes an event parent, so it will stop receiving propagated events + removeEventParent: function (obj) { + if (this._eventParents) { + delete this._eventParents[stamp(obj)]; + } + return this; + }, + + _propagateEvent: function (e) { + for (var id in this._eventParents) { + this._eventParents[id].fire(e.type, extend({ + layer: e.target, + propagatedFrom: e.target + }, e), true); + } + } + }; + + // aliases; we should ditch those eventually + + // @method addEventListener(…): this + // Alias to [`on(…)`](#evented-on) + Events.addEventListener = Events.on; + + // @method removeEventListener(…): this + // Alias to [`off(…)`](#evented-off) + + // @method clearAllEventListeners(…): this + // Alias to [`off()`](#evented-off) + Events.removeEventListener = Events.clearAllEventListeners = Events.off; + + // @method addOneTimeEventListener(…): this + // Alias to [`once(…)`](#evented-once) + Events.addOneTimeEventListener = Events.once; + + // @method fireEvent(…): this + // Alias to [`fire(…)`](#evented-fire) + Events.fireEvent = Events.fire; + + // @method hasEventListeners(…): Boolean + // Alias to [`listens(…)`](#evented-listens) + Events.hasEventListeners = Events.listens; + + var Evented = Class.extend(Events); + + /* + * @class Point + * @aka L.Point + * + * Represents a point with `x` and `y` coordinates in pixels. + * + * @example + * + * ```js + * var point = L.point(200, 300); + * ``` + * + * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent: + * + * ```js + * map.panBy([200, 300]); + * map.panBy(L.point(200, 300)); + * ``` + * + * Note that `Point` does not inherit from Leaflet's `Class` object, + * which means new classes can't inherit from it, and new methods + * can't be added to it with the `include` function. + */ + + function Point(x, y, round) { + // @property x: Number; The `x` coordinate of the point + this.x = (round ? Math.round(x) : x); + // @property y: Number; The `y` coordinate of the point + this.y = (round ? Math.round(y) : y); + } + + var trunc = Math.trunc || function (v) { + return v > 0 ? Math.floor(v) : Math.ceil(v); + }; + + Point.prototype = { + + // @method clone(): Point + // Returns a copy of the current point. + clone: function () { + return new Point(this.x, this.y); + }, + + // @method add(otherPoint: Point): Point + // Returns the result of addition of the current and the given points. + add: function (point) { + // non-destructive, returns a new point + return this.clone()._add(toPoint(point)); + }, + + _add: function (point) { + // destructive, used directly for performance in situations where it's safe to modify existing point + this.x += point.x; + this.y += point.y; + return this; + }, + + // @method subtract(otherPoint: Point): Point + // Returns the result of subtraction of the given point from the current. + subtract: function (point) { + return this.clone()._subtract(toPoint(point)); + }, + + _subtract: function (point) { + this.x -= point.x; + this.y -= point.y; + return this; + }, + + // @method divideBy(num: Number): Point + // Returns the result of division of the current point by the given number. + divideBy: function (num) { + return this.clone()._divideBy(num); + }, + + _divideBy: function (num) { + this.x /= num; + this.y /= num; + return this; + }, + + // @method multiplyBy(num: Number): Point + // Returns the result of multiplication of the current point by the given number. + multiplyBy: function (num) { + return this.clone()._multiplyBy(num); + }, + + _multiplyBy: function (num) { + this.x *= num; + this.y *= num; + return this; + }, + + // @method scaleBy(scale: Point): Point + // Multiply each coordinate of the current point by each coordinate of + // `scale`. In linear algebra terms, multiply the point by the + // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation) + // defined by `scale`. + scaleBy: function (point) { + return new Point(this.x * point.x, this.y * point.y); + }, + + // @method unscaleBy(scale: Point): Point + // Inverse of `scaleBy`. Divide each coordinate of the current point by + // each coordinate of `scale`. + unscaleBy: function (point) { + return new Point(this.x / point.x, this.y / point.y); + }, + + // @method round(): Point + // Returns a copy of the current point with rounded coordinates. + round: function () { + return this.clone()._round(); + }, + + _round: function () { + this.x = Math.round(this.x); + this.y = Math.round(this.y); + return this; + }, + + // @method floor(): Point + // Returns a copy of the current point with floored coordinates (rounded down). + floor: function () { + return this.clone()._floor(); + }, + + _floor: function () { + this.x = Math.floor(this.x); + this.y = Math.floor(this.y); + return this; + }, + + // @method ceil(): Point + // Returns a copy of the current point with ceiled coordinates (rounded up). + ceil: function () { + return this.clone()._ceil(); + }, + + _ceil: function () { + this.x = Math.ceil(this.x); + this.y = Math.ceil(this.y); + return this; + }, + + // @method trunc(): Point + // Returns a copy of the current point with truncated coordinates (rounded towards zero). + trunc: function () { + return this.clone()._trunc(); + }, + + _trunc: function () { + this.x = trunc(this.x); + this.y = trunc(this.y); + return this; + }, + + // @method distanceTo(otherPoint: Point): Number + // Returns the cartesian distance between the current and the given points. + distanceTo: function (point) { + point = toPoint(point); + + var x = point.x - this.x, + y = point.y - this.y; + + return Math.sqrt(x * x + y * y); + }, + + // @method equals(otherPoint: Point): Boolean + // Returns `true` if the given point has the same coordinates. + equals: function (point) { + point = toPoint(point); + + return point.x === this.x && + point.y === this.y; + }, + + // @method contains(otherPoint: Point): Boolean + // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values). + contains: function (point) { + point = toPoint(point); + + return Math.abs(point.x) <= Math.abs(this.x) && + Math.abs(point.y) <= Math.abs(this.y); + }, + + // @method toString(): String + // Returns a string representation of the point for debugging purposes. + toString: function () { + return 'Point(' + + formatNum(this.x) + ', ' + + formatNum(this.y) + ')'; + } + }; + + // @factory L.point(x: Number, y: Number, round?: Boolean) + // Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values. + + // @alternative + // @factory L.point(coords: Number[]) + // Expects an array of the form `[x, y]` instead. + + // @alternative + // @factory L.point(coords: Object) + // Expects a plain object of the form `{x: Number, y: Number}` instead. + function toPoint(x, y, round) { + if (x instanceof Point) { + return x; + } + if (isArray(x)) { + return new Point(x[0], x[1]); + } + if (x === undefined || x === null) { + return x; + } + if (typeof x === 'object' && 'x' in x && 'y' in x) { + return new Point(x.x, x.y); + } + return new Point(x, y, round); + } + + /* + * @class Bounds + * @aka L.Bounds + * + * Represents a rectangular area in pixel coordinates. + * + * @example + * + * ```js + * var p1 = L.point(10, 10), + * p2 = L.point(40, 60), + * bounds = L.bounds(p1, p2); + * ``` + * + * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: + * + * ```js + * otherBounds.intersects([[10, 10], [40, 60]]); + * ``` + * + * Note that `Bounds` does not inherit from Leaflet's `Class` object, + * which means new classes can't inherit from it, and new methods + * can't be added to it with the `include` function. + */ + + function Bounds(a, b) { + if (!a) { return; } + + var points = b ? [a, b] : a; + + for (var i = 0, len = points.length; i < len; i++) { + this.extend(points[i]); + } + } + + Bounds.prototype = { + // @method extend(point: Point): this + // Extends the bounds to contain the given point. + + // @alternative + // @method extend(otherBounds: Bounds): this + // Extend the bounds to contain the given bounds + extend: function (obj) { + var min2, max2; + if (!obj) { return this; } + + if (obj instanceof Point || typeof obj[0] === 'number' || 'x' in obj) { + min2 = max2 = toPoint(obj); + } else { + obj = toBounds(obj); + min2 = obj.min; + max2 = obj.max; + + if (!min2 || !max2) { return this; } + } + + // @property min: Point + // The top left corner of the rectangle. + // @property max: Point + // The bottom right corner of the rectangle. + if (!this.min && !this.max) { + this.min = min2.clone(); + this.max = max2.clone(); + } else { + this.min.x = Math.min(min2.x, this.min.x); + this.max.x = Math.max(max2.x, this.max.x); + this.min.y = Math.min(min2.y, this.min.y); + this.max.y = Math.max(max2.y, this.max.y); + } + return this; + }, + + // @method getCenter(round?: Boolean): Point + // Returns the center point of the bounds. + getCenter: function (round) { + return toPoint( + (this.min.x + this.max.x) / 2, + (this.min.y + this.max.y) / 2, round); + }, + + // @method getBottomLeft(): Point + // Returns the bottom-left point of the bounds. + getBottomLeft: function () { + return toPoint(this.min.x, this.max.y); + }, + + // @method getTopRight(): Point + // Returns the top-right point of the bounds. + getTopRight: function () { // -> Point + return toPoint(this.max.x, this.min.y); + }, + + // @method getTopLeft(): Point + // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)). + getTopLeft: function () { + return this.min; // left, top + }, + + // @method getBottomRight(): Point + // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)). + getBottomRight: function () { + return this.max; // right, bottom + }, + + // @method getSize(): Point + // Returns the size of the given bounds + getSize: function () { + return this.max.subtract(this.min); + }, + + // @method contains(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle contains the given one. + // @alternative + // @method contains(point: Point): Boolean + // Returns `true` if the rectangle contains the given point. + contains: function (obj) { + var min, max; + + if (typeof obj[0] === 'number' || obj instanceof Point) { + obj = toPoint(obj); + } else { + obj = toBounds(obj); + } + + if (obj instanceof Bounds) { + min = obj.min; + max = obj.max; + } else { + min = max = obj; + } + + return (min.x >= this.min.x) && + (max.x <= this.max.x) && + (min.y >= this.min.y) && + (max.y <= this.max.y); + }, + + // @method intersects(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle intersects the given bounds. Two bounds + // intersect if they have at least one point in common. + intersects: function (bounds) { // (Bounds) -> Boolean + bounds = toBounds(bounds); + + var min = this.min, + max = this.max, + min2 = bounds.min, + max2 = bounds.max, + xIntersects = (max2.x >= min.x) && (min2.x <= max.x), + yIntersects = (max2.y >= min.y) && (min2.y <= max.y); + + return xIntersects && yIntersects; + }, + + // @method overlaps(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle overlaps the given bounds. Two bounds + // overlap if their intersection is an area. + overlaps: function (bounds) { // (Bounds) -> Boolean + bounds = toBounds(bounds); + + var min = this.min, + max = this.max, + min2 = bounds.min, + max2 = bounds.max, + xOverlaps = (max2.x > min.x) && (min2.x < max.x), + yOverlaps = (max2.y > min.y) && (min2.y < max.y); + + return xOverlaps && yOverlaps; + }, + + // @method isValid(): Boolean + // Returns `true` if the bounds are properly initialized. + isValid: function () { + return !!(this.min && this.max); + }, + + + // @method pad(bufferRatio: Number): Bounds + // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction. + // For example, a ratio of 0.5 extends the bounds by 50% in each direction. + // Negative values will retract the bounds. + pad: function (bufferRatio) { + var min = this.min, + max = this.max, + heightBuffer = Math.abs(min.x - max.x) * bufferRatio, + widthBuffer = Math.abs(min.y - max.y) * bufferRatio; + + + return toBounds( + toPoint(min.x - heightBuffer, min.y - widthBuffer), + toPoint(max.x + heightBuffer, max.y + widthBuffer)); + }, + + + // @method equals(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle is equivalent to the given bounds. + equals: function (bounds) { + if (!bounds) { return false; } + + bounds = toBounds(bounds); + + return this.min.equals(bounds.getTopLeft()) && + this.max.equals(bounds.getBottomRight()); + }, + }; + + + // @factory L.bounds(corner1: Point, corner2: Point) + // Creates a Bounds object from two corners coordinate pairs. + // @alternative + // @factory L.bounds(points: Point[]) + // Creates a Bounds object from the given array of points. + function toBounds(a, b) { + if (!a || a instanceof Bounds) { + return a; + } + return new Bounds(a, b); + } + + /* + * @class LatLngBounds + * @aka L.LatLngBounds + * + * Represents a rectangular geographical area on a map. + * + * @example + * + * ```js + * var corner1 = L.latLng(40.712, -74.227), + * corner2 = L.latLng(40.774, -74.125), + * bounds = L.latLngBounds(corner1, corner2); + * ``` + * + * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: + * + * ```js + * map.fitBounds([ + * [40.712, -74.227], + * [40.774, -74.125] + * ]); + * ``` + * + * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range. + * + * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object, + * which means new classes can't inherit from it, and new methods + * can't be added to it with the `include` function. + */ + + function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[]) + if (!corner1) { return; } + + var latlngs = corner2 ? [corner1, corner2] : corner1; + + for (var i = 0, len = latlngs.length; i < len; i++) { + this.extend(latlngs[i]); + } + } + + LatLngBounds.prototype = { + + // @method extend(latlng: LatLng): this + // Extend the bounds to contain the given point + + // @alternative + // @method extend(otherBounds: LatLngBounds): this + // Extend the bounds to contain the given bounds + extend: function (obj) { + var sw = this._southWest, + ne = this._northEast, + sw2, ne2; + + if (obj instanceof LatLng) { + sw2 = obj; + ne2 = obj; + + } else if (obj instanceof LatLngBounds) { + sw2 = obj._southWest; + ne2 = obj._northEast; + + if (!sw2 || !ne2) { return this; } + + } else { + return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this; + } + + if (!sw && !ne) { + this._southWest = new LatLng(sw2.lat, sw2.lng); + this._northEast = new LatLng(ne2.lat, ne2.lng); + } else { + sw.lat = Math.min(sw2.lat, sw.lat); + sw.lng = Math.min(sw2.lng, sw.lng); + ne.lat = Math.max(ne2.lat, ne.lat); + ne.lng = Math.max(ne2.lng, ne.lng); + } + + return this; + }, + + // @method pad(bufferRatio: Number): LatLngBounds + // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction. + // For example, a ratio of 0.5 extends the bounds by 50% in each direction. + // Negative values will retract the bounds. + pad: function (bufferRatio) { + var sw = this._southWest, + ne = this._northEast, + heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, + widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; + + return new LatLngBounds( + new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), + new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); + }, + + // @method getCenter(): LatLng + // Returns the center point of the bounds. + getCenter: function () { + return new LatLng( + (this._southWest.lat + this._northEast.lat) / 2, + (this._southWest.lng + this._northEast.lng) / 2); + }, + + // @method getSouthWest(): LatLng + // Returns the south-west point of the bounds. + getSouthWest: function () { + return this._southWest; + }, + + // @method getNorthEast(): LatLng + // Returns the north-east point of the bounds. + getNorthEast: function () { + return this._northEast; + }, + + // @method getNorthWest(): LatLng + // Returns the north-west point of the bounds. + getNorthWest: function () { + return new LatLng(this.getNorth(), this.getWest()); + }, + + // @method getSouthEast(): LatLng + // Returns the south-east point of the bounds. + getSouthEast: function () { + return new LatLng(this.getSouth(), this.getEast()); + }, + + // @method getWest(): Number + // Returns the west longitude of the bounds + getWest: function () { + return this._southWest.lng; + }, + + // @method getSouth(): Number + // Returns the south latitude of the bounds + getSouth: function () { + return this._southWest.lat; + }, + + // @method getEast(): Number + // Returns the east longitude of the bounds + getEast: function () { + return this._northEast.lng; + }, + + // @method getNorth(): Number + // Returns the north latitude of the bounds + getNorth: function () { + return this._northEast.lat; + }, + + // @method contains(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle contains the given one. + + // @alternative + // @method contains (latlng: LatLng): Boolean + // Returns `true` if the rectangle contains the given point. + contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean + if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) { + obj = toLatLng(obj); + } else { + obj = toLatLngBounds(obj); + } + + var sw = this._southWest, + ne = this._northEast, + sw2, ne2; + + if (obj instanceof LatLngBounds) { + sw2 = obj.getSouthWest(); + ne2 = obj.getNorthEast(); + } else { + sw2 = ne2 = obj; + } + + return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && + (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); + }, + + // @method intersects(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common. + intersects: function (bounds) { + bounds = toLatLngBounds(bounds); + + var sw = this._southWest, + ne = this._northEast, + sw2 = bounds.getSouthWest(), + ne2 = bounds.getNorthEast(), + + latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), + lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); + + return latIntersects && lngIntersects; + }, + + // @method overlaps(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area. + overlaps: function (bounds) { + bounds = toLatLngBounds(bounds); + + var sw = this._southWest, + ne = this._northEast, + sw2 = bounds.getSouthWest(), + ne2 = bounds.getNorthEast(), + + latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat), + lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng); + + return latOverlaps && lngOverlaps; + }, + + // @method toBBoxString(): String + // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data. + toBBoxString: function () { + return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); + }, + + // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean + // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number. + equals: function (bounds, maxMargin) { + if (!bounds) { return false; } + + bounds = toLatLngBounds(bounds); + + return this._southWest.equals(bounds.getSouthWest(), maxMargin) && + this._northEast.equals(bounds.getNorthEast(), maxMargin); + }, + + // @method isValid(): Boolean + // Returns `true` if the bounds are properly initialized. + isValid: function () { + return !!(this._southWest && this._northEast); + } + }; + + // TODO International date line? + + // @factory L.latLngBounds(corner1: LatLng, corner2: LatLng) + // Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle. + + // @alternative + // @factory L.latLngBounds(latlngs: LatLng[]) + // Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds). + function toLatLngBounds(a, b) { + if (a instanceof LatLngBounds) { + return a; + } + return new LatLngBounds(a, b); + } + + /* @class LatLng + * @aka L.LatLng + * + * Represents a geographical point with a certain latitude and longitude. + * + * @example + * + * ``` + * var latlng = L.latLng(50.5, 30.5); + * ``` + * + * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent: + * + * ``` + * map.panTo([50, 30]); + * map.panTo({lon: 30, lat: 50}); + * map.panTo({lat: 50, lng: 30}); + * map.panTo(L.latLng(50, 30)); + * ``` + * + * Note that `LatLng` does not inherit from Leaflet's `Class` object, + * which means new classes can't inherit from it, and new methods + * can't be added to it with the `include` function. + */ + + function LatLng(lat, lng, alt) { + if (isNaN(lat) || isNaN(lng)) { + throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); + } + + // @property lat: Number + // Latitude in degrees + this.lat = +lat; + + // @property lng: Number + // Longitude in degrees + this.lng = +lng; + + // @property alt: Number + // Altitude in meters (optional) + if (alt !== undefined) { + this.alt = +alt; + } + } + + LatLng.prototype = { + // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean + // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number. + equals: function (obj, maxMargin) { + if (!obj) { return false; } + + obj = toLatLng(obj); + + var margin = Math.max( + Math.abs(this.lat - obj.lat), + Math.abs(this.lng - obj.lng)); + + return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); + }, + + // @method toString(): String + // Returns a string representation of the point (for debugging purposes). + toString: function (precision) { + return 'LatLng(' + + formatNum(this.lat, precision) + ', ' + + formatNum(this.lng, precision) + ')'; + }, + + // @method distanceTo(otherLatLng: LatLng): Number + // Returns the distance (in meters) to the given `LatLng` calculated using the [Spherical Law of Cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines). + distanceTo: function (other) { + return Earth.distance(this, toLatLng(other)); + }, + + // @method wrap(): LatLng + // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees. + wrap: function () { + return Earth.wrapLatLng(this); + }, + + // @method toBounds(sizeInMeters: Number): LatLngBounds + // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`. + toBounds: function (sizeInMeters) { + var latAccuracy = 180 * sizeInMeters / 40075017, + lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); + + return toLatLngBounds( + [this.lat - latAccuracy, this.lng - lngAccuracy], + [this.lat + latAccuracy, this.lng + lngAccuracy]); + }, + + clone: function () { + return new LatLng(this.lat, this.lng, this.alt); + } + }; + + + + // @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng + // Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude). + + // @alternative + // @factory L.latLng(coords: Array): LatLng + // Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead. + + // @alternative + // @factory L.latLng(coords: Object): LatLng + // Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead. + + function toLatLng(a, b, c) { + if (a instanceof LatLng) { + return a; + } + if (isArray(a) && typeof a[0] !== 'object') { + if (a.length === 3) { + return new LatLng(a[0], a[1], a[2]); + } + if (a.length === 2) { + return new LatLng(a[0], a[1]); + } + return null; + } + if (a === undefined || a === null) { + return a; + } + if (typeof a === 'object' && 'lat' in a) { + return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); + } + if (b === undefined) { + return null; + } + return new LatLng(a, b, c); + } + + /* + * @namespace CRS + * @crs L.CRS.Base + * Object that defines coordinate reference systems for projecting + * geographical points into pixel (screen) coordinates and back (and to + * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See + * [spatial reference system](https://en.wikipedia.org/wiki/Spatial_reference_system). + * + * Leaflet defines the most usual CRSs by default. If you want to use a + * CRS not defined by default, take a look at the + * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin. + * + * Note that the CRS instances do not inherit from Leaflet's `Class` object, + * and can't be instantiated. Also, new classes can't inherit from them, + * and methods can't be added to them with the `include` function. + */ + + var CRS = { + // @method latLngToPoint(latlng: LatLng, zoom: Number): Point + // Projects geographical coordinates into pixel coordinates for a given zoom. + latLngToPoint: function (latlng, zoom) { + var projectedPoint = this.projection.project(latlng), + scale = this.scale(zoom); + + return this.transformation._transform(projectedPoint, scale); + }, + + // @method pointToLatLng(point: Point, zoom: Number): LatLng + // The inverse of `latLngToPoint`. Projects pixel coordinates on a given + // zoom into geographical coordinates. + pointToLatLng: function (point, zoom) { + var scale = this.scale(zoom), + untransformedPoint = this.transformation.untransform(point, scale); + + return this.projection.unproject(untransformedPoint); + }, + + // @method project(latlng: LatLng): Point + // Projects geographical coordinates into coordinates in units accepted for + // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services). + project: function (latlng) { + return this.projection.project(latlng); + }, + + // @method unproject(point: Point): LatLng + // Given a projected coordinate returns the corresponding LatLng. + // The inverse of `project`. + unproject: function (point) { + return this.projection.unproject(point); + }, + + // @method scale(zoom: Number): Number + // Returns the scale used when transforming projected coordinates into + // pixel coordinates for a particular zoom. For example, it returns + // `256 * 2^zoom` for Mercator-based CRS. + scale: function (zoom) { + return 256 * Math.pow(2, zoom); + }, + + // @method zoom(scale: Number): Number + // Inverse of `scale()`, returns the zoom level corresponding to a scale + // factor of `scale`. + zoom: function (scale) { + return Math.log(scale / 256) / Math.LN2; + }, + + // @method getProjectedBounds(zoom: Number): Bounds + // Returns the projection's bounds scaled and transformed for the provided `zoom`. + getProjectedBounds: function (zoom) { + if (this.infinite) { return null; } + + var b = this.projection.bounds, + s = this.scale(zoom), + min = this.transformation.transform(b.min, s), + max = this.transformation.transform(b.max, s); + + return new Bounds(min, max); + }, + + // @method distance(latlng1: LatLng, latlng2: LatLng): Number + // Returns the distance between two geographical coordinates. + + // @property code: String + // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`) + // + // @property wrapLng: Number[] + // An array of two numbers defining whether the longitude (horizontal) coordinate + // axis wraps around a given range and how. Defaults to `[-180, 180]` in most + // geographical CRSs. If `undefined`, the longitude axis does not wrap around. + // + // @property wrapLat: Number[] + // Like `wrapLng`, but for the latitude (vertical) axis. + + // wrapLng: [min, max], + // wrapLat: [min, max], + + // @property infinite: Boolean + // If true, the coordinate space will be unbounded (infinite in both axes) + infinite: false, + + // @method wrapLatLng(latlng: LatLng): LatLng + // Returns a `LatLng` where lat and lng has been wrapped according to the + // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds. + wrapLatLng: function (latlng) { + var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, + lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat, + alt = latlng.alt; + + return new LatLng(lat, lng, alt); + }, + + // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds + // Returns a `LatLngBounds` with the same size as the given one, ensuring + // that its center is within the CRS's bounds. + // Only accepts actual `L.LatLngBounds` instances, not arrays. + wrapLatLngBounds: function (bounds) { + var center = bounds.getCenter(), + newCenter = this.wrapLatLng(center), + latShift = center.lat - newCenter.lat, + lngShift = center.lng - newCenter.lng; + + if (latShift === 0 && lngShift === 0) { + return bounds; + } + + var sw = bounds.getSouthWest(), + ne = bounds.getNorthEast(), + newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift), + newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift); + + return new LatLngBounds(newSw, newNe); + } + }; + + /* + * @namespace CRS + * @crs L.CRS.Earth + * + * Serves as the base for CRS that are global such that they cover the earth. + * Can only be used as the base for other CRS and cannot be used directly, + * since it does not have a `code`, `projection` or `transformation`. `distance()` returns + * meters. + */ + + var Earth = extend({}, CRS, { + wrapLng: [-180, 180], + + // Mean Earth Radius, as recommended for use by + // the International Union of Geodesy and Geophysics, + // see https://rosettacode.org/wiki/Haversine_formula + R: 6371000, + + // distance between two geographical points using spherical law of cosines approximation + distance: function (latlng1, latlng2) { + var rad = Math.PI / 180, + lat1 = latlng1.lat * rad, + lat2 = latlng2.lat * rad, + sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2), + sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2), + a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon, + c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return this.R * c; + } + }); + + /* + * @namespace Projection + * @projection L.Projection.SphericalMercator + * + * Spherical Mercator projection — the most common projection for online maps, + * used by almost all free and commercial tile providers. Assumes that Earth is + * a sphere. Used by the `EPSG:3857` CRS. + */ + + var earthRadius = 6378137; + + var SphericalMercator = { + + R: earthRadius, + MAX_LATITUDE: 85.0511287798, + + project: function (latlng) { + var d = Math.PI / 180, + max = this.MAX_LATITUDE, + lat = Math.max(Math.min(max, latlng.lat), -max), + sin = Math.sin(lat * d); + + return new Point( + this.R * latlng.lng * d, + this.R * Math.log((1 + sin) / (1 - sin)) / 2); + }, + + unproject: function (point) { + var d = 180 / Math.PI; + + return new LatLng( + (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, + point.x * d / this.R); + }, + + bounds: (function () { + var d = earthRadius * Math.PI; + return new Bounds([-d, -d], [d, d]); + })() + }; + + /* + * @class Transformation + * @aka L.Transformation + * + * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d` + * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing + * the reverse. Used by Leaflet in its projections code. + * + * @example + * + * ```js + * var transformation = L.transformation(2, 5, -1, 10), + * p = L.point(1, 2), + * p2 = transformation.transform(p), // L.point(7, 8) + * p3 = transformation.untransform(p2); // L.point(1, 2) + * ``` + */ + + + // factory new L.Transformation(a: Number, b: Number, c: Number, d: Number) + // Creates a `Transformation` object with the given coefficients. + function Transformation(a, b, c, d) { + if (isArray(a)) { + // use array properties + this._a = a[0]; + this._b = a[1]; + this._c = a[2]; + this._d = a[3]; + return; + } + this._a = a; + this._b = b; + this._c = c; + this._d = d; + } + + Transformation.prototype = { + // @method transform(point: Point, scale?: Number): Point + // Returns a transformed point, optionally multiplied by the given scale. + // Only accepts actual `L.Point` instances, not arrays. + transform: function (point, scale) { // (Point, Number) -> Point + return this._transform(point.clone(), scale); + }, + + // destructive transform (faster) + _transform: function (point, scale) { + scale = scale || 1; + point.x = scale * (this._a * point.x + this._b); + point.y = scale * (this._c * point.y + this._d); + return point; + }, + + // @method untransform(point: Point, scale?: Number): Point + // Returns the reverse transformation of the given point, optionally divided + // by the given scale. Only accepts actual `L.Point` instances, not arrays. + untransform: function (point, scale) { + scale = scale || 1; + return new Point( + (point.x / scale - this._b) / this._a, + (point.y / scale - this._d) / this._c); + } + }; + + // factory L.transformation(a: Number, b: Number, c: Number, d: Number) + + // @factory L.transformation(a: Number, b: Number, c: Number, d: Number) + // Instantiates a Transformation object with the given coefficients. + + // @alternative + // @factory L.transformation(coefficients: Array): Transformation + // Expects an coefficients array of the form + // `[a: Number, b: Number, c: Number, d: Number]`. + + function toTransformation(a, b, c, d) { + return new Transformation(a, b, c, d); + } + + /* + * @namespace CRS + * @crs L.CRS.EPSG3857 + * + * The most common CRS for online maps, used by almost all free and commercial + * tile providers. Uses Spherical Mercator projection. Set in by default in + * Map's `crs` option. + */ + + var EPSG3857 = extend({}, Earth, { + code: 'EPSG:3857', + projection: SphericalMercator, + + transformation: (function () { + var scale = 0.5 / (Math.PI * SphericalMercator.R); + return toTransformation(scale, 0.5, -scale, 0.5); + }()) + }); + + var EPSG900913 = extend({}, EPSG3857, { + code: 'EPSG:900913' + }); + + // @namespace SVG; @section + // There are several static functions which can be called without instantiating L.SVG: + + // @function create(name: String): SVGElement + // Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement), + // corresponding to the class name passed. For example, using 'line' will return + // an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement). + function svgCreate(name) { + return document.createElementNS('http://www.w3.org/2000/svg', name); + } + + // @function pointsToPath(rings: Point[], closed: Boolean): String + // Generates a SVG path string for multiple rings, with each ring turning + // into "M..L..L.." instructions + function pointsToPath(rings, closed) { + var str = '', + i, j, len, len2, points, p; + + for (i = 0, len = rings.length; i < len; i++) { + points = rings[i]; + + for (j = 0, len2 = points.length; j < len2; j++) { + p = points[j]; + str += (j ? 'L' : 'M') + p.x + ' ' + p.y; + } + + // closes the ring for polygons; "x" is VML syntax + str += closed ? (Browser.svg ? 'z' : 'x') : ''; + } + + // SVG complains about empty path strings + return str || 'M0 0'; + } + + /* + * @namespace Browser + * @aka L.Browser + * + * A namespace with static properties for browser/feature detection used by Leaflet internally. + * + * @example + * + * ```js + * if (L.Browser.ielt9) { + * alert('Upgrade your browser, dude!'); + * } + * ``` + */ + + var style = document.documentElement.style; + + // @property ie: Boolean; `true` for all Internet Explorer versions (not Edge). + var ie = 'ActiveXObject' in window; + + // @property ielt9: Boolean; `true` for Internet Explorer versions less than 9. + var ielt9 = ie && !document.addEventListener; + + // @property edge: Boolean; `true` for the Edge web browser. + var edge = 'msLaunchUri' in navigator && !('documentMode' in document); + + // @property webkit: Boolean; + // `true` for webkit-based browsers like Chrome and Safari (including mobile versions). + var webkit = userAgentContains('webkit'); + + // @property android: Boolean + // **Deprecated.** `true` for any browser running on an Android platform. + var android = userAgentContains('android'); + + // @property android23: Boolean; **Deprecated.** `true` for browsers running on Android 2 or Android 3. + var android23 = userAgentContains('android 2') || userAgentContains('android 3'); + + /* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */ + var webkitVer = parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit + // @property androidStock: Boolean; **Deprecated.** `true` for the Android stock browser (i.e. not Chrome) + var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window); + + // @property opera: Boolean; `true` for the Opera browser + var opera = !!window.opera; + + // @property chrome: Boolean; `true` for the Chrome browser. + var chrome = !edge && userAgentContains('chrome'); + + // @property gecko: Boolean; `true` for gecko-based browsers like Firefox. + var gecko = userAgentContains('gecko') && !webkit && !opera && !ie; + + // @property safari: Boolean; `true` for the Safari browser. + var safari = !chrome && userAgentContains('safari'); + + var phantom = userAgentContains('phantom'); + + // @property opera12: Boolean + // `true` for the Opera browser supporting CSS transforms (version 12 or later). + var opera12 = 'OTransition' in style; + + // @property win: Boolean; `true` when the browser is running in a Windows platform + var win = navigator.platform.indexOf('Win') === 0; + + // @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms. + var ie3d = ie && ('transition' in style); + + // @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms. + var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23; + + // @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms. + var gecko3d = 'MozPerspective' in style; + + // @property any3d: Boolean + // `true` for all browsers supporting CSS transforms. + var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom; + + // @property mobile: Boolean; `true` for all browsers running in a mobile device. + var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile'); + + // @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device. + var mobileWebkit = mobile && webkit; + + // @property mobileWebkit3d: Boolean + // `true` for all webkit-based browsers in a mobile device supporting CSS transforms. + var mobileWebkit3d = mobile && webkit3d; + + // @property msPointer: Boolean + // `true` for browsers implementing the Microsoft touch events model (notably IE10). + var msPointer = !window.PointerEvent && window.MSPointerEvent; + + // @property pointer: Boolean + // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx). + var pointer = !!(window.PointerEvent || msPointer); + + // @property touchNative: Boolean + // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events). + // **This does not necessarily mean** that the browser is running in a computer with + // a touchscreen, it only means that the browser is capable of understanding + // touch events. + var touchNative = 'ontouchstart' in window || !!window.TouchEvent; + + // @property touch: Boolean + // `true` for all browsers supporting either [touch](#browser-touch) or [pointer](#browser-pointer) events. + // Note: pointer events will be preferred (if available), and processed for all `touch*` listeners. + var touch = !window.L_NO_TOUCH && (touchNative || pointer); + + // @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device. + var mobileOpera = mobile && opera; + + // @property mobileGecko: Boolean + // `true` for gecko-based browsers running in a mobile device. + var mobileGecko = mobile && gecko; + + // @property retina: Boolean + // `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%. + var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1; + + // @property passiveEvents: Boolean + // `true` for browsers that support passive events. + var passiveEvents = (function () { + var supportsPassiveOption = false; + try { + var opts = Object.defineProperty({}, 'passive', { + get: function () { // eslint-disable-line getter-return + supportsPassiveOption = true; + } + }); + window.addEventListener('testPassiveEventSupport', falseFn, opts); + window.removeEventListener('testPassiveEventSupport', falseFn, opts); + } catch (e) { + // Errors can safely be ignored since this is only a browser support test. + } + return supportsPassiveOption; + }()); + + // @property canvas: Boolean + // `true` when the browser supports [``](https://developer.mozilla.org/docs/Web/API/Canvas_API). + var canvas$1 = (function () { + return !!document.createElement('canvas').getContext; + }()); + + // @property svg: Boolean + // `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG). + var svg$1 = !!(document.createElementNS && svgCreate('svg').createSVGRect); + + var inlineSvg = !!svg$1 && (function () { + var div = document.createElement('div'); + div.innerHTML = ''; + return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg'; + })(); + + // @property vml: Boolean + // `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language). + var vml = !svg$1 && (function () { + try { + var div = document.createElement('div'); + div.innerHTML = ''; + + var shape = div.firstChild; + shape.style.behavior = 'url(#default#VML)'; + + return shape && (typeof shape.adj === 'object'); + + } catch (e) { + return false; + } + }()); + + + // @property mac: Boolean; `true` when the browser is running in a Mac platform + var mac = navigator.platform.indexOf('Mac') === 0; + + // @property mac: Boolean; `true` when the browser is running in a Linux platform + var linux = navigator.platform.indexOf('Linux') === 0; + + function userAgentContains(str) { + return navigator.userAgent.toLowerCase().indexOf(str) >= 0; + } + + + var Browser = { + ie: ie, + ielt9: ielt9, + edge: edge, + webkit: webkit, + android: android, + android23: android23, + androidStock: androidStock, + opera: opera, + chrome: chrome, + gecko: gecko, + safari: safari, + phantom: phantom, + opera12: opera12, + win: win, + ie3d: ie3d, + webkit3d: webkit3d, + gecko3d: gecko3d, + any3d: any3d, + mobile: mobile, + mobileWebkit: mobileWebkit, + mobileWebkit3d: mobileWebkit3d, + msPointer: msPointer, + pointer: pointer, + touch: touch, + touchNative: touchNative, + mobileOpera: mobileOpera, + mobileGecko: mobileGecko, + retina: retina, + passiveEvents: passiveEvents, + canvas: canvas$1, + svg: svg$1, + vml: vml, + inlineSvg: inlineSvg, + mac: mac, + linux: linux + }; + + /* + * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. + */ + + var POINTER_DOWN = Browser.msPointer ? 'MSPointerDown' : 'pointerdown'; + var POINTER_MOVE = Browser.msPointer ? 'MSPointerMove' : 'pointermove'; + var POINTER_UP = Browser.msPointer ? 'MSPointerUp' : 'pointerup'; + var POINTER_CANCEL = Browser.msPointer ? 'MSPointerCancel' : 'pointercancel'; + var pEvent = { + touchstart : POINTER_DOWN, + touchmove : POINTER_MOVE, + touchend : POINTER_UP, + touchcancel : POINTER_CANCEL + }; + var handle = { + touchstart : _onPointerStart, + touchmove : _handlePointer, + touchend : _handlePointer, + touchcancel : _handlePointer + }; + var _pointers = {}; + var _pointerDocListener = false; + + // Provides a touch events wrapper for (ms)pointer events. + // ref https://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 + + function addPointerListener(obj, type, handler) { + if (type === 'touchstart') { + _addPointerDocListener(); + } + if (!handle[type]) { + console.warn('wrong event specified:', type); + return falseFn; + } + handler = handle[type].bind(this, handler); + obj.addEventListener(pEvent[type], handler, false); + return handler; + } + + function removePointerListener(obj, type, handler) { + if (!pEvent[type]) { + console.warn('wrong event specified:', type); + return; + } + obj.removeEventListener(pEvent[type], handler, false); + } + + function _globalPointerDown(e) { + _pointers[e.pointerId] = e; + } + + function _globalPointerMove(e) { + if (_pointers[e.pointerId]) { + _pointers[e.pointerId] = e; + } + } + + function _globalPointerUp(e) { + delete _pointers[e.pointerId]; + } + + function _addPointerDocListener() { + // need to keep track of what pointers and how many are active to provide e.touches emulation + if (!_pointerDocListener) { + // we listen document as any drags that end by moving the touch off the screen get fired there + document.addEventListener(POINTER_DOWN, _globalPointerDown, true); + document.addEventListener(POINTER_MOVE, _globalPointerMove, true); + document.addEventListener(POINTER_UP, _globalPointerUp, true); + document.addEventListener(POINTER_CANCEL, _globalPointerUp, true); + + _pointerDocListener = true; + } + } + + function _handlePointer(handler, e) { + if (e.pointerType === (e.MSPOINTER_TYPE_MOUSE || 'mouse')) { return; } + + e.touches = []; + for (var i in _pointers) { + e.touches.push(_pointers[i]); + } + e.changedTouches = [e]; + + handler(e); + } + + function _onPointerStart(handler, e) { + // IE10 specific: MsTouch needs preventDefault. See #2000 + if (e.MSPOINTER_TYPE_TOUCH && e.pointerType === e.MSPOINTER_TYPE_TOUCH) { + preventDefault(e); + } + _handlePointer(handler, e); + } + + /* + * Extends the event handling code with double tap support for mobile browsers. + * + * Note: currently most browsers fire native dblclick, with only a few exceptions + * (see https://github.com/Leaflet/Leaflet/issues/7012#issuecomment-595087386) + */ + + function makeDblclick(event) { + // in modern browsers `type` cannot be just overridden: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only + var newEvent = {}, + prop, i; + for (i in event) { + prop = event[i]; + newEvent[i] = prop && prop.bind ? prop.bind(event) : prop; + } + event = newEvent; + newEvent.type = 'dblclick'; + newEvent.detail = 2; + newEvent.isTrusted = false; + newEvent._simulated = true; // for debug purposes + return newEvent; + } + + var delay = 200; + function addDoubleTapListener(obj, handler) { + // Most browsers handle double tap natively + obj.addEventListener('dblclick', handler); + + // On some platforms the browser doesn't fire native dblclicks for touch events. + // It seems that in all such cases `detail` property of `click` event is always `1`. + // So here we rely on that fact to avoid excessive 'dblclick' simulation when not needed. + var last = 0, + detail; + function simDblclick(e) { + if (e.detail !== 1) { + detail = e.detail; // keep in sync to avoid false dblclick in some cases + return; + } + + if (e.pointerType === 'mouse' || + (e.sourceCapabilities && !e.sourceCapabilities.firesTouchEvents)) { + + return; + } + + // When clicking on an , the browser generates a click on its + //