diff --git a/static/build/artifact/PR790/IITC_Mobile-test.apk b/static/build/artifact/PR790/IITC_Mobile-test.apk
new file mode 100644
index 0000000000..f794847dda
Binary files /dev/null and b/static/build/artifact/PR790/IITC_Mobile-test.apk differ
diff --git a/static/build/artifact/PR790/meta.json b/static/build/artifact/PR790/meta.json
new file mode 100644
index 0000000000..a5c3e7622c
--- /dev/null
+++ b/static/build/artifact/PR790/meta.json
@@ -0,0 +1,788 @@
+{
+ "categories": {
+ "Cache": {
+ "name": "Cache",
+ "name:ru": "Кэш",
+ "description": "Data caching to prevent reloading",
+ "description:ru": "Кэширование данных для предотвращения повторной загрузки",
+ "plugins": [
+ {
+ "filename": "cache-portals-on-map.user.js",
+ "id": "cache-portals-on-map",
+ "name": "Cache viewed portals on map",
+ "author": "jonatkins",
+ "description": "Cache the details of recently viewed portals and use this to populate the map when possible",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/cache-portals-on-map.svg"
+ }
+ ]
+ },
+ "Controls": {
+ "name": "Controls",
+ "name:ru": "Управление",
+ "description": "Map controls/widgets",
+ "description:ru": "Виджеты для управления картой",
+ "plugins": [
+ {
+ "filename": "pan-control.user.js",
+ "id": "pan-control",
+ "name": "Pan control",
+ "author": "fragger",
+ "description": "Show a panning control on the map.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/pan-control.svg"
+ },
+ {
+ "filename": "minimap.user.js",
+ "id": "minimap",
+ "name": "Mini map",
+ "author": "johnd0e",
+ "description": "Show a mini map on the corner of the map.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.4.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/minimap.svg"
+ },
+ {
+ "filename": "scale-bar.user.js",
+ "id": "scale-bar",
+ "name": "Scale bar",
+ "author": "breunigs",
+ "description": "Show scale bar on the map.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/scale-bar.svg"
+ },
+ {
+ "filename": "zoom-slider.user.js",
+ "id": "zoom-slider",
+ "name": "Zoom slider",
+ "author": "fragger",
+ "description": "Show a zoom slider on the map instead of the zoom buttons.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/zoom-slider.svg"
+ },
+ {
+ "filename": "multi-projects-extension.user.js",
+ "id": "multi-projects-extension",
+ "name": "Multi Projects Extension",
+ "author": "ZasoGD",
+ "description": "Create separated projects in some plugins.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/multi-projects-extension.svg"
+ },
+ {
+ "filename": "bookmarks.user.js",
+ "id": "bookmarks",
+ "name": "Bookmarks for maps and portals",
+ "author": "ZasoGD",
+ "description": "Save your favorite Maps and Portals and move the intel map with a click. Works with sync. Supports Multi-Project-Extension",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.4.6.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/bookmarks.svg"
+ }
+ ]
+ },
+ "Draw": {
+ "name": "Draw",
+ "name:ru": "Рисование",
+ "description": "Allow drawing things onto the current map so you may plan your next move",
+ "description:ru": "Позволяет рисовать на текущей карте, чтобы вы могли спланировать свой следующий шаг",
+ "plugins": [
+ {
+ "filename": "done-links.user.js",
+ "id": "done-links",
+ "name": "Done links",
+ "author": "jonatkins",
+ "description": "A companion to the Cross Links plugin. Highlights any links that match existing draw-tools line/polygon edges",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/done-links.svg"
+ },
+ {
+ "filename": "fly-links.user.js",
+ "id": "fly-links",
+ "name": "Fly Links",
+ "author": "Fly33",
+ "description": "Calculate how to link the portals to create the largest tidy set of nested fields. Enable from the layer chooser.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.5.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/fly-links.svg"
+ },
+ {
+ "filename": "draw-tools.user.js",
+ "id": "draw-tools",
+ "name": "Draw tools",
+ "author": "breunigs",
+ "description": "Allow drawing things onto the current map so you may plan your next move. Supports Multi-Project-Extension.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.10.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/draw-tools.svg"
+ },
+ {
+ "filename": "cross-links.user.js",
+ "id": "cross-links",
+ "name": "Cross links",
+ "author": "mcben",
+ "description": "Checks for existing links that cross planned links. Requires draw-tools plugin.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "1.3.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/cross-links.svg"
+ },
+ {
+ "filename": "tidy-links.user.js",
+ "id": "tidy-links",
+ "name": "Tidy Links",
+ "author": "boombuler",
+ "description": "Calculate how to link the portals to create a reasonably tidy set of links/fields. Enable from the layer chooser. (former `Max Links`)",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.6.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/tidy-links.svg"
+ }
+ ]
+ },
+ "Highlighter": {
+ "name": "Highlighter",
+ "name:ru": "Подсветка",
+ "description": "Portal highlighters",
+ "description:ru": "Подсветка порталов",
+ "plugins": [
+ {
+ "filename": "highlight-portals-my-level.user.js",
+ "id": "highlight-portals-my-level",
+ "name": "Highlight portals by my level",
+ "author": "vita10gy",
+ "description": "Use the portal fill color to denote if the portal is either at and above, or at and below your level.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-portals-my-level.svg"
+ },
+ {
+ "filename": "highlight-needs-recharge.user.js",
+ "id": "highlight-needs-recharge",
+ "name": "Highlight portals that need recharging",
+ "author": "vita10gy",
+ "description": "Use the portal fill color to denote if the portal needs recharging and how much. Yellow: above 85%. Orange: above 70%. Red: above 15%. Magenta: below 15%.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-needs-recharge.svg"
+ },
+ {
+ "filename": "highlight-ornaments.user.js",
+ "id": "highlight-ornaments",
+ "name": "Highlight portals with ornaments",
+ "author": "jonatkins",
+ "description": "Use the portal fill color to denote portals with additional 'ornament' markers. e.g. Anomaly portals",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-ornaments.svg"
+ },
+ {
+ "filename": "highlight-hide-team.user.js",
+ "id": "highlight-hide-team",
+ "name": "Hide portal ownership",
+ "author": "vita10gy",
+ "description": "Show all portals as neutral, as if uncaptured. Great for creating plans.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-hide-team.svg"
+ },
+ {
+ "filename": "highlight-portal-history.user.js",
+ "id": "highlight-portal-history",
+ "name": "Highlight portals based on history",
+ "author": "Johtaja",
+ "description": "Use the portal fill color to denote the portal has been visited, captured, scout controlled",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-portal-history.svg"
+ },
+ {
+ "filename": "highlight-high-level.user.js",
+ "id": "highlight-high-level",
+ "name": "Highlight high level portals",
+ "author": "jonatkins",
+ "description": "Use the portal fill color to denote high level portals: Purple L8, Red L7, Orange L6",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-high-level.svg"
+ },
+ {
+ "filename": "highlight-level-color.user.js",
+ "id": "highlight-level-color",
+ "name": "Highlight portals by level color",
+ "author": "vita10gy",
+ "description": "Use the portal fill color to denote the portal level by using the game level colors.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-level-color.svg"
+ },
+ {
+ "filename": "highlight-weakness.user.js",
+ "id": "highlight-weakness",
+ "name": "Highlight portal weakness",
+ "author": "vita10gy",
+ "description": "Use the fill color of the portals to denote if the portal is weak. Stronger red indicates recharge required, missing resonators, or both.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.8.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-weakness.svg"
+ },
+ {
+ "filename": "highlight-forgotten.user.js",
+ "id": "highlight-forgotten",
+ "name": "Highlight inactive portals",
+ "author": "jonatkins",
+ "description": "Use the portal fill color to denote if the portal is unclaimed with no recent activity. Shades of red from one week to one month, then tinted to purple for longer. May also highlight captured portals that are stuck and fail to decay every 24 hours.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-forgotten.svg"
+ },
+ {
+ "filename": "highlight-moved-portals.user.js",
+ "id": "highlight-moved-portals",
+ "name": "Highlight moved portals",
+ "author": "screach",
+ "description": "Highlights portals with links with different location data",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.1.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-moved-portals.svg"
+ },
+ {
+ "filename": "highlight-missing-resonators.user.js",
+ "id": "highlight-missing-resonators",
+ "name": "Highlight portals missing resonators",
+ "author": "vita10gy",
+ "description": "Use the portal fill color to denote if the portal is missing resonators.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/highlight-missing-resonators.svg"
+ }
+ ]
+ },
+ "Info": {
+ "name": "Info",
+ "name:ru": "Информация",
+ "description": "Display additional information",
+ "description:ru": "Отображение дополнительной информации",
+ "plugins": [
+ {
+ "filename": "portal-counts.user.js",
+ "id": "portal-counts",
+ "name": "Portal count",
+ "author": "yenky",
+ "description": "Display a list of all localized portals by level and faction.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.6.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/portal-counts.svg"
+ },
+ {
+ "filename": "scoreboard.user.js",
+ "id": "scoreboard",
+ "name": "Localized scoreboard",
+ "author": "Costaspap",
+ "description": "Display a scoreboard about all visible portals with statistics about both teams,like average portal level,link & field counts etc.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.4.1.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/scoreboard.svg"
+ },
+ {
+ "filename": "player-level-guess.user.js",
+ "id": "player-level-guess",
+ "name": "Player level guess",
+ "author": "breunigs",
+ "description": "Try to determine player levels from the data available in the current view.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.5.11.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/player-level-guess.svg"
+ },
+ {
+ "filename": "portals-list.user.js",
+ "id": "portals-list",
+ "name": "Portals list",
+ "author": "teo96",
+ "description": "Display a sortable list of all visible portals with full details about the team, resonators, links, etc.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.4.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/portals-list.svg"
+ },
+ {
+ "filename": "missions.user.js",
+ "id": "missions",
+ "name": "Missions",
+ "author": "jonatkins",
+ "description": "View missions. Marking progress on waypoints/missions basis. Showing mission paths on the map.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/missions.svg"
+ },
+ {
+ "filename": "ap-stats.user.js",
+ "id": "ap-stats",
+ "name": "Available AP statistics",
+ "author": "Hollow011",
+ "description": "Displays the per-team AP gains available in the current view.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.4.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/ap-stats.svg"
+ },
+ {
+ "filename": "layer-count.user.js",
+ "id": "layer-count",
+ "name": "Layer count",
+ "author": "fkloft",
+ "description": "Allow users to count nested fields",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/layer-count.svg"
+ },
+ {
+ "filename": "score-cycle-times.user.js",
+ "id": "score-cycle-times",
+ "name": "Scoring cycle / checkpoint times",
+ "author": "jonatkins",
+ "description": "Show the times used for the septicycle and checkpoints for regional scoreboards.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/score-cycle-times.svg"
+ }
+ ]
+ },
+ "Keys": {
+ "name": "Keys",
+ "name:ru": "Ключи",
+ "description": "Manual key management",
+ "description:ru": "Ручное управление ключами"
+ },
+ "Layer": {
+ "name": "Layer",
+ "name:ru": "Слои",
+ "description": "Additional map layers",
+ "description:ru": "Дополнительные слои карт",
+ "plugins": [
+ {
+ "filename": "farms-find.user.js",
+ "id": "farms-find",
+ "name": "Find farms on map",
+ "author": "949",
+ "description": "Show farms by minimum level",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "1.4.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/farms-find.svg"
+ },
+ {
+ "filename": "overlay-kml.user.js",
+ "id": "overlay-kml",
+ "name": "Overlay KML / GPX / GeoJSON",
+ "author": "danielatkins",
+ "description": "Allow users to overlay their own KML / GPX / GeoJSON files on top of IITC.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/overlay-kml.svg"
+ },
+ {
+ "filename": "regions.user.js",
+ "id": "regions",
+ "name": "Ingress scoring regions",
+ "author": "jonatkins",
+ "description": "Show the regional scoring cells grid on the map",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/regions.svg"
+ },
+ {
+ "filename": "portal-level-numbers.user.js",
+ "id": "portal-level-numbers",
+ "name": "Portal Level Numbers",
+ "author": "rongou",
+ "description": "Show portal level numbers on map.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/portal-level-numbers.svg"
+ },
+ {
+ "filename": "machina-tracker.user.js",
+ "id": "machina-tracker",
+ "name": "Machina tracker",
+ "author": "McBen",
+ "description": "Show locations of Machina activities",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "1.1.0.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/machina-tracker.svg"
+ },
+ {
+ "filename": "portal-names.user.js",
+ "id": "portal-names",
+ "name": "Portal Names",
+ "author": "ZasoGD",
+ "description": "Show portal names on the map.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/portal-names.svg"
+ },
+ {
+ "filename": "ornament-icons.user.js",
+ "id": "ornament-icons",
+ "name": "Ornament icons basic",
+ "author": "johtata",
+ "description": "Add own icons and names for ornaments",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/ornament-icons.svg"
+ },
+ {
+ "filename": "keys-on-map.user.js",
+ "id": "keys-on-map",
+ "name": "Keys on map",
+ "author": "xelio",
+ "description": "Show the manually entered key counts from the 'keys' plugin on the map.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/keys-on-map.svg"
+ },
+ {
+ "filename": "remove-extra-layers.user.js",
+ "id": "remove-extra-layers",
+ "name": "Remove extra layers",
+ "author": "johnd0e",
+ "description": "Remove 'Artifacts', 'Beacons' and 'Frackers' from layerChooser (still keeping them on map)",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/remove-extra-layers.svg"
+ },
+ {
+ "filename": "wayfarer-range.user.js",
+ "id": "wayfarer-range",
+ "name": "Wayfarer portal submission range",
+ "author": "morph",
+ "description": "Add a 20m range around portals, to aid Wayfarer portals submissions",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.0.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/wayfarer-range.svg"
+ },
+ {
+ "filename": "hide-portal-levels.user.js",
+ "id": "hide-portal-levels",
+ "name": "Hide portal levels",
+ "author": "johnd0e",
+ "description": "Replace all levels with single layerChooser's entry; reverting on longclick",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/hide-portal-levels.svg"
+ },
+ {
+ "filename": "player-activity-tracker.user.js",
+ "id": "player-activity-tracker",
+ "name": "Player activity tracker",
+ "author": "breunigs",
+ "description": "Draw trails for the path a user took onto the map based on status messages in COMMs. Uses up to three hours of data. Does not request chat data on its own, even if that would be useful.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.14.0.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/player-activity-tracker.svg"
+ },
+ {
+ "filename": "ornament-icons-extended.user.js",
+ "id": "ornament-icons-extended",
+ "name": "Ornament icons extended",
+ "author": "johtata",
+ "description": "Additonal icons and names for beacons",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/ornament-icons-extended.svg"
+ },
+ {
+ "filename": "links-to-moved-portals.user.js",
+ "id": "links-to-moved-portals",
+ "name": "Links to moved portals",
+ "author": "screach",
+ "description": "Show links to portals with different location data",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.1.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/links-to-moved-portals.svg"
+ },
+ {
+ "filename": "zaprange.user.js",
+ "id": "zaprange",
+ "name": "Zaprange",
+ "author": "ZasoGD",
+ "description": "Shows the maximum range of attack by the portals.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.8.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/zaprange.svg"
+ }
+ ]
+ },
+ "Map Tiles": {
+ "name": "Map Tiles",
+ "name:ru": "Провайдеры карт",
+ "description": "Alternative map layers",
+ "description:ru": "Альтернативные провайдеры карт",
+ "plugins": [
+ {
+ "filename": "basemap-openstreetmap.user.js",
+ "id": "basemap-openstreetmap",
+ "name": "OpenStreetMap.org map",
+ "author": "jonatkins",
+ "description": "Add the native OpenStreetMap.org map tiles as an optional layer.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.6.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-openstreetmap.svg"
+ },
+ {
+ "filename": "basemap-kartverket.user.js",
+ "id": "basemap-kartverket",
+ "name": "Kartverket.no maps (Norway)",
+ "author": "johnd0e",
+ "description": "Add Kartverket.no map layers.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.1.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-kartverket.svg"
+ },
+ {
+ "filename": "basemap-stamen.user.js",
+ "id": "basemap-stamen",
+ "name": "Stamen.com map layers",
+ "author": "jonatkins",
+ "description": "Add the 'Toner' and 'Watercolor' map layers from maps.stamen.com.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-stamen.svg"
+ },
+ {
+ "filename": "basemap-blank.user.js",
+ "id": "basemap-blank",
+ "name": "Blank map",
+ "author": "jonatkins",
+ "description": "Add a blank map layer - no roads or other features.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.6.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-blank.svg"
+ },
+ {
+ "filename": "basemap-yandex.user.js",
+ "id": "basemap-yandex",
+ "name": "Yandex maps",
+ "author": "johnd0e",
+ "description": "Add Yandex.com (Russian/Русский) map layers",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-yandex.svg"
+ },
+ {
+ "filename": "basemap-gaode.user.js",
+ "id": "basemap-gaode",
+ "name": "Gaode (高德地图) / AutoNavi map",
+ "author": "johnd0e",
+ "description": "Map layers from AutoNavi / Gaode (高德地图)",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-gaode.svg"
+ },
+ {
+ "filename": "basemap-bing.user.js",
+ "id": "basemap-bing",
+ "name": "Bing maps",
+ "author": "johnd0e",
+ "description": "Add the bing.com map layers.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-bing.svg"
+ },
+ {
+ "filename": "basemap-google-gray.user.js",
+ "id": "basemap-google-gray",
+ "name": "Gray Google map",
+ "author": "jacob1123",
+ "description": "Add a simplified gray Version of Google map tiles as an optional layer.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.7.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/basemap-google-gray.svg"
+ }
+ ]
+ },
+ "Portal Info": {
+ "name": "Portal Info",
+ "name:ru": "Информация о портале",
+ "description": "Enhanced information on the selected portal",
+ "description:ru": "Подробная информация на выбранном портале",
+ "plugins": [
+ {
+ "filename": "distance-to-portal.user.js",
+ "id": "distance-to-portal",
+ "name": "Distance to portal",
+ "author": "jonatkins",
+ "description": "Allows your current location to be set manually, then shows the distance to the selected portal. Useful when managing portal keys.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/distance-to-portal.svg"
+ },
+ {
+ "filename": "debug-raw-portal-data.user.js",
+ "id": "debug-raw-portal-data",
+ "name": "Debug: Raw portal JSON data",
+ "author": "jonatkins",
+ "description": "Developer debugging aid: Add a link to the portal details to show the raw data of a portal.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.7.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/debug-raw-portal-data.svg"
+ },
+ {
+ "filename": "linked-portals-show.user.js",
+ "id": "linked-portals-show",
+ "name": "Linked portals",
+ "author": "fstopienski",
+ "description": "Try to show the linked portals (image, name and link direction) in portal detail view and jump to linked portal on click. Some details may not be available if the linked portal is not in the current view.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.4.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/linked-portals-show.svg"
+ },
+ {
+ "filename": "reso-energy-pct.user.js",
+ "id": "reso-energy-pct",
+ "name": "Reso energy % in portal details",
+ "author": "xelio",
+ "description": "Show resonator energy percentage on resonator energy bar in portal details panel.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.5.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/reso-energy-pct.svg"
+ }
+ ]
+ },
+ "Tweaks": {
+ "name": "Tweaks",
+ "name:ru": "Настройки",
+ "description": "Adjust IITC settings",
+ "description:ru": "Настройка параметров IITC",
+ "plugins": [
+ {
+ "filename": "fix-china-map-offset.user.js",
+ "id": "fix-china-map-offset",
+ "name": "Fix maps offsets in China",
+ "author": "modos189",
+ "description": "Show correct maps for China user by applying offset tweaks.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.3.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/fix-china-map-offset.svg"
+ },
+ {
+ "filename": "periodic-refresh.user.js",
+ "id": "periodic-refresh",
+ "name": "Periodic refresh",
+ "author": "jonatkins",
+ "description": "For use for unattended display screens only, this plugin causes idle mode to be left once per hour.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/periodic-refresh.svg"
+ },
+ {
+ "filename": "link-show-direction.user.js",
+ "id": "link-show-direction",
+ "name": "Direction of links on map",
+ "author": "jonatkins",
+ "description": "Show the direction of links on the map by adding short dashes to the line at the origin portal.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.4.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/link-show-direction.svg"
+ },
+ {
+ "filename": "scroll-wheel-zoom-disable.user.js",
+ "id": "scroll-wheel-zoom-disable",
+ "name": "Disable mouse wheel zoom",
+ "author": "jonatkins",
+ "description": "Disable the use of mouse wheel to zoom. The map zoom controls or keyboard are still available.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.1.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/scroll-wheel-zoom-disable.svg"
+ }
+ ]
+ },
+ "Misc": {
+ "name": "Misc",
+ "name:ru": "Разное",
+ "description": "Unclassified plugins",
+ "description:ru": "Неклассифицированные плагины",
+ "plugins": [
+ {
+ "filename": "privacy-view.user.js",
+ "id": "privacy-view",
+ "name": "Privacy view on Intel",
+ "author": "johnd0e",
+ "description": "Hide info from intel which shouldn't leak to players of the other faction.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "1.2.2.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/privacy-view.svg"
+ },
+ {
+ "filename": "uniques.user.js",
+ "id": "uniques",
+ "name": "Uniques",
+ "author": "3ch01c",
+ "description": "Allow manual entry of portals visited/captured. Use the 'highlighter-uniques' plugin to show the uniques on the map, and 'sync' to share between multiple browsers or desktop/mobile. It will try and guess which portals you have captured from COMM/portal details, but this will not catch every case.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.7.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/uniques.svg"
+ },
+ {
+ "filename": "keys.user.js",
+ "id": "keys",
+ "name": "Keys",
+ "author": "xelio",
+ "description": "Allow manual entry of key counts for each portal. Use the 'keys-on-map' plugin to show the numbers on the map, and 'sync' to share between multiple browsers or desktop/mobile.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.4.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/keys.svg"
+ },
+ {
+ "filename": "sync.user.js",
+ "id": "sync",
+ "name": "Sync",
+ "author": "xelio",
+ "description": "Sync data between clients via Google Drive API. Only syncs data from specific plugins (currently: Keys, Bookmarks, Uniques). Sign in via the 'Sync' link. Data is synchronized every 3 minutes.",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.5.3.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/sync.svg"
+ },
+ {
+ "filename": "machina-tools.user.js",
+ "id": "machina-tools",
+ "name": "Machina Tools",
+ "author": "Perringaiden",
+ "description": "Machina investigation tools - 2 new layers to see possible Machina spread and portal detail links to display Machina cluster information and to navigate to parent or seed Machina portal",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.9.2.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/machina-tools.svg"
+ }
+ ]
+ },
+ "Obsolete": {
+ "name": "Obsolete",
+ "name:ru": "Устаревшее",
+ "description": "Plugins that are no longer recommended, due to being superceded by others or similar",
+ "description:ru": "Плагины, которые больше не рекомендуются в связи с заменой другими или аналогичными плагинами"
+ },
+ "Deleted": {
+ "name": "Deleted",
+ "name:ru": "Удалённое",
+ "description": "Deleted plugins – listed here for reference only. No download available",
+ "description:ru": "Удаленные плагины – перечислены здесь только для справки. Нет возможности скачать"
+ },
+ "Debug": {
+ "name": "Debug",
+ "plugins": [
+ {
+ "filename": "debug-console.user.js",
+ "id": "debug-console",
+ "name": "Debug console tab",
+ "author": "jaiperdu",
+ "description": "Add a debug console tab",
+ "namespace": "https://github.com/IITC-CE/ingress-intel-total-conversion",
+ "version": "0.2.0.20241216.092245",
+ "icon": "https://iitc.app/extras/plugin-icons/debug-console.svg"
+ }
+ ]
+ }
+ },
+ "iitc_version": "0.39.1.20241216.092245"
+}
\ No newline at end of file
diff --git a/static/build/artifact/PR790/plugins/ap-stats.meta.js b/static/build/artifact/PR790/plugins/ap-stats.meta.js
new file mode 100644
index 0000000000..4857503f49
--- /dev/null
+++ b/static/build/artifact/PR790/plugins/ap-stats.meta.js
@@ -0,0 +1,15 @@
+// ==UserScript==
+// @author Hollow011
+// @name IITC plugin: Available AP statistics
+// @category Info
+// @version 0.4.5.20241216.092245
+// @description Displays the per-team AP gains available in the current view.
+// @id ap-stats
+// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion
+// @updateURL https://iitc.app/build/artifact/PR790/plugins/ap-stats.meta.js
+// @downloadURL https://iitc.app/build/artifact/PR790/plugins/ap-stats.user.js
+// @match https://intel.ingress.com/*
+// @match https://intel-x.ingress.com/*
+// @icon https://iitc.app/extras/plugin-icons/ap-stats.svg
+// @grant none
+// ==/UserScript==
diff --git a/static/build/artifact/PR790/plugins/ap-stats.user.js b/static/build/artifact/PR790/plugins/ap-stats.user.js
new file mode 100644
index 0000000000..092c43a0bc
--- /dev/null
+++ b/static/build/artifact/PR790/plugins/ap-stats.user.js
@@ -0,0 +1,208 @@
+// ==UserScript==
+// @author Hollow011
+// @name IITC plugin: Available AP statistics
+// @category Info
+// @version 0.4.5.20241216.092245
+// @description Displays the per-team AP gains available in the current view.
+// @id ap-stats
+// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion
+// @updateURL https://iitc.app/build/artifact/PR790/plugins/ap-stats.meta.js
+// @downloadURL https://iitc.app/build/artifact/PR790/plugins/ap-stats.user.js
+// @match https://intel.ingress.com/*
+// @match https://intel-x.ingress.com/*
+// @icon https://iitc.app/extras/plugin-icons/ap-stats.svg
+// @grant none
+// ==/UserScript==
+
+function wrapper(plugin_info) {
+// ensure plugin framework is there, even if iitc is not yet loaded
+if(typeof window.plugin !== 'function') window.plugin = function() {};
+
+//PLUGIN AUTHORS: writing a plugin outside of the IITC build environment? if so, delete these lines!!
+//(leaving them in place might break the 'About IITC' page or break update checks)
+plugin_info.buildName = 'test';
+plugin_info.dateTimeVersion = '2024-12-16-092245';
+plugin_info.pluginId = 'ap-stats';
+//END PLUGIN AUTHORS NOTE
+
+/* exported setup, changelog --eslint */
+
+var changelog = [
+ {
+ version: '0.4.5',
+ changes: ['Refactoring: fix eslint'],
+ },
+ {
+ version: '0.4.4',
+ changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'],
+ },
+ {
+ version: '0.4.3',
+ changes: ['Version upgrade due to a change in the wrapper: added plugin icon'],
+ },
+];
+
+// use own namespace for plugin
+window.plugin.compAPStats = function () {};
+
+window.plugin.compAPStats.setupCallback = function () {
+ // add a new div to the bottom of the sidebar and style it
+ $('#sidebar').append('
');
+ $('#available_ap_display').css({ color: '#ffce00', 'font-size': '90%', padding: '4px 2px' });
+
+ // do an initial calc for sidebar sizing purposes
+ window.plugin.compAPStats.update(false);
+
+ // make the value update when the map data updates
+ window.addHook('mapDataRefreshEnd', window.plugin.compAPStats.mapDataRefreshEnd);
+ window.addHook('requestFinished', window.plugin.compAPStats.requestFinished);
+};
+
+window.plugin.compAPStats.mapDataRefreshEnd = function () {
+ if (window.plugin.compAPStats.timer) {
+ clearTimeout(window.plugin.compAPStats.timer);
+ window.plugin.compAPStats.timer = undefined;
+ }
+
+ window.plugin.compAPStats.update(true);
+};
+
+window.plugin.compAPStats.requestFinished = function () {
+ // process on a short delay, so if multiple requests finish in a short time we only calculate once
+ if (window.plugin.compAPStats.timer === undefined) {
+ window.plugin.compAPStats.timer = setTimeout(function () {
+ window.plugin.compAPStats.timer = undefined;
+ window.plugin.compAPStats.update(false);
+ }, 0.75 * 1000);
+ }
+};
+
+window.plugin.compAPStats.updateNoPortals = function () {
+ $('#available_ap_display').html('Available AP in this area:
Zoom closer to get all portals loaded.
');
+};
+
+window.plugin.compAPStats.update = function (hasFinished) {
+ if (!window.getDataZoomTileParameters().hasPortals) {
+ window.plugin.compAPStats.updateNoPortals(hasFinished);
+ return;
+ }
+
+ var result = window.plugin.compAPStats.compAPStats();
+ var loading = hasFinished ? '' : 'Loading...';
+
+ var formatRow = function (team, data) {
+ var title =
+ `Destroy and capture ${data.destroyPortals} portals\n` +
+ `Destroy ${data.destroyLinks} links and ${data.destroyFields} fields\n` +
+ `Capture ${data.capturePortals} neutral portals, complete ${data.finishPortals} portals\n` +
+ `(unknown additional AP for links/fields)`;
+ return `
${team}
${window.digits(data.AP)}
`;
+ };
+
+ $('#available_ap_display').html(
+ `Available AP in this area: ${loading}
';
+
+/* ****************************************************************************************************************** */
+
+/**
+ * CONFIG OPTIONS
+ * @namespace config_options
+ */
+
+/**
+ * Controls how often the map should refresh, in seconds, default 30.
+ * @type {number}
+ * @memberof config_options
+ */
+window.REFRESH = 30;
+
+/**
+ * Controls the extra refresh delay per zoom level, in seconds, default 5.
+ * @type {number}
+ * @memberof config_options
+ */
+window.ZOOM_LEVEL_ADJ = 5;
+
+/**
+ * Wait this long before refreshing the view after the map has been moved, in seconds, default 2.5
+ * @type {number}
+ * @memberof config_options
+ */
+window.ON_MOVE_REFRESH = 2.5;
+
+/**
+ * Limit on refresh time since previous refresh, limiting repeated move refresh rate, in seconds, default 10
+ * @type {number}
+ * @memberof config_options
+ */
+window.MINIMUM_OVERRIDE_REFRESH = 10;
+
+/**
+ * Controls how long to wait between refreshing the global score, in seconds, default 15*60 (15 mins)
+ * @type {number}
+ * @memberof config_options
+ */
+window.REFRESH_GAME_SCORE = 15 * 60;
+
+/**
+ * The maximum idle time in seconds before the map stops updating, in seconds, default 15*60 (15 mins)
+ * @type {number}
+ * @memberof config_options
+ */
+window.MAX_IDLE_TIME = 15 * 60;
+
+/**
+ * How much space to leave for scrollbars, in pixels, default 20.
+ * @type {number}
+ * @memberof config_options
+ */
+window.HIDDEN_SCROLLBAR_ASSUMED_WIDTH = 20;
+
+/**
+ * How wide should the sidebar be, in pixels, default 300.
+ * @type {number}
+ * @memberof config_options
+ */
+window.SIDEBAR_WIDTH = 300;
+
+/**
+ * Controls requesting chat data based on the pixel distance from the line currently in view
+ * and the top of history, in pixels, default 200
+ * @type {number}
+ * @memberof config_options
+ */
+window.CHAT_REQUEST_SCROLL_TOP = 200;
+
+/**
+ * Controls height of chat when chat is collapsed, in pixels, default 60
+ * @type {number}
+ * @memberof config_options
+ */
+window.CHAT_SHRINKED = 60;
+
+/**
+ * What colour should the selected portal be, string(css hex code), default ‘#f0f’ (hot pink)
+ * @type {string}
+ * @memberof config_options
+ */
+window.COLOR_SELECTED_PORTAL = '#f0f';
+
+/**
+ * Defines the color values associated with different teams, used in various elements such as portals, player names, etc.
+ * The colors are represented in a CSS hex code format.
+ * The array format represents: [none, res, enl, mac].
+ * @type {string[]}
+ * @memberof config_options
+ */
+window.COLORS = ['#FF6600', '#0088FF', '#03DC03', '#FF0028'];
+
+/**
+ * Colour values for levels, consistent with Ingress, with index 0 being white for neutral portals.
+ * @type {string[]}
+ * @memberof config_options
+ */
+window.COLORS_LVL = ['#000', '#FECE5A', '#FFA630', '#FF7315', '#E40000', '#FD2992', '#EB26CD', '#C124E0', '#9627F4'];
+
+/**
+ * Colour values for displaying mods, consistent with Ingress. Very Rare also used for AXA shields and Ultra Links.
+ * @type {object}
+ * @property {string} VERY_RARE=#b08cff
+ * @property {string} RARE=#73a8ff
+ * @property {string} COMMON=#8cffbf
+ * @memberof config_options
+ */
+window.COLORS_MOD = { VERY_RARE: '#b08cff', RARE: '#73a8ff', COMMON: '#8cffbf' };
+
+/**
+ * What colour should the hacking range circle be (the small circle that appears around a selected portal,
+ * marking a ~40 metre radius), string(css colour value), default ‘orange’
+ * @type {string}
+ * @memberof config_options
+ */
+window.ACCESS_INDICATOR_COLOR = 'orange';
+
+/**
+ * What colour should the linkable range circle be, string(css colour value), default ‘red’
+ * @type {string}
+ * @memberof config_options
+ */
+window.RANGE_INDICATOR_COLOR = 'red';
+
+/**
+ * Min zoom for intel map - should match that used by stock intel, default 3
+ * @type {number}
+ * @memberof config_options
+ */
+window.MIN_ZOOM = 3;
+
+/**
+ * Used when zoom level is not specified explicitly (must contain all the portals)
+ * @type {number}
+ * @memberof config_options
+ */
+window.DEFAULT_ZOOM = 15;
+
+/**
+ * URL of the default image for the portal
+ * @type {string}
+ * @memberof config_options
+ */
+window.DEFAULT_PORTAL_IMG = '//commondatastorage.googleapis.com/ingress.com/img/default-portal-image.png';
+
+/**
+ * URL to call the Nominatim geocoder service, string.
+ * @type {string}
+ * @memberof config_options
+ */
+window.NOMINATIM = '//nominatim.openstreetmap.org/search?format=json&polygon_geojson=1&q=';
+
+/* ****************************************************************************************************************** */
+
+/**
+ * INGRESS CONSTANTS
+ * http://decodeingress.me/2012/11/18/ingress-portal-levels-and-link-range/
+ * @namespace ingress_constants
+ */
+
+/**
+ * Resonator energy per level, 1-based array, XM
+ * @type {number[]}
+ * @const
+ * @memberof ingress_constants
+ */
+window.RESO_NRG = [0, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000];
+
+/**
+ * Maximum radius around a portal from which the portal is hackable, metres.
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.HACK_RANGE = 40;
+
+/**
+ * The maximum radius around the portal from which the Machine can link
+ * @type {number[]}
+ * @const
+ * @memberof ingress_constants
+ */
+window.LINK_RANGE_MAC = [0, 200, 250, 350, 400, 500, 600, 700, 1000, 1000]; // in meters
+
+/**
+ * Resonator octant cardinal directions
+ * @type {string[]}
+ * @const
+ * @memberof ingress_constants
+ */
+window.OCTANTS = ['E', 'NE', 'N', 'NW', 'W', 'SW', 'S', 'SE'];
+
+/**
+ * Resonator octant arrows
+ * @type {string[]}
+ * @const
+ * @memberof ingress_constants
+ */
+window.OCTANTS_ARROW = ['→', '↗', '↑', '↖', '←', '↙', '↓', '↘'];
+
+/**
+ * AP for destroying portal
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.DESTROY_RESONATOR = 75;
+
+/**
+ * AP for destroying link
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.DESTROY_LINK = 187;
+
+/**
+ * AP for destroying field
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.DESTROY_FIELD = 750;
+
+/**
+ * AP for capturing a portal
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.CAPTURE_PORTAL = 500;
+
+/**
+ * AP for deploying a resonator
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.DEPLOY_RESONATOR = 125;
+
+/**
+ * AP for deploying all resonators on portal
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.COMPLETION_BONUS = 250;
+
+/**
+ * AP for upgrading another's resonator
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.UPGRADE_ANOTHERS_RESONATOR = 65;
+
+/**
+ * Maximum portal level.
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.MAX_PORTAL_LEVEL = 8;
+
+/**
+ * How many resonators of a given level can one deploy; 1-based array where the index is the resonator level.
+ * @type {number[]}
+ * @const
+ * @memberof ingress_constants
+ */
+window.MAX_RESO_PER_PLAYER = [0, 8, 4, 4, 4, 2, 2, 1, 1];
+
+/**
+ * The base value of how long you need to wait between portal hacks, in seconds.
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.BASE_HACK_COOLDOWN = 300; // 5 mins - 300 seconds
+
+/**
+ * Base value, how many times at most you can hack the portal.
+ * @type {number}
+ * @const
+ * @memberof ingress_constants
+ */
+window.BASE_HACK_COUNT = 4;
+
+/* ****************************************************************************************************************** */
+
+/**
+ * OTHER MORE-OR-LESS CONSTANTS
+ * @namespace other_constants
+ */
+
+/**
+ * @type {number}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_NONE = 0;
+
+/**
+ * @type {number}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_RES = 1;
+
+/**
+ * @type {number}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_ENL = 2;
+
+/**
+ * @type {number}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_MAC = 3;
+
+/**
+ * @type {string[]}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_TO_CSS = ['none', 'res', 'enl', 'mac'];
+
+/**
+ * @type {string[]}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_NAMES = ['Neutral', 'Resistance', 'Enlightened', '__MACHINA__'];
+
+/**
+ * @type {string[]}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_CODES = ['N', 'R', 'E', 'M'];
+
+/**
+ * @type {string[]}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_CODENAMES = ['NEUTRAL', 'RESISTANCE', 'ENLIGHTENED', 'MACHINA'];
+window.TEAM_SHORTNAMES = ['NEU', 'RES', 'ENL', 'MAC'];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_NAME_NONE = window.TEAM_NAMES[window.TEAM_NONE];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_NAME_RES = window.TEAM_NAMES[window.TEAM_RES];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_NAME_ENL = window.TEAM_NAMES[window.TEAM_ENL];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_NAME_MAC = window.TEAM_NAMES[window.TEAM_MAC];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_CODE_NONE = window.TEAM_CODES[window.TEAM_NONE];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_CODE_RES = window.TEAM_CODES[window.TEAM_RES];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_CODE_ENL = window.TEAM_CODES[window.TEAM_ENL];
+
+/**
+ * @type {string}
+ * @const
+ * @memberof other_constants
+ */
+window.TEAM_CODE_MAC = window.TEAM_CODES[window.TEAM_MAC];
+
+/* ****************************************************************************************************************** */
+
+/**
+ * Global variables used for storage. Most likely READ ONLY. Proper ay would be to encapsulate them in an anonymous
+ * function and write getters/setters, but if you are careful enough, this works.
+ * @namespace storage_variables
+ */
+
+/**
+ * Stores the id of the timeout that kicks off the next refresh (ie value returned by ``setTimeout()``)
+ * @type {number|undefined}
+ * @memberof storage_variables
+ */
+window.refreshTimeout = undefined;
+
+/**
+ * Portal GUID if the original URL had it.
+ * @type {string|null}
+ * @memberof storage_variables
+ */
+window.urlPortal = null;
+
+/**
+ * Portal lng/lat if the orignial URL had it.
+ * @type {object|null}
+ * @memberof storage_variables
+ */
+window.urlPortalLL = null;
+
+/**
+ * Stores the GUID of the selected portal, or is `null` if there is none.
+ * @type {string|null}
+ * @memberof storage_variables
+ */
+window.selectedPortal = null;
+
+/**
+ * Reference to the linking range indicator of the selected portal. This is a Leaflet layer.
+ * @type {object|null}
+ * @memberof storage_variables
+ */
+window.portalRangeIndicator = null;
+
+/**
+ * Reference to the hacking range indicator of the selected portal. This is a Leaflet layer.
+ * @type {object|null}
+ * @memberof storage_variables
+ */
+window.portalAccessIndicator = null;
+
+/**
+ * References to Leaflet objects representing portals, indexed by entity ID.
+ * This object stores the mapping in the format `{ id1: feature1, ... }`.
+ * Note: While these are Leaflet objects, not all may be added to the map due to render limits.
+ * @type {Object.}
+ * @memberof storage_variables
+ */
+window.portals = {};
+
+/**
+ * References to Leaflet objects representing links, indexed by entity ID.
+ * This object stores the mapping in the format `{ id1: feature1, ... }`.
+ * Note: While these are Leaflet objects, not all may be added to the map due to render limits.
+ * @type {Object.}
+ * @memberof storage_variables
+ */
+window.links = {};
+
+/**
+ * References to Leaflet objects representing fields, indexed by entity ID.
+ * This object stores the mapping in the format `{ id1: feature1, ... }`.
+ * Note: While these are Leaflet objects, not all may be added to the map due to render limits.
+ * @type {Object.}
+ * @memberof storage_variables
+ */
+window.fields = {};
+
+/**
+ * @class L
+ * @description Root class for all Leaflet-related functionalities, extended with custom methods and properties.
+ */
+
+// plugin framework. Plugins may load earlier than iitc, so don’t
+// overwrite data
+if (typeof window.plugin !== 'function') window.plugin = function () {};
+
+// eslint-disable-next-line no-unused-vars
+const ulog = (function (module) {
+ // *** included: external/ulog.min.js ***
+!function(e,n,t){"function"==typeof define&&define.amd?define(n,[],t):e[n]=t()}(this,"ulog",function(){"use strict";function l(e){return e?a[e]||(a[e]=n(function(e,n){n=new Function("n","log","return {'"+e+"':function(){log.invoke(n,[].slice.call(arguments))}}[n]")(e,l);try{Object.defineProperty(n,"name",{get:function(){return e}})}catch(e){}return n}(e),l)):n(l)}l.formats=[],l.extends=[],l.enable=function(e){var n,t=(e||"").split(/[\s,]+/);for(n=0;n window.PLAYER.level ? 0 : window.MAX_RESO_PER_PLAYER[i];
+ }
+ $.each(resonators_on_portal, function (ind, reso) {
+ if (reso !== null && reso.owner === window.PLAYER.nickname) {
+ player_resontators[reso.level]--;
+ }
+ resonator_levels.push(reso === null ? 0 : reso.level);
+ });
+
+ resonator_levels.sort(function (a, b) {
+ return a - b;
+ });
+
+ // Max out portal
+ var install_index = 0;
+ for (var j = window.MAX_PORTAL_LEVEL; j >= 1; j--) {
+ for (var install = player_resontators[j]; install > 0; install--) {
+ if (resonator_levels[install_index] < j) {
+ resonator_levels[install_index] = j;
+ install_index++;
+ }
+ }
+ }
+
+ potential_level =
+ resonator_levels.reduce(function (a, b) {
+ return a + b;
+ }) / 8;
+ }
+ return potential_level;
+};
+
+/**
+ * Finds the latitude and longitude for a portal using all available data sources.
+ * This includes the list of portals, cached portal details, and information from links and fields.
+ *
+ * @deprecated
+ * @function findPortalLatLng
+ * @param {string} guid - The GUID of the portal.
+ * @returns {L.LatLng|undefined} The LatLng location of the portal, or undefined if not found.
+ */
+window.findPortalLatLng = function (guid) {
+ if (window.portals[guid]) {
+ return window.portals[guid].getLatLng();
+ }
+
+ // not found in portals - try the cached (and possibly stale) details - good enough for location
+ var details = window.portalDetail.get(guid);
+ if (details) {
+ return L.latLng(details.latE6 / 1e6, details.lngE6 / 1e6);
+ }
+
+ // now try searching through fields
+ for (var fguid in window.fields) {
+ var f = window.fields[fguid].options.data;
+
+ for (var i in f.points) {
+ if (f.points[i].guid === guid) {
+ return L.latLng(f.points[i].latE6 / 1e6, f.points[i].lngE6 / 1e6);
+ }
+ }
+ }
+
+ // and finally search through links
+ for (var lguid in window.links) {
+ var l = window.links[lguid].options.data;
+ if (l.oGuid === guid) {
+ return L.latLng(l.oLatE6 / 1e6, l.oLngE6 / 1e6);
+ }
+ if (l.dGuid === guid) {
+ return L.latLng(l.dLatE6 / 1e6, l.dLngE6 / 1e6);
+ }
+ }
+
+ // no luck finding portal lat/lng
+ return undefined;
+};
+
+// to be ovewritten in app.js
+/**
+ * Finds the latitude and longitude for a portal using all available data sources.
+ * This includes the list of portals, cached portal details, and information from links and fields.
+ *
+ * @deprecated
+ * @function androidCopy
+ */
+window.androidCopy = function () {
+ return true; // i.e. execute other actions
+};
+
+/**
+ * Given the entity detail data, returns the team the entity belongs to.
+ * Uses TEAM_* enum values.
+ *
+ * @deprecated
+ * @function getTeam
+ * @param {Object} details - The details hash of an entity.
+ * @returns {number} The team ID the entity belongs to.
+ */
+window.getTeam = function (details) {
+ return IITC.utils.getTeamId(details.team);
+};
+
+
+})();
+
+
+// *** module: app.js ***
+(function () {
+var log = ulog('app');
+/* global L -- eslint */
+
+/**
+ * @file This file contains the main JavaScript code for the app, including utility functions,
+ * app-specific behaviors, and integration with the Android environment.
+ * @module app
+ */
+
+/**
+ * Global flag indicating whether the app is running as a standalone app or within a browser.
+ * @type {boolean}
+ * @memberof module:app
+ */
+var isApp = typeof app !== 'undefined' || typeof android !== 'undefined';
+window.isApp = isApp;
+
+/**
+ * Determines whether to use the interface for mobile devices depending on the application environment and device type.
+ *
+ * @function useAppPanes
+ * @returns {boolean} Returns true if app panes should be used, false otherwise.
+ */
+window.useAppPanes = function () {
+ // isSmartphone is important to disable panes in desktop mode
+ return isApp && window.app.addPane && window.isSmartphone();
+};
+window.useAndroidPanes = window.useAppPanes; // compatibility
+
+if (isApp) {
+ if (typeof app === 'undefined') {
+ // compatibility
+ window.app = window.android;
+ } else {
+ window.android = window.app;
+ }
+
+ window.requestFile = function (callback) {
+ // deprecated
+ L.FileListLoader.loadFiles().on('load', function (e) {
+ callback(e.file.name, e.reader.result);
+ });
+ };
+}
+
+/**
+ * Returns a function, that, as long as it continues to be invoked, will not be triggered.
+ * The function will be called after it stops being called for N milliseconds.
+ * source: https://gist.github.com/nmsdvid/8807205#gistcomment-2641356
+ *
+ * @function debounce
+ * @param {Function} callback - The function to debounce.
+ * @param {number} time - The debounce time in milliseconds.
+ * @returns {Function} Returns a debounced version of the given function.
+ */
+function debounce(callback, time) {
+ var timeout;
+ return function () {
+ var context = this;
+ var args = arguments;
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ timeout = setTimeout(function () {
+ timeout = null;
+ callback.apply(context, args);
+ }, time);
+ };
+}
+
+function extendLayerChooser() {
+ if (window.app.setLayers) {
+ // hook some additional code into the LayerControl so it's easy for the mobile app to interface with it
+ window.LayerChooser.include({
+ _setAppLayers: debounce(function () {
+ var l = this.getLayers();
+ window.app.setLayers(JSON.stringify(l.baseLayers), JSON.stringify(l.overlayLayers));
+ }, 1000),
+
+ setLabel: (function (setLabel) {
+ return function () {
+ this._setAppLayers();
+ return setLabel.apply(this, arguments);
+ };
+ })(window.LayerChooser.prototype.setLabel),
+
+ _update: function () {
+ this._setAppLayers();
+ return L.Control.Layers.prototype._update.apply(this, arguments);
+ },
+ });
+ }
+}
+
+window.runOnAppBeforeBoot = function () {
+ if (!isApp) {
+ return;
+ }
+
+ if (window.app.showZoom) {
+ window.mapOptions.zoomControl = window.app.showZoom();
+ }
+
+ extendLayerChooser();
+
+ // add jquery listeners ******************************************************
+ if (window.app.dialogOpened && window.app.dialogFocused) {
+ $(document.body).on({
+ // hints for iitc mobile
+ dialogopen: function (e) {
+ var id = $(e.target).data('id');
+ window.app.dialogOpened(id, true);
+ },
+ dialogclose: function (e) {
+ var id = $(e.target).data('id');
+ window.app.dialogOpened(id, false);
+ },
+ dialogfocus: function (e) {
+ var id = $(e.target).data('id');
+ window.app.dialogFocused(id);
+ },
+ });
+ }
+ // notify app that a select spinner is enabled.
+ // this disables javascript injection on app's side.
+ // if app is not notified, the spinner closes on the next JS call
+ if (window.app.spinnerEnabled) {
+ $(document.body).on('click', 'select', function () {
+ window.app.spinnerEnabled(true);
+ });
+ }
+
+ // add iitc hooks ************************************************************
+ if (window.app.switchToPane) {
+ window.addHook('paneChanged', function (name) {
+ // https://stackoverflow.com/a/59158952/2520247
+ window.app.switchToPane(name);
+ });
+ }
+
+ // overwrite some functions **************************************************
+ if (window.app.copy) {
+ window.androidCopy = function (text) {
+ window.app.copy(text);
+ return false;
+ };
+ }
+
+ if (window.app.saveFile) {
+ window.saveFile = function (data, filename, dataType) {
+ window.app.saveFile(filename || '', dataType || '*/*', data);
+ };
+ }
+
+ if (window.app.intentPosLink) {
+ window.renderPortalUrl = function (lat, lng, title, guid) {
+ // one share link option - and the app provides an interface to share the URL,
+ // share as a geo: intent (navigation via google maps), etc
+
+ var shareLink = $('')
+ .text('Share portal')
+ .click(function () {
+ window.app.intentPosLink(lat, lng, window.map.getZoom(), title, true, guid);
+ });
+ $('.linkdetails').append($('");a=e[0];d=a.callback;g=function(){var c;c=[];for(b in a)h=a[b],"callback"!==b&&c.push(" "+b+"='"+h+"'");return c}().join("");return this.replace(f,function(c,b,a){c=("function"===typeof d?d(a):
+void 0)||""+a+"";return""+b+c})}}).call(this);
+
+
+;
+
+ window.L_NO_TOUCH = navigator.maxTouchPoints === 0; // prevent mobile style on desktop https://github.com/IITC-CE/ingress-intel-total-conversion/pull/189
+ // eslint-disable-next-line
+ // *** included: external/leaflet-src.js ***
+/* @preserve
+ * Leaflet 1.8.0, a JS library for interactive maps. https://leafletjs.com
+ * (c) 2010-2022 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.8.0";
+
+ /*
+ * @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) {
+ 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) {
+ if (typeof fn !== 'function') {
+ console.warn('wrong listener type: ' + typeof fn);
+ return;
+ }
+ this._events = this._events || {};
+
+ /* get/init listeners for type */
+ var typeListeners = this._events[type];
+ if (!typeListeners) {
+ typeListeners = [];
+ this._events[type] = typeListeners;
+ }
+
+ if (context === this) {
+ // Less memory footprint.
+ context = undefined;
+ }
+ var newListener = {fn: fn, ctx: context},
+ listeners = typeListeners;
+
+ // check if fn already there
+ for (var i = 0, len = listeners.length; i < len; i++) {
+ if (listeners[i].fn === fn && listeners[i].ctx === context) {
+ return;
+ }
+ }
+
+ listeners.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 (context === this) {
+ context = undefined;
+ }
+
+ if (typeof fn !== 'function') {
+ console.warn('wrong listener type: ' + typeof fn);
+ return;
+ }
+ // find fn and remove it
+ for (i = 0, len = listeners.length; i < len; i++) {
+ var l = listeners[i];
+ if (l.ctx !== context) { continue; }
+ if (l.fn === fn) {
+ if (this._firingCount) {
+ // set the removed listener to noop so that's not called if remove happens in fire
+ l.fn = falseFn;
+
+ /* copy array in case events are being fired */
+ this._events[type] = listeners = listeners.slice();
+ }
+ listeners.splice(i, 1);
+
+ return;
+ }
+ }
+ console.warn('listener not found');
+ },
+
+ // @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];
+ l.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
+ // 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, propagate) {
+ if (typeof type !== 'string') {
+ console.warn('"string" type argument expected');
+ }
+ var listeners = this._events && this._events[type];
+ if (listeners && listeners.length) { return true; }
+
+ if (propagate) {
+ // also check parents for listeners if event propagates
+ for (var id in this._eventParents) {
+ if (this._eventParents[id].listens(type, propagate)) { return true; }
+ }
+ }
+ 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) {
+
+ if (typeof types === 'object') {
+ for (var type in types) {
+ this.once(type, types[type], fn);
+ }
+ return this;
+ }
+
+ var handler = bind(function () {
+ this
+ .off(types, fn, context)
+ .off(types, handler, context);
+ }, this);
+
+ // add a listener that's executed once and removed after that
+ return this
+ .on(types, fn, context)
+ .on(types, handler, context);
+ },
+
+ // @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.
+ extend: function (point) { // (Point)
+ point = toPoint(point);
+
+ // @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 = point.clone();
+ this.max = point.clone();
+ } else {
+ this.min.x = Math.min(point.x, this.min.x);
+ this.max.x = Math.max(point.x, this.max.x);
+ this.min.y = Math.min(point.y, this.min.y);
+ this.max.y = Math.max(point.y, this.max.y);
+ }
+ return this;
+ },
+
+ // @method getCenter(round?: Boolean): Point
+ // Returns the center point of the bounds.
+ getCenter: function (round) {
+ return new Point(
+ (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 new Point(this.min.x, this.max.y);
+ },
+
+ // @method getTopRight(): Point
+ // Returns the top-right point of the bounds.
+ getTopRight: function () { // -> Point
+ return new Point(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;
+ },
+
+ isValid: function () {
+ return !!(this.min && this.max);
+ }
+ };
+
+
+ // @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 [`