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}${formatRow('Enlightened', result.enl)}${formatRow('Resistance', result.res)}
` + ); +}; + +window.plugin.compAPStats.compAPStats = function () { + var result = { + res: { AP: 0, destroyPortals: 0, capturePortals: 0, finishPortals: 0, destroyLinks: 0, destroyFields: 0 }, + enl: { AP: 0, destroyPortals: 0, capturePortals: 0, finishPortals: 0, destroyLinks: 0, destroyFields: 0 }, + }; + + var displayBounds = window.map.getBounds(); + + // AP to fully deploy a neutral portal + var PORTAL_FULL_DEPLOY_AP = window.CAPTURE_PORTAL + 8 * window.DEPLOY_RESONATOR + window.COMPLETION_BONUS; + + // Grab every portal in the viewable area and compute individual AP stats + // (fields and links are counted separately below) + $.each(window.portals, function (ind, portal) { + var data = portal.options.data; + + // eliminate offscreen portals + if (!displayBounds.contains(portal.getLatLng())) return true; // $.each 'continue' + + // AP to complete a portal - assuming it's already captured (so no CAPTURE_PORTAL) + var completePortalAp = 0; + if ('resCount' in data && data.resCount < 8) { + completePortalAp = (8 - data.resCount) * window.DEPLOY_RESONATOR + window.COMPLETION_BONUS; + } + + // AP to destroy this portal + var destroyAp = (data.resCount || 0) * window.DESTROY_RESONATOR; + + if (portal.options.team === window.TEAM_ENL) { + result.res.AP += destroyAp + PORTAL_FULL_DEPLOY_AP; + result.res.destroyPortals++; + if (completePortalAp) { + result.enl.AP += completePortalAp; + result.enl.finishPortals++; + } + } else if (portal.options.team === window.TEAM_RES) { + result.enl.AP += destroyAp + PORTAL_FULL_DEPLOY_AP; + result.enl.destroyPortals++; + if (completePortalAp) { + result.res.AP += completePortalAp; + result.res.finishPortals++; + } + } else { + // it's a neutral portal, potential for both teams. by definition no fields or edges + result.enl.AP += PORTAL_FULL_DEPLOY_AP; + result.enl.capturePortals++; + result.res.AP += PORTAL_FULL_DEPLOY_AP; + result.res.capturePortals++; + } + }); + + // now every link that starts/ends at a point on screen + $.each(window.links, function (guid, link) { + // only consider links that start/end on-screen + var points = link.getLatLngs(); + if (displayBounds.contains(points[0]) || displayBounds.contains(points[1])) { + if (link.options.team === window.TEAM_ENL) { + result.res.AP += window.DESTROY_LINK; + result.res.destroyLinks++; + } else if (link.options.team === window.TEAM_RES) { + result.enl.AP += window.DESTROY_LINK; + result.enl.destroyLinks++; + } + } + }); + + // and now all fields that have a vertex on screen + $.each(window.fields, function (guid, field) { + // only consider fields with at least one vertex on screen + var points = field.getLatLngs(); + if (displayBounds.contains(points[0]) || displayBounds.contains(points[1]) || displayBounds.contains(points[2])) { + if (field.options.team === window.TEAM_ENL) { + result.res.AP += window.DESTROY_FIELD; + result.res.destroyFields++; + } else if (field.options.team === window.TEAM_RES) { + result.enl.AP += window.DESTROY_FIELD; + result.enl.destroyFields++; + } + } + }); + + return result; +}; + +var setup = function () { + window.plugin.compAPStats.setupCallback(); +}; + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-bing.meta.js b/static/build/artifact/PR790/plugins/basemap-bing.meta.js new file mode 100644 index 0000000000..57bb5e0668 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-bing.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Bing maps +// @category Map Tiles +// @version 0.3.4.20241216.092245 +// @description Add the bing.com map layers. +// @id basemap-bing +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-bing.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-bing.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-bing.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-bing.user.js b/static/build/artifact/PR790/plugins/basemap-bing.user.js new file mode 100644 index 0000000000..be0957598a --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-bing.user.js @@ -0,0 +1,386 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Bing maps +// @category Map Tiles +// @version 0.3.4.20241216.092245 +// @description Add the bing.com map layers. +// @id basemap-bing +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-bing.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-bing.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-bing.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 = 'basemap-bing'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.3.4', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.3.3', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.3.2', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +var mapBing = {}; + +mapBing.sets = { + Road: { + imagerySet: 'RoadOnDemand', + }, + Dark: { + imagerySet: 'CanvasDark', + }, + Aerial: { + imagerySet: 'Aerial', + }, + Hybrid: { + imagerySet: 'AerialWithLabelsOnDemand', + }, +}; + +mapBing.options = { + // set this to your API key + key: 'ArR2hTa2C9cRQZT-RmgrDkfvh3PwEVRl0gB34OO4wJI7vQNElg3DDWvbo5lfUs3p', +}; + +function setup() { + setupBingLeaflet(); + + for (var name in mapBing.sets) { + var options = L.extend({}, mapBing.options, mapBing.sets[name]); + window.layerChooser.addBaseLayer(L.bingLayer(options), 'Bing ' + name); + } +} + +function setupBingLeaflet() { + try { + // https://github.com/shramov/leaflet-plugins/blob/master/layer/tile/Bing.js + // *** included: external/Bing.js *** +// Bing maps API: https://docs.microsoft.com/en-us/bingmaps/rest-services/ + +L.BingLayer = L.TileLayer.extend({ + options: { + // imagerySet: https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#template-parameters + // supported: + // - Aerial, AerialWithLabels (Deprecated), AerialWithLabelsOnDemand + // - Road (Deprecated), RoadOnDemand + // - CanvasDark, CanvasLight, CanvasGray + // not supported: Birdseye*, Streetside + imagerySet: 'Aerial', // to be changed on next major version!! + + // https://docs.microsoft.com/en-us/bingmaps/rest-services/common-parameters-and-types/supported-culture-codes + culture: '', + + // https://docs.microsoft.com/en-us/bingmaps/articles/custom-map-styles-in-bing-maps#custom-map-styles-in-the-rest-and-tile-services + style: '', + + // https://blogs.bing.com/maps/2015/02/12/high-ppi-maps-now-available-in-the-bing-maps-ajax-control + // not documented in REST API docs, but working + // warning: deprecated imagery sets may not support some values (depending also on zoom level) + retinaDpi: 'd2', + + attribution: 'Bing', + minZoom: 1, + maxZoom: 21 + // Actual `maxZoom` value may be less, depending on imagery set / coverage area + // - 19~20 for all 'Aerial*' + // - 20 for 'Road' (Deprecated) + }, + + initialize: function (key, options) { + if (typeof key === 'object') { + options = key; + key = false; + } + L.TileLayer.prototype.initialize.call(this, null, options); + + options = this.options; + options.key = options.key || options.bingMapsKey; + options.imagerySet = options.imagerySet || options.type; + if (key) { options.key = key; } + }, + + tile2quad: function (x, y, z) { + var quad = ''; + for (var i = z; i > 0; i--) { + var digit = 0; + var mask = 1 << i - 1; + if ((x & mask) !== 0) { digit += 1; } + if ((y & mask) !== 0) { digit += 2; } + quad = quad + digit; + } + return quad; + }, + + getTileUrl: function (coords) { + var data = { + subdomain: this._getSubdomain(coords), + quadkey: this.tile2quad(coords.x, coords.y, this._getZoomForUrl()), + culture: this.options.culture // compatibility for deprecated imagery sets ('Road' etc) + }; + return L.Util.template(this._url, data); + }, + + callRestService: function (request, callback, context) { + context = context || this; + var uniqueName = '_bing_metadata_' + L.Util.stamp(this); + while (window[uniqueName]) { uniqueName += '_'; } + request += '&jsonp=' + uniqueName; + var script = document.createElement('script'); + script.setAttribute('type', 'text/javascript'); + script.setAttribute('src', request); + window[uniqueName] = function (response) { + delete window[uniqueName]; + script.remove(); + if (response.errorDetails) { + throw new Error(response.errorDetails); + } + callback.call(context, response); + }; + document.body.appendChild(script); + }, + + _makeApiUrl: function (restApi, resourcePath, query) { + var baseAPIparams = { + version: 'v1', + restApi: restApi, + resourcePath: resourcePath + }; + query = L.extend({ + // errorDetail: true, // seems no effect + key: this.options.key + }, query); + + // https://docs.microsoft.com/en-us/bingmaps/rest-services/common-parameters-and-types/base-url-structure + var template = 'https://dev.virtualearth.net/REST/{version}/{restApi}/{resourcePath}'; // ?queryParameters&key=BingMapsKey + return L.Util.template(template, baseAPIparams) + L.Util.getParamString(query); + }, + + loadMetadata: function () { + if (this.metaRequested) { return; } + this.metaRequested = true; + var options = this.options; + // https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#complete-metadata-urls + var request = this._makeApiUrl('Imagery/Metadata', options.imagerySet, { + UriScheme: 'https', + include: 'ImageryProviders', + culture: options.culture, + style: options.style + }); + this.callRestService(request, function (meta) { + var r = meta.resourceSets[0].resources[0]; + if (!r.imageUrl) { throw new Error('imageUrl not found in response'); } + if (r.imageUrlSubdomains) { options.subdomains = r.imageUrlSubdomains; } + this._providers = r.imageryProviders ? this._prepAttrBounds(r.imageryProviders) : []; + this._attributions = []; + this._url = r.imageUrl; + if (options.retinaDpi && options.detectRetina && options.zoomOffset) { + this._url += '&dpi=' + options.retinaDpi; + } + this.fire('load', {meta: meta}); + if (this._map) { this._update(); } + }); + }, + + _prepAttrBounds: function (providers) { + providers.forEach(function (provider) { + provider.coverageAreas.forEach(function (area) { + area.bounds = L.latLngBounds( + [area.bbox[0], area.bbox[1]], + [area.bbox[2], area.bbox[3]] + ); + }); + }); + return providers; + }, + + _update: function (center) { + if (!this._url) { return; } + L.GridLayer.prototype._update.call(this, center); + this._update_attribution(); + }, + + _update_attribution: function (remove) { + var attributionControl = this._map.attributionControl; + if (!attributionControl) { + this._attributions = {}; return; + } + var bounds = this._map.getBounds(); + bounds = L.latLngBounds(bounds.getSouthWest().wrap(), bounds.getNorthEast().wrap()); + var zoom = this._getZoomForUrl(); + var attributions = this._providers.map(function (provider) { + return remove ? false : provider.coverageAreas.some(function (area) { + return zoom <= area.zoomMax && zoom >= area.zoomMin && + bounds.intersects(area.bounds); + }); + }); + attributions.forEach(function (a,i) { + if (a == this._attributions[i]) { // eslint-disable-line eqeqeq + return; + } else if (a) { + attributionControl.addAttribution(this._providers[i].attribution); + } else { + attributionControl.removeAttribution(this._providers[i].attribution); + } + }, this); + this._attributions = attributions; + }, + + onAdd: function (map) { + // Note: Metadata could be loaded earlier, on layer initialize, + // but according to docs even such request is billable: + // https://docs.microsoft.com/en-us/bingmaps/getting-started/bing-maps-dev-center-help/understanding-bing-maps-transactions#rest-services + // That's why it's important to defer it till BingLayer is actually added to map + this.loadMetadata(); + L.GridLayer.prototype.onAdd.call(this, map); + }, + + onRemove: function (map) { + if (this._providers) { this._update_attribution(true); } + L.GridLayer.prototype.onRemove.call(this, map); + } +}); + +L.bingLayer = function (key, options) { + return new L.BingLayer(key, options); +}; + + +; // eslint-disable-line + + // https://github.com/shramov/leaflet-plugins/blob/master/layer/tile/Bing.addon.applyMaxNativeZoom.js + // *** included: external/Bing.addon.applyMaxNativeZoom.js *** +/* + * Metadata response has `zoomMin`/`zoomMax` properties, that currently (in most cases) are constant: `1`/`21`. + * But in fact, imagery for 'Aerial*' and (deprecated) 'Road' sets may be absent at high zoom levels, + * depending on location. + * This addon is intended to find and apply *real* maximum available zoom (for current location) on layer add. + * Ref: https://stackoverflow.com/questions/12788245/bing-maps-determine-max-zoom-level-for-static-aerial-map-with-rest-imagery-api + * + * @option applyMaxNativeZoom: Boolean|String = 'auto' + * Determines whether `applyMaxNativeZoom` method will be called on layer add. + * 'auto' means that option will be active for 'Aerial*' and 'Road' imagery sets + * (but only if `maxNativeZoom` is not explicitely provided in options). + * + * @option applyMaxNativeZoom_validityRadius: Number = 10000000 + * Limits validity of 'measured' max zoom to specified radius. + * Metadata requests are asynchronous, so when result is ready actual map position can be already changed. + * if distance between old and new locations is longer than defined by this option, + * then maxNativeZoom will be recalculated for new position. + * + * @method applyMaxNativeZoom(latlng: LatLng): this + * Try to find maximum available zoom (for current location), and apply it as `maxNativeZoom`. + * There is no official way, so use heuristic: check `vintageStart` in metadata response. + * Currently method makes sense for 'Aerial*' and 'Road' imagery sets only. + * + * @event maxNativeZoomApplied: Event + * Fired when applyMaxNativeZoom method succeed. + * Extends event object with these properties: value, oldValue, latlng. + */ + +L.BingLayer.mergeOptions({ + applyMaxNativeZoom: 'auto', + applyMaxNativeZoom_validityRadius: 10000000 +}); + +L.BingLayer.addInitHook(function () { + var options = this.options; + if (options.applyMaxNativeZoom === 'auto' && !options.maxNativeZoom) { + options.applyMaxNativeZoom = options.imagerySet === 'Road' || + options.imagerySet.substring(0,6) === 'Aerial'; + } + if (options.applyMaxNativeZoom) { + this.on('add',function () { + this.applyMaxNativeZoom(this._map.getCenter()); + }); + } +}); + +L.BingLayer.include({ + applyMaxNativeZoom: function (latlng) { + var options = this.options; + // https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#basic-metadata-url + var request = this._makeApiUrl('Imagery/BasicMetadata', L.Util.template('{imagerySet}/{centerPoint}', { + imagerySet: options.imagerySet, + centerPoint: L.Util.template('{lat},{lng}', latlng) + })); + var zoomOffset = options.zoomOffset || 0; // detectRetina sideeffects on maxZoom / maxNativeZoom + this._findVintage(request, options.maxZoom + zoomOffset, function (zoom) { + if (!zoom || !this._map) { return; } + var newLatlng = this._map.getCenter(); + var validityRadius = this.options.applyMaxNativeZoom_validityRadius; + if (newLatlng.distanceTo(latlng) > validityRadius) { + this.applyMaxNativeZoom(newLatlng); return; + } + zoom -= zoomOffset; + var oldValue = options.maxNativeZoom || options.maxZoom; + options.maxNativeZoom = zoom; + var mapZoom = this._map.getZoom(); + if (zoomoldValue && mapZoom>oldValue) { + this._resetView(); + } + this.fire('maxNativeZoomApplied',{ + latlng: latlng, + value: zoom, + oldValue: oldValue + }); + }); + return this; + }, + + _findVintage: function (request, zoomLevel, callback, context) { + // there is no official way, so use heuristic: check `vintageStart` in metadata response + this.callRestService(request + '&zoomLevel='+zoomLevel, function (meta) { + if (meta.resourceSets[0].resources[0].vintageStart || zoomLevel === 0) { + return callback.call(context || this, zoomLevel); + } + this._findVintage(request, zoomLevel-1, callback, context); + }); + } +}); + + +; // eslint-disable-line + } catch (e) { + console.error('Bing.js loading failed'); + throw e; + } +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-blank.meta.js b/static/build/artifact/PR790/plugins/basemap-blank.meta.js new file mode 100644 index 0000000000..c17a3eac22 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-blank.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author jonatkins +// @name IITC plugin: Blank map +// @category Map Tiles +// @version 0.1.6.20241216.092245 +// @description Add a blank map layer - no roads or other features. +// @id basemap-blank +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-blank.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-blank.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-blank.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-blank.user.js b/static/build/artifact/PR790/plugins/basemap-blank.user.js new file mode 100644 index 0000000000..6c4460e0ea --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-blank.user.js @@ -0,0 +1,75 @@ +// ==UserScript== +// @author jonatkins +// @name IITC plugin: Blank map +// @category Map Tiles +// @version 0.1.6.20241216.092245 +// @description Add a blank map layer - no roads or other features. +// @id basemap-blank +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-blank.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-blank.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-blank.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 = 'basemap-blank'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.1.6', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.1.5', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.1.4', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +var mapTileBlank = {}; + +mapTileBlank.addLayer = function () { + var blankOpt = { attribution: '', maxNativeZoom: 18, maxZoom: 21 }; + var blankWhite = new L.TileLayer('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gEFCSU6z3A8pwAAAA1JREFUCNdj+P///38ACfsD/dGDjPAAAAAASUVORK5CYII=', blankOpt); + var blankBlack = new L.TileLayer('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gEFCQkJSZE2HwAAAAxJREFUCNdjYGBgAAAABAABJzQnCgAAAABJRU5ErkJggg==', blankOpt); + + window.layerChooser.addBaseLayer(blankWhite, 'Blank Map (White)'); + window.layerChooser.addBaseLayer(blankBlack, 'Blank Map (Black)'); +}; + +function setup() { + mapTileBlank.addLayer(); +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-gaode.meta.js b/static/build/artifact/PR790/plugins/basemap-gaode.meta.js new file mode 100644 index 0000000000..f543fee2c1 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-gaode.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Gaode (高德地图) / AutoNavi map +// @category Map Tiles +// @version 0.1.4.20241216.092245 +// @description Map layers from AutoNavi / Gaode (高德地图) +// @id basemap-gaode +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-gaode.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-gaode.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-gaode.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-gaode.user.js b/static/build/artifact/PR790/plugins/basemap-gaode.user.js new file mode 100644 index 0000000000..a87a37637f --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-gaode.user.js @@ -0,0 +1,132 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Gaode (高德地图) / AutoNavi map +// @category Map Tiles +// @version 0.1.4.20241216.092245 +// @description Map layers from AutoNavi / Gaode (高德地图) +// @id basemap-gaode +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-gaode.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-gaode.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-gaode.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 = 'basemap-gaode'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.1.4', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.1.3', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.1.2', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +var mapGaode = {}; + +mapGaode.setup = function () { + // sample tile: https://webrd01.is.autonavi.com/appmaptile?style=8&x=13720&y=6693&z=14&lang=zh_cn + + var baseUrl = [ + 'https://wprd0{s}.is.autonavi.com/appmaptile?style={style}&x={x}&y={y}&z={z}', + 'https://webrd0{s}.is.autonavi.com/appmaptile?style={style}&x={x}&y={y}&z={z}&size=1&scale=1', + 'https://webst0{s}.is.autonavi.com/appmaptile?style={style}&x={x}&y={y}&z={z}', // same as wprd0 + ]; + + var GaodeLayer = L.TileLayer.extend({ + options: { + subdomains: '1234', + minZoom: 3, + maxZoom: 20, + maxNativeZoom: 18, + // detectRetina: true, + type: 'roads', + attribution: '© AutoNavi', + needFixChinaOffset: true, // depends on fix-china-map-offset plugin + }, + initialize: function (options) { + function expand(field) { + return options[field] ? `&${field}=${options[field]}` : ''; + } + var extra = expand('lang'); + extra += expand('scl'); + var url = baseUrl[options.site || 0] + extra; + L.TileLayer.prototype.initialize.call(this, url, options); + }, + }); + + var trafficUrl = 'https://tm.amap.com/trafficengine/mapabc/traffictile?v=1.0&;t=1&z={z}&y={y}&x={x}&t={time}'; + var AmapTraffic = GaodeLayer.extend({ + getTileUrl: function (coords) { + this.options.time = new Date().getTime(); + return L.TileLayer.prototype.getTileUrl.call(this, coords); + }, + initialize: function (options) { + L.TileLayer.prototype.initialize.call(this, trafficUrl, options); + }, + minZoom: 6, + maxNativeZoom: 17, + }); + + function add(name, layer) { + window.layerChooser.addBaseLayer(layer, name); + return layer; + } + + var Roads = // en, zh_en + add('Gaode Roads [zh]', new GaodeLayer({ style: 7, maxNativeZoom: 20, lang: 'zh_cn' })); + + // add('Gaode Roads', new GaodeLayer({ style: 7, maxNativeZoom: 20 })); + // add('Gaode Roads 7', new GaodeLayer({ style: 7, site: 1 })); + // add('Gaode Roads 8', new GaodeLayer({ style: 8, site: 1 })); + // add('Gaode Roads 8 [zh]',new GaodeLayer({ style: 8, site: 1, lang: 'zh_cn' })); + + add('Gaode Roads + Traffic', L.layerGroup([Roads, new AmapTraffic({ opacity: 0.75 })])); + + var Satellite = add('Gaode Satellite', new GaodeLayer({ style: 6, type: 'satellite' })); + + // new GaodeLayer({ style: 8, type: 'roadnet', opacity: 0.75, lang: 'zh_cn', scl: 2 }), // (512*512 tile, w/o labels) + // new GaodeLayer({ style: 8, type: 'labels', opacity: 0.75, lang: 'zh_cn', ltype: 4 }) // (feature mask) here: 2: roads, 4: labels) + add('Gaode Hybrid', L.layerGroup([Satellite, new GaodeLayer({ style: 8, type: 'roadnet', opacity: 0.75 })])); +}; + +function setup() { + mapGaode.setup(); +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-google-gray.meta.js b/static/build/artifact/PR790/plugins/basemap-google-gray.meta.js new file mode 100644 index 0000000000..326ba2870c --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-google-gray.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author jacob1123 +// @name IITC plugin: Gray Google map +// @category Map Tiles +// @version 0.1.7.20241216.092245 +// @description Add a simplified gray Version of Google map tiles as an optional layer. +// @id basemap-google-gray +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-google-gray.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-google-gray.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-google-gray.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-google-gray.user.js b/static/build/artifact/PR790/plugins/basemap-google-gray.user.js new file mode 100644 index 0000000000..c07645aef2 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-google-gray.user.js @@ -0,0 +1,88 @@ +// ==UserScript== +// @author jacob1123 +// @name IITC plugin: Gray Google map +// @category Map Tiles +// @version 0.1.7.20241216.092245 +// @description Add a simplified gray Version of Google map tiles as an optional layer. +// @id basemap-google-gray +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-google-gray.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-google-gray.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-google-gray.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 = 'basemap-google-gray'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.1.7', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.1.6', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.1.5', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +var grayGMaps = {}; + +grayGMaps.addLayer = function () { + var grayGMapsOptions = { + maxZoom: 21, + styles: [ + { featureType: 'landscape.natural', stylers: [{ visibility: 'simplified' }, { saturation: -100 }, { lightness: -80 }, { gamma: 2.44 }] }, + { featureType: 'road', stylers: [{ visibility: 'simplified' }, { color: '#bebebe' }, { weight: 0.6 }] }, + { featureType: 'poi', stylers: [{ saturation: -100 }, { visibility: 'on' }, { gamma: 0.34 }] }, + { featureType: 'water', stylers: [{ color: '#32324f' }] }, + { featureType: 'transit', stylers: [{ visibility: 'off' }] }, + { featureType: 'road', elementType: 'labels', stylers: [{ visibility: 'off' }] }, + { featureType: 'poi', elementType: 'labels', stylers: [{ visibility: 'off' }] }, + { featureType: 'poi' }, + { featureType: 'landscape.man_made', stylers: [{ saturation: -100 }, { gamma: 0.13 }] }, + { featureType: 'water', elementType: 'labels', stylers: [{ visibility: 'off' }] }, + ], + }; + + var grayGMaps = L.gridLayer.googleMutant(grayGMapsOptions); + + window.layerChooser.addBaseLayer(grayGMaps, 'Google Gray'); +}; + +function setup() { + grayGMaps.addLayer(); +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-kartverket.meta.js b/static/build/artifact/PR790/plugins/basemap-kartverket.meta.js new file mode 100644 index 0000000000..c5a3f86e93 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-kartverket.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Kartverket.no maps (Norway) +// @category Map Tiles +// @version 0.3.1.20241216.092245 +// @description Add Kartverket.no map layers. +// @id basemap-kartverket +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-kartverket.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-kartverket.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-kartverket.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-kartverket.user.js b/static/build/artifact/PR790/plugins/basemap-kartverket.user.js new file mode 100644 index 0000000000..e15a1a91ce --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-kartverket.user.js @@ -0,0 +1,136 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Kartverket.no maps (Norway) +// @category Map Tiles +// @version 0.3.1.20241216.092245 +// @description Add Kartverket.no map layers. +// @id basemap-kartverket +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-kartverket.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-kartverket.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-kartverket.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 = 'basemap-kartverket'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.3.1', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.3.0', + changes: [ + 'Migrated to new WMTS server due to deprecation of Statkart opencache', + 'Version upgrade due to a change in the wrapper: plugin icons are now vectorized', + ], + }, + { + version: '0.2.3', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +var mapKartverket = {}; + +mapKartverket.setup = function () { + L.TileLayer.Kartverket = L.TileLayer.extend({ + baseUrl: 'https://cache.kartverket.no/v1/wmts/1.0.0/' + '{layer}/default/webmercator/{z}/{y}/{x}.png', + + options: { + maxNativeZoom: 18, + attribution: '© Kartverket', + }, + + mappings: { + bakgrunnskart_forenklet: 'topograatone', + egk: 'topo', // *1 + europa: 'topo', // *1 + havbunn_grunnkart: 'topo', // *1 + kartdata2: 'topo', + matrikkel_bakgrunn: 'topo', + matrikkel_bakgrunn2: 'topo', + norges_grunnkart: 'topo', + norges_grunnkart_graatone: 'topograatone', + norgeskart_bakgrunn: 'topo', + sjo_hovedkart2: 'topo', // *1 + sjokartraster: 'topo', // *1 + terreng_norgeskart: 'topo', + toporaster3: 'toporaster', + topo2: 'topo', + topo4: 'topo', + topo2graatone: 'topograatone', + topo4graatone: 'topograatone', + // *1 = This layer is not provided on cache.kartverket.no. + }, + + layers: { + topo: 'Kartverket Topo (farger)', + topograatone: 'Kartverket Topo (gråtone)', + toporaster: 'Kartverket Topo (raster)', + }, + + initialize: function (layer, options) { + if (typeof this.layers[layer] === 'undefined') { + if (this.mappings[layer]) { + layer = this.mappings[layer]; + } else { + throw new Error('Unknown layer "' + layer + '"'); + } + } + + L.TileLayer.prototype.initialize.call(this, this.baseUrl, options); + this.options.layer = layer; + this._name = this.layers[layer] || layer; + }, + }); + + L.tileLayer.kartverket = function (layer, options) { + return new L.TileLayer.Kartverket(layer, options); + }; + + L.tileLayer.kartverket.getLayers = function () { + return L.extend({}, L.TileLayer.Kartverket.prototype.layers); + }; + + var l, layer; + for (layer in L.tileLayer.kartverket.getLayers()) { + l = L.tileLayer.kartverket(layer); + window.layerChooser.addBaseLayer(l, l._name); + } +}; + +function setup() { + mapKartverket.setup(); +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-openstreetmap.meta.js b/static/build/artifact/PR790/plugins/basemap-openstreetmap.meta.js new file mode 100644 index 0000000000..af2244ca75 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-openstreetmap.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author jonatkins +// @name IITC plugin: OpenStreetMap.org map +// @category Map Tiles +// @version 0.1.6.20241216.092245 +// @description Add the native OpenStreetMap.org map tiles as an optional layer. +// @id basemap-openstreetmap +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-openstreetmap.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-openstreetmap.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-openstreetmap.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-openstreetmap.user.js b/static/build/artifact/PR790/plugins/basemap-openstreetmap.user.js new file mode 100644 index 0000000000..ef3a0192d4 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-openstreetmap.user.js @@ -0,0 +1,104 @@ +// ==UserScript== +// @author jonatkins +// @name IITC plugin: OpenStreetMap.org map +// @category Map Tiles +// @version 0.1.6.20241216.092245 +// @description Add the native OpenStreetMap.org map tiles as an optional layer. +// @id basemap-openstreetmap +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-openstreetmap.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-openstreetmap.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-openstreetmap.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 = 'basemap-openstreetmap'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +// use own namespace for plugin +var mapOpenStreetMap = {}; +window.plugin.mapOpenStreetMap = mapOpenStreetMap; + +var changelog = [ + { + version: '0.1.6', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.1.5', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.1.4', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, + { + version: '0.1.3', + changes: ['Update OSM tile provider', 'Add CyclOSM tiles', 'Expose config'], + }, +]; + +// https://wiki.openstreetmap.org/wiki/Raster_tile_providers + +// Common options +var osmOpt = { + attribution: 'Map data © OpenStreetMap contributors', + maxNativeZoom: 18, + maxZoom: 21, +}; + +mapOpenStreetMap.LAYERS = [ + { + name: 'OpenStreetMap', + url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + options: Object.assign({}, osmOpt), + }, + { + name: 'Humanitarian', + url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + options: Object.assign({}, osmOpt), + }, + { + name: 'CyclOSM', + url: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + options: Object.assign({}, osmOpt), + }, +]; + +function setup() { + // OpenStreetMap tiles - we shouldn't use these by default - https://wiki.openstreetmap.org/wiki/Tile_usage_policy + // "Heavy use (e.g. distributing an app that uses tiles from openstreetmap.org) is forbidden without prior permission from the System Administrators" + + for (var entry of mapOpenStreetMap.LAYERS) { + var layer = new L.TileLayer(entry.url, entry.options); + window.layerChooser.addBaseLayer(layer, entry.name); + } +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-stamen.meta.js b/static/build/artifact/PR790/plugins/basemap-stamen.meta.js new file mode 100644 index 0000000000..ce22f2e18a --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-stamen.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author jonatkins +// @name IITC plugin: Stamen.com map layers +// @category Map Tiles +// @version 0.2.5.20241216.092245 +// @description Add the 'Toner' and 'Watercolor' map layers from maps.stamen.com. +// @id basemap-stamen +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-stamen.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-stamen.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-stamen.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-stamen.user.js b/static/build/artifact/PR790/plugins/basemap-stamen.user.js new file mode 100644 index 0000000000..1b28212c20 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-stamen.user.js @@ -0,0 +1,124 @@ +// ==UserScript== +// @author jonatkins +// @name IITC plugin: Stamen.com map layers +// @category Map Tiles +// @version 0.2.5.20241216.092245 +// @description Add the 'Toner' and 'Watercolor' map layers from maps.stamen.com. +// @id basemap-stamen +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-stamen.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-stamen.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-stamen.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 = 'basemap-stamen'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.2.5', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.2.4', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.2.3', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +var mapStamen = {}; + +// see API here http://maps.stamen.com/ +// https://stamen-maps.a.ssl.fastly.net/js/tile.stamen.js (overcomplicated) + +mapStamen.setup = function () { + var baseUrl = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{layer}/{z}/{x}/{y}.{type}'; + var L_StamenTileLayer = L.TileLayer.extend({ + options: { + subdomains: 'abcd', + type: 'png', + minZoom: 0, + maxZoom: 21, + attribution: [ + 'Map tiles by Stamen Design, ', + 'under CC BY 3.0. ', + 'Data by OpenStreetMap, ', + 'under CC BY SA.', + ].join(''), + }, + initialize: function (name, options) { + options.layer = name.replace(' ', '-').toLowerCase(); + L.TileLayer.prototype.initialize.call(this, baseUrl, options); + }, + }); + + function addLayer(name, options) { + window.layerChooser.addBaseLayer(new L_StamenTileLayer(name, options), 'Stamen ' + name); + } + + var options = { minZoom: 0, maxNativeZoom: 20 }; + addLayer('Toner', options); + addLayer('Toner Background', options); + addLayer('Toner Lite', options); + // transparent layers. could be useful over satellite imagery or similar + // addLayer('Toner Hybrid',options); + // addLayer('Toner Labels',options); + // addLayer('Toner Lines',options); + + options = { minZoom: 1, maxNativeZoom: 13 }; // Should support up to 18, but too many 404 on zoom > 13 + // addLayer('Terrain',options); + // addLayer('Terrain Labels',options); + // addLayer('Terrain Lines',options); + // addLayer('Terrain Background',options); + + options = { + minZoom: 1, + maxZoom: 21, + maxNativeZoom: 18, + type: 'jpg', + attribution: [ + 'Map tiles by Stamen Design, ', + 'under CC BY 3.0. ', + 'Data by OpenStreetMap, ', + 'under ODbL.', + ].join(''), + }; + addLayer('Watercolor', options); +}; + +function setup() { + mapStamen.setup(); +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/basemap-yandex.meta.js b/static/build/artifact/PR790/plugins/basemap-yandex.meta.js new file mode 100644 index 0000000000..59520f89ff --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-yandex.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Yandex maps +// @category Map Tiles +// @version 0.3.4.20241216.092245 +// @description Add Yandex.com (Russian/Русский) map layers +// @id basemap-yandex +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-yandex.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-yandex.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-yandex.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/basemap-yandex.user.js b/static/build/artifact/PR790/plugins/basemap-yandex.user.js new file mode 100644 index 0000000000..1f508c6b69 --- /dev/null +++ b/static/build/artifact/PR790/plugins/basemap-yandex.user.js @@ -0,0 +1,390 @@ +// ==UserScript== +// @author johnd0e +// @name IITC plugin: Yandex maps +// @category Map Tiles +// @version 0.3.4.20241216.092245 +// @description Add Yandex.com (Russian/Русский) map layers +// @id basemap-yandex +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/basemap-yandex.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/basemap-yandex.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/basemap-yandex.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 = 'basemap-yandex'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.3.4', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.3.3', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.3.2', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +var mapYandex = {}; + +mapYandex.types = { + map: { + type: 'map', + }, + satellite: { + type: 'satellite', + }, + hybrid: { + type: 'hybrid', + }, +}; + +mapYandex.options = { + // set this to your API key + apiParams: '', +}; + +function setup() { + setupYandexLeaflet(); + + for (var name in mapYandex.types) { + var options = L.extend({}, mapYandex.options, mapYandex.types[name]); + window.layerChooser.addBaseLayer(L.yandex(options), 'Yandex ' + name); + } +} + +function setupYandexLeaflet() { + try { + // https://github.com/shramov/leaflet-plugins/blob/master/layer/tile/Yandex.js + // *** included: external/Yandex.js *** +// https://tech.yandex.com/maps/doc/jsapi/2.1/quick-start/index-docpage/ + +/* global ymaps: true */ + +L.Yandex = L.Layer.extend({ + + options: { + type: 'yandex#map', // 'map', 'satellite', 'hybrid', 'map~vector' | 'overlay', 'skeleton' + mapOptions: { // https://tech.yandex.com/maps/doc/jsapi/2.1/ref/reference/Map-docpage/#Map__param-options + // yandexMapDisablePoiInteractivity: true, + balloonAutoPan: false, + suppressMapOpenBlock: true + }, + overlayOpacity: 0.8, + minZoom: 0, + maxZoom: 19 + }, + + initialize: function (type, options) { + if (typeof type === 'object') { + options = type; + type = false; + } + options = L.Util.setOptions(this, options); + if (type) { options.type = type; } + this._isOverlay = options.type.indexOf('overlay') !== -1 || + options.type.indexOf('skeleton') !== -1; + this._animatedElements = []; + }, + + _setStyle: function (el, style) { + for (var prop in style) { + el.style[prop] = style[prop]; + } + }, + + _initContainer: function (parentEl) { + var zIndexClass = this._isOverlay ? 'leaflet-overlay-pane' : 'leaflet-tile-pane'; + var _container = L.DomUtil.create('div', 'leaflet-yandex-container leaflet-pane ' + zIndexClass); + var opacity = this.options.opacity || this._isOverlay && this.options.overlayOpacity; + if (opacity) { + L.DomUtil.setOpacity(_container, opacity); + } + var auto = {width: '100%', height: '100%'}; + this._setStyle(parentEl, auto); // need to set this explicitly, + this._setStyle(_container, auto); // otherwise ymaps fails to follow container size changes + return _container; + }, + + onAdd: function (map) { + var mapPane = map.getPane('mapPane'); + if (!this._container) { + this._container = this._initContainer(mapPane); + map.once('unload', this._destroy, this); + this._initApi(); + } + mapPane.appendChild(this._container); + if (!this._yandex) { return; } + this._setEvents(map); + this._update(); + }, + + beforeAdd: function (map) { + map._addZoomLimit(this); + }, + + onRemove: function (map) { + map._removeZoomLimit(this); + }, + + _destroy: function (e) { + if (!this._map || this._map === e.target) { + if (this._yandex) { + this._yandex.destroy(); + delete this._yandex; + } + delete this._container; + } + }, + + _setEvents: function (map) { + var events = { + move: this._update, + resize: function () { + this._yandex.container.fitToViewport(); + } + }; + if (this._zoomAnimated) { + events.zoomanim = this._animateZoom; + events.zoomend = this._animateZoomEnd; + } + map.on(events, this); + this.once('remove', function () { + map.off(events, this); + this._container.remove(); // we do not call this until api is initialized (ymaps API expects DOM element) + }, this); + }, + + _update: function () { + var map = this._map; + var center = map.getCenter(); + this._yandex.setCenter([center.lat, center.lng], map.getZoom()); + var offset = L.point(0,0).subtract(L.DomUtil.getPosition(map.getPane('mapPane'))); + L.DomUtil.setPosition(this._container, offset); // move to visible part of pane + }, + + _resyncView: function () { // for use in addons + if (!this._map) { return; } + var ymap = this._yandex; + this._map.setView(ymap.getCenter(), ymap.getZoom(), {animate: false}); + }, + + _animateZoom: function (e) { + var map = this._map; + var viewHalf = map.getSize()._divideBy(2); + var topLeft = map.project(e.center, e.zoom)._subtract(viewHalf)._round(); + var offset = map.project(map.getBounds().getNorthWest(), e.zoom)._subtract(topLeft); + var scale = map.getZoomScale(e.zoom); + this._animatedElements.length = 0; + this._yandex.panes._array.forEach(function (el) { + if (el.pane instanceof ymaps.pane.MovablePane) { + var element = el.pane.getElement(); + L.DomUtil.addClass(element, 'leaflet-zoom-animated'); + L.DomUtil.setTransform(element, offset, scale); + this._animatedElements.push(element); + } + },this); + }, + + _animateZoomEnd: function () { + this._animatedElements.forEach(function (el) { + L.DomUtil.setTransform(el, 0, 1); + }); + this._animatedElements.length = 0; + }, + + _initApi: function () { // to be extended in addons + ymaps.ready(this._initMapObject, this); + }, + + _mapType: function () { + var shortType = this.options.type; + if (!shortType || shortType.indexOf('#') !== -1) { + return shortType; + } + return 'yandex#' + shortType; + }, + + _initMapObject: function () { + ymaps.mapType.storage.add('yandex#overlay', new ymaps.MapType('overlay', [])); + ymaps.mapType.storage.add('yandex#skeleton', new ymaps.MapType('skeleton', ['yandex#skeleton'])); + ymaps.mapType.storage.add('yandex#map~vector', new ymaps.MapType('map~vector', ['yandex#map~vector'])); + var ymap = new ymaps.Map(this._container, { + center: [0, 0], zoom: 0, behaviors: [], controls: [], + type: this._mapType() + }, this.options.mapOptions); + + if (this._isOverlay) { + ymap.container.getElement().style.background = 'transparent'; + } + this._container.remove(); + this._yandex = ymap; + if (this._map) { this.onAdd(this._map); } + + this.fire('load'); + } +}); + +L.yandex = function (type, options) { + return new L.Yandex(type, options); +}; + + +; // eslint-disable-line + + // *** included: external/Yandex.addon.LoadApi.js *** +// @options apiLoader: function or thennable = undefined +// Function that will be used to load Yandex JS API (if it turns out not enabled on layer add). +// Must return any Promise-like thennable object. +// Instead of function it's also possible to specify Promise/thennable directly as option value. + +// Alternatively: +// Predefined loader will be used if apiUrl / apiParams specified. + +// @options apiVersion: string = '2.1' +// Can be specified to use api version other then default, +// more info: https://tech.yandex.com/maps/jsapi/doc/2.1/versions/index-docpage/ + +// @options apiUrl: string = 'https://api-maps.yandex.ru/{version}/' +// This may need to be changed for using commercial versions of the api. +// It's also possible to directly include params in apiUrl. +// Please note that some parameters are mandatory, +// more info: https://tech.yandex.com/maps/jsapi/doc/2.1/dg/concepts/load-docpage/ + +// @option apiParams: object or string +// Parameters to use when enabling API. +// There are some predefined defaults (see in code), but 'apikey' is still mandatory. +// It's also possible to specify apikey directly as apiParams string value. + +// @method apiLoad(options?: Object): this +// Loads API immediately. +// If API loader / params are not specified in layer options, +// they must be provided in `options` argument (otherwise it may be omitted). + +/* global ymaps: true */ + +L.Yandex.include({ + _initLoader: function (options) { + if (this._loader) { return; } + options = options || this.options; + var loader = options.apiLoader; + if (loader) { + if (loader.then) { loader = {loading: loader}; } + } else { + var url = this._makeUrl(options); + loader = url && this._loadScript.bind(this,url); + } + if (loader) { + L.Yandex.prototype._loader = loader; + } + }, + + loadApi: function (options) { + if (typeof ymaps !== 'undefined') { return this; } + this._initLoader(options); + var loader = this._loader; + if (!loader) { + throw new Error('api params expected in options'); + } + if (!loader.loading) { + loader.loading = loader(); + } + return this; + }, + + _initApi: function (afterload) { + var loader = this._loader; + if (typeof ymaps !== 'undefined') { + return ymaps.ready(this._initMapObject, this); + } else if (afterload || !loader) { + throw new Error('API is not available'); + } + var loading = loader.loading; + if (!loading) { + loading = loader(); + loader.loading = loading; + } + loading.then(this._initApi.bind(this,'afterload')); + }, + + _apiDefaults: { // https://tech.yandex.com/maps/jsapi/doc/2.1/dg/concepts/load-docpage/ + url: 'https://api-maps.yandex.ru/{version}/', + version: '2.1', + params: { + lang: 'ru_RU', + onerror: 'console.error' + } + }, + + _makeUrl: function (options) { + var url = options.apiUrl, + params = options.apiParams, + def = this._apiDefaults; + if (!url && !params) { return false; } + if (params) { + if (typeof params === 'string') { params = { apikey: params }; } + params = L.extend({}, def.params, params); + url = (url || def.url) + + L.Util.getParamString(params,url); + } + return L.Util.template(url, { version: options.apiVersion || def.version }); + }, + + _loadScript: function (url) { + return new Promise(function (resolve, reject) { + var script = document.createElement('script'); + script.onload = resolve; + script.onerror = function () { + reject('API loading failed'); + }; + script.src = url; + document.body.appendChild(script); + }); + } + +}); + +L.Yandex.addInitHook(L.Yandex.prototype._initLoader); + + +; // eslint-disable-line + + } catch (e) { + console.error('Yandex.js loading failed'); + throw e; + } +} + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/bookmarks.meta.js b/static/build/artifact/PR790/plugins/bookmarks.meta.js new file mode 100644 index 0000000000..71f275a47c --- /dev/null +++ b/static/build/artifact/PR790/plugins/bookmarks.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author ZasoGD +// @name IITC plugin: Bookmarks for maps and portals +// @category Controls +// @version 0.4.6.20241216.092245 +// @description Save your favorite Maps and Portals and move the intel map with a click. Works with sync. Supports Multi-Project-Extension +// @id bookmarks +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/bookmarks.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/bookmarks.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/bookmarks.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/bookmarks.user.js b/static/build/artifact/PR790/plugins/bookmarks.user.js new file mode 100644 index 0000000000..d5918a4471 --- /dev/null +++ b/static/build/artifact/PR790/plugins/bookmarks.user.js @@ -0,0 +1,2239 @@ +// ==UserScript== +// @author ZasoGD +// @name IITC plugin: Bookmarks for maps and portals +// @category Controls +// @version 0.4.6.20241216.092245 +// @description Save your favorite Maps and Portals and move the intel map with a click. Works with sync. Supports Multi-Project-Extension +// @id bookmarks +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/bookmarks.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/bookmarks.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/bookmarks.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 = 'bookmarks'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global IITC, L -- eslint */ + +var changelog = [ + { + version: '0.4.6', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.4.5', + changes: ['Simplying API for adding a new portal', 'Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.4.4', + changes: ['IITC.toolbox API is used to create plugin buttons'], + }, + { + version: '0.4.3', + changes: ['Extracted "formatDistance" function for global use'], + }, +]; + +/* ********************************************************************** + + HOOKS: + - pluginBkmrksEdit: fired when a bookmarks/folder is removed, added or sorted, also when a folder is opened/closed; + - pluginBkmrksOpenOpt: fired when the "Bookmarks Options" panel is opened (you can add new options); + - pluginBkmrksSyncEnd: fired when the sync is finished; + +********************************************************************** */ + +// use own namespace for plugin +window.plugin.bookmarks = function () {}; + +window.plugin.bookmarks.SYNC_DELAY = 5000; + +window.plugin.bookmarks.KEY_OTHER_BKMRK = 'idOthers'; +window.plugin.bookmarks.KEY_STORAGE = 'plugin-bookmarks'; +window.plugin.bookmarks.KEY_STATUS_BOX = 'plugin-bookmarks-box'; + +window.plugin.bookmarks.KEY = { key: window.plugin.bookmarks.KEY_STORAGE, field: 'bkmrksObj' }; +window.plugin.bookmarks.IsDefaultStorageKey = true; // as default on startup +window.plugin.bookmarks.UPDATE_QUEUE = { key: 'plugin-bookmarks-queue', field: 'updateQueue' }; +window.plugin.bookmarks.UPDATING_QUEUE = { key: 'plugin-bookmarks-updating-queue', field: 'updatingQueue' }; + +window.plugin.bookmarks.bkmrksObj = {}; +window.plugin.bookmarks.statusBox = {}; +window.plugin.bookmarks.updateQueue = {}; +window.plugin.bookmarks.updatingQueue = {}; + +window.plugin.bookmarks.IDcount = 0; + +window.plugin.bookmarks.enableSync = false; + +window.plugin.bookmarks.starLayers = {}; +window.plugin.bookmarks.starLayerGroup = null; + +window.plugin.bookmarks.isSmart = undefined; + +/** *******************************************************************************************************************/ + +// Generate an ID for the bookmark (date time + random number) +window.plugin.bookmarks.generateID = function () { + var d = new Date(); + var ID = d.getTime().toString() + window.plugin.bookmarks.IDcount.toString() + (Math.floor(Math.random() * 99) + 1); + window.plugin.bookmarks.IDcount++; + ID = 'id' + ID.toString(); + return ID; +}; + +// Format the string +window.plugin.bookmarks.escapeHtml = function (text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/') + .replace(/\\/g, '\'); +}; + +window.plugin.bookmarks.escapeUnicode = function (str) { + for (var result = '', index = 0, charCode; !isNaN((charCode = str.charCodeAt(index))); ) { + if ((charCode & 127) === charCode) { + result += str[index]; + } else { + result += '\\u' + ('0000' + charCode.toString(16)).slice(-4); + } + index++; + } + return result; +}; + +// Update the localStorage +window.plugin.bookmarks.saveStorage = function () { + localStorage[window.plugin.bookmarks.KEY_STORAGE] = JSON.stringify(window.plugin.bookmarks.bkmrksObj); +}; + +// Load the localStorage +window.plugin.bookmarks.loadStorage = function () { + window.plugin.bookmarks.bkmrksObj = JSON.parse(localStorage[window.plugin.bookmarks.KEY_STORAGE]); +}; + +window.plugin.bookmarks.saveStorageBox = function () { + localStorage[window.plugin.bookmarks.KEY_STATUS_BOX] = JSON.stringify(window.plugin.bookmarks.statusBox); +}; + +window.plugin.bookmarks.loadStorageBox = function () { + window.plugin.bookmarks.statusBox = JSON.parse(localStorage[window.plugin.bookmarks.KEY_STATUS_BOX]); +}; + +window.plugin.bookmarks.upgradeToNewStorage = function () { + if (localStorage['plugin-bookmarks-portals-data'] && localStorage['plugin-bookmarks-maps-data']) { + var oldStor_1 = JSON.parse(localStorage['plugin-bookmarks-maps-data']); + var oldStor_2 = JSON.parse(localStorage['plugin-bookmarks-portals-data']); + + window.plugin.bookmarks.bkmrksObj.maps = oldStor_1.bkmrk_maps; + window.plugin.bookmarks.bkmrksObj.portals = oldStor_2.bkmrk_portals; + window.plugin.bookmarks.saveStorage(); + + localStorage.removeItem('plugin-bookmarks-maps-data'); + localStorage.removeItem('plugin-bookmarks-portals-data'); + localStorage.removeItem('plugin-bookmarks-status-box'); + } +}; + +window.plugin.bookmarks.createStorage = function () { + if (!localStorage[window.plugin.bookmarks.KEY_STORAGE]) { + window.plugin.bookmarks.bkmrksObj.maps = { idOthers: { label: 'Others', state: 1, bkmrk: {} } }; + window.plugin.bookmarks.bkmrksObj.portals = { idOthers: { label: 'Others', state: 1, bkmrk: {} } }; + window.plugin.bookmarks.saveStorage(); + } + if (!localStorage[window.plugin.bookmarks.KEY_STATUS_BOX]) { + window.plugin.bookmarks.statusBox.show = 1; + window.plugin.bookmarks.statusBox.page = 0; + window.plugin.bookmarks.statusBox.pos = { x: 100, y: 100 }; + window.plugin.bookmarks.saveStorageBox(); + } +}; + +window.plugin.bookmarks.refreshBkmrks = function () { + $('#bkmrk_maps > ul, #bkmrk_portals > ul').remove(); + + window.plugin.bookmarks.loadStorage(); + window.plugin.bookmarks.loadList('maps'); + window.plugin.bookmarks.loadList('portals'); + + window.plugin.bookmarks.updateStarPortal(); + window.plugin.bookmarks.jquerySortableScript(); +}; + +/** *************************************************************************************************************************************************************/ + +// Show/hide the bookmarks box +window.plugin.bookmarks.switchStatusBkmrksBox = function (status) { + var newStatus = status; + + if (newStatus === 'switch') { + if (window.plugin.bookmarks.statusBox.show === 1) { + newStatus = 0; + } else { + newStatus = 1; + } + } + + if (newStatus === 1) { + $('#bookmarksBox').css('height', 'auto'); + $('#bkmrksTrigger').css('height', '0'); + } else { + $('#bkmrksTrigger').css('height', '64px'); + $('#bookmarksBox').css('height', '0'); + } + + window.plugin.bookmarks.statusBox['show'] = newStatus; + window.plugin.bookmarks.saveStorageBox(); +}; + +window.plugin.bookmarks.onPaneChanged = function (pane) { + if (pane === 'plugin-bookmarks') $('#bookmarksBox').css('display', ''); + else $('#bookmarksBox').css('display', 'none'); +}; + +// Switch list (maps/portals) +window.plugin.bookmarks.switchPageBkmrksBox = function (elem, page) { + window.plugin.bookmarks.statusBox.page = page; + window.plugin.bookmarks.saveStorageBox(); + + $('h5').removeClass('current'); + $(elem).addClass('current'); + + var sectList = '#' + $(elem).attr('class').replace(' current', ''); + $('#bookmarksBox .bookmarkList').removeClass('current'); + $(sectList).addClass('current'); +}; + +// Switch the status folder to open/close (in the localStorage) +window.plugin.bookmarks.openFolder = function (elem) { + $(elem).parent().parent('li').toggleClass('open'); + + var typeList = $(elem).parent().parent().parent().parent('div').attr('id').replace('bkmrk_', ''); + var ID = $(elem).parent().parent('li').attr('id'); + + var newFlag; + var flag = window.plugin.bookmarks.bkmrksObj[typeList][ID]['state']; + if (flag) { + newFlag = 0; + } else if (!flag) { + newFlag = 1; + } + + window.plugin.bookmarks.bkmrksObj[typeList][ID]['state'] = newFlag; + window.plugin.bookmarks.saveStorage(); + window.runHooks('pluginBkmrksEdit', { target: 'folder', action: newFlag ? 'open' : 'close', id: ID }); +}; + +// Load the HTML bookmarks +window.plugin.bookmarks.loadList = function (typeList) { + var element = ''; + var elementTemp = ''; + var elementExc = ''; + var returnToMap = ''; + + if (window.plugin.bookmarks.isSmart) { + returnToMap = "window.show('map');"; + } + + // For each folder + var list = window.plugin.bookmarks.bkmrksObj[typeList]; + + for (var idFolders in list) { + var folders = list[idFolders]; + var active = ''; + + // Create a label and a anchor for the sortable + var folderDelete = + 'X'; + var folderName = + '' + folders['label'] + ''; // '; + var folderLabel = folderDelete + folderName; + + if (folders['state']) { + active = ' open'; + } + if (idFolders === window.plugin.bookmarks.KEY_OTHER_BKMRK) { + folderLabel = ''; + active = ' othersBookmarks open'; + } + // Create a folder + elementTemp = `
  • ${folderLabel}
      `; + + // For each bookmark + var fold = folders['bkmrk']; + for (var idBkmrk in fold) { + var btn_link; + var btn_remove = `X`; + + var btn_move = ''; + if (window.plugin.bookmarks.isSmart) { + btn_move = `=`; + } + + var bkmrk = fold[idBkmrk]; + var label = bkmrk['label']; + var latlng = bkmrk['latlng']; + + // If it's a map + if (typeList === 'maps') { + if (bkmrk['label'] === '') { + label = bkmrk['latlng'] + ' [' + bkmrk['z'] + ']'; + } + btn_link = `${label}`; + } + // If it's a portal + else if (typeList === 'portals') { + var guid = bkmrk['guid']; + const bookmarks_onclick = `$('a.bookmarksLink.selected').removeClass('selected');${returnToMap}window.zoomToAndShowPortal('${guid}', [${latlng}]);return false;`; + btn_link = `${label}`; + } + // Create the bookmark + elementTemp += '
    • ' + btn_remove + btn_move + btn_link + '
    • '; + } + elementTemp += '
    '; + + // Add folder 'Others' in last position + if (idFolders !== window.plugin.bookmarks.KEY_OTHER_BKMRK) { + element += elementTemp; + } else { + elementExc = elementTemp; + } + } + element += elementExc; + element = '
      ' + element + '
    '; + + // Append all folders and bookmarks + $('#bkmrk_' + typeList).append(element); +}; + +/** *************************************************************************************************************************************************************/ + +window.plugin.bookmarks.findByGuid = function (guid) { + var list = window.plugin.bookmarks.bkmrksObj['portals']; + + for (var idFolders in list) { + for (var idBkmrk in list[idFolders]['bkmrk']) { + var portalGuid = list[idFolders]['bkmrk'][idBkmrk]['guid']; + if (guid === portalGuid) { + return { id_folder: idFolders, id_bookmark: idBkmrk }; + } + } + } +}; + +// Append a 'star' flag in sidebar. +window.plugin.bookmarks.onPortalSelectedPending = false; +window.plugin.bookmarks.onPortalSelected = function () { + $('.bkmrksStar').remove(); + + if (window.selectedPortal === null) return; + + if (!window.plugin.bookmarks.onPortalSelectedPending) { + window.plugin.bookmarks.onPortalSelectedPending = true; + + setTimeout(function () { + // the sidebar is constructed after firing the hook + window.plugin.bookmarks.onPortalSelectedPending = false; + + $('.bkmrksStar').remove(); + + if (typeof Storage === 'undefined') { + $('#portaldetails > .imgpreview').after(window.plugin.bookmarks.htmlDisabledMessage); + return; + } + + // Prepend a star to mobile status-bar + if (window.plugin.bookmarks.isSmart) { + $('#updatestatus').prepend(window.plugin.bookmarks.htmlStar); + $('#updatestatus .bkmrksStar').attr('title', ''); + } + + $('#portaldetails > h3.title').prepend(window.plugin.bookmarks.htmlStar); + window.plugin.bookmarks.updateStarPortal(); + }, 0); + } +}; + +// Update the status of the star (when a portal is selected from the map/bookmarks-list) +window.plugin.bookmarks.updateStarPortal = function () { + var guid = window.selectedPortal; + $('.bkmrksStar').removeClass('favorite'); + $('.bkmrk a.bookmarksLink.selected').removeClass('selected'); + + // If current portal is into bookmarks: select bookmark portal from portals list and select the star + if (localStorage[window.plugin.bookmarks.KEY_STORAGE].search(guid) !== -1) { + var bkmrkData = window.plugin.bookmarks.findByGuid(guid); + if (bkmrkData) { + $('.bkmrk#' + bkmrkData['id_bookmark'] + ' a.bookmarksLink').addClass('selected'); + $('.bkmrksStar').addClass('favorite'); + } + } +}; + +// Switch the status of the star +window.plugin.bookmarks.switchStarPortal = function (guid) { + if (guid === undefined) guid = window.selectedPortal; + + // If portal is saved in bookmarks: Remove this bookmark + var bkmrkData = window.plugin.bookmarks.findByGuid(guid); + if (bkmrkData) { + var list = window.plugin.bookmarks.bkmrksObj['portals']; + delete list[bkmrkData['id_folder']]['bkmrk'][bkmrkData['id_bookmark']]; + $('.bkmrk#' + bkmrkData['id_bookmark'] + '').remove(); + + window.plugin.bookmarks.saveStorage(); + window.plugin.bookmarks.updateStarPortal(); + + window.runHooks('pluginBkmrksEdit', { target: 'portal', action: 'remove', folder: bkmrkData['id_folder'], id: bkmrkData['id_bookmark'], guid: guid }); + console.log('BOOKMARKS: removed portal (' + bkmrkData['id_bookmark'] + ' situated in ' + bkmrkData['id_folder'] + ' folder)'); + } + // If portal isn't saved in bookmarks: Add this bookmark + else { + window.plugin.bookmarks.addPortalBookmarkByGuid(guid, true); + } +}; + +/** + * Adds a portal to the default bookmark folder. + * + * @param {L.circleMarker} marker - As enhanced when added to + * window.portals. + * @param {boolean} doPostProcess - Whether additional post-processing + * should be done after the bookmark was added. E.g., saving to local + * storage, refreshing the widget, and running hooks. If part of a batch + * update, this should probably be false. + */ +window.plugin.bookmarks.addPortalBookmarkByMarker = function (marker, doPostProcess) { + const guid = marker.options.guid; + const label = marker.options.data.title; + const ll = marker.getLatLng(); + const latlng = `${ll.lat},${ll.lng}`; + const ID = window.plugin.bookmarks.generateID(); + + window.plugin.bookmarks.bkmrksObj['portals'][window.plugin.bookmarks.KEY_OTHER_BKMRK]['bkmrk'][ID] = { + guid: guid, + latlng: latlng, + label: label, + }; + + if (doPostProcess) { + window.plugin.bookmarks.saveStorage(); + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { + target: 'portal', + action: 'add', + id: ID, + guid: guid, + }); + console.log(`BOOKMARKS: added portal ${ID}`); + } +}; + +/** + * Adds a portal to the default bookmark folder. + * + * @param {string} guid - The GUID of the portal. + * @param {boolean} doPostProcess - Whether additional post processing + * should be done after the bookmark was added. E.g., saving to local + * storage, refreshing the widget, and running hooks. If part of a batch + * update, this should probably be false. + * @throws {Error} - If guid does not exist in window.portals. + */ +window.plugin.bookmarks.addPortalBookmarkByGuid = function (guid, doPostProcess) { + const marker = window.portals[guid]; + if (marker) { + window.plugin.bookmarks.addPortalBookmarkByMarker(marker, doPostProcess); + } else { + throw new Error(`Could not find portal information for guid "${guid}"`); + } +}; + +/** + * Adds a portal to the default bookmark folder. + * + * The window.plugin.bookmarks.addPortalBookmarkBy{Guid,Marker}() functions + * should be used for new code. + * + * @deprecated + * @param {string} guid - The GUID of the portal. + * @param {string} latlng - 'lat,lng' for the portal. + * @param {string} label - The title of the portal. Typically this is the + * same value as the options.data.title property from the appropriate + * window.portals entry, though nothing enforces this. + */ +window.plugin.bookmarks.addPortalBookmark = function (guid, latlng, label) { + var ID = window.plugin.bookmarks.generateID(); + + // Add bookmark in the localStorage + window.plugin.bookmarks.bkmrksObj['portals'][window.plugin.bookmarks.KEY_OTHER_BKMRK]['bkmrk'][ID] = { guid: guid, latlng: latlng, label: label }; + + window.plugin.bookmarks.saveStorage(); + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { target: 'portal', action: 'add', id: ID, guid: guid }); + console.log('BOOKMARKS: added portal ' + ID); +}; + +// Add BOOKMARK/FOLDER +window.plugin.bookmarks.addElement = function (elem, type) { + var ID = window.plugin.bookmarks.generateID(); + var typeList = $(elem).parent().parent('div').attr('id'); + + // Get the label | Convert some characters | Set the input (empty) + var input = `#${typeList} .addForm input`; + var label = $(input).val(); + label = window.plugin.bookmarks.escapeHtml(label); + $(input).val(''); + + // Add a map + if (type === 'map') { + // Get the coordinates and zoom + var c = window.map.getCenter(); + var lat = Math.round(c.lat * 1e6) / 1e6; + var lng = Math.round(c.lng * 1e6) / 1e6; + var latlng = lat + ',' + lng; + var zoom = parseInt(window.map.getZoom()); + // Add bookmark in the localStorage + window.plugin.bookmarks.bkmrksObj['maps'][window.plugin.bookmarks.KEY_OTHER_BKMRK]['bkmrk'][ID] = { label: label, latlng: latlng, z: zoom }; + } else { + if (label === '') { + label = 'Folder'; + } + var short_type = typeList.replace('bkmrk_', ''); + // Add new folder in the localStorage + window.plugin.bookmarks.bkmrksObj[short_type][ID] = { label: label, state: 1, bkmrk: {} }; + } + window.plugin.bookmarks.saveStorage(); + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { target: type, action: 'add', id: ID }); + console.log('BOOKMARKS: added ' + type + ' ' + ID); +}; + +// Remove BOOKMARK/FOLDER +window.plugin.bookmarks.removeElement = function (elem, type) { + if (type === 'maps' || type === 'portals') { + const typeList = $(elem).parent().parent().parent().parent().parent('div').attr('id'); + const ID = $(elem).parent('li').attr('id'); + var IDfold = $(elem).parent().parent().parent('li').attr('id'); + var guid = window.plugin.bookmarks.bkmrksObj[typeList.replace('bkmrk_', '')][IDfold]['bkmrk'][ID].guid; + + delete window.plugin.bookmarks.bkmrksObj[typeList.replace('bkmrk_', '')][IDfold]['bkmrk'][ID]; + $(elem).parent('li').remove(); + + if (type === 'portals') { + window.plugin.bookmarks.updateStarPortal(); + window.plugin.bookmarks.saveStorage(); + + window.runHooks('pluginBkmrksEdit', { target: 'portal', action: 'remove', folder: IDfold, id: ID, guid: guid }); + console.log('BOOKMARKS: removed portal (' + ID + ' situated in ' + IDfold + ' folder)'); + } else { + window.plugin.bookmarks.saveStorage(); + window.runHooks('pluginBkmrksEdit', { target: 'map', action: 'remove', id: ID }); + console.log('BOOKMARKS: removed map ' + ID); + } + } else if (type === 'folder') { + const typeList = $(elem).parent().parent().parent().parent('div').attr('id'); + const ID = $(elem).parent().parent('li').attr('id'); + + delete window.plugin.bookmarks.bkmrksObj[typeList.replace('bkmrk_', '')][ID]; + $(elem).parent().parent('li').remove(); + window.plugin.bookmarks.saveStorage(); + window.plugin.bookmarks.updateStarPortal(); + window.runHooks('pluginBkmrksEdit', { target: 'folder', action: 'remove', id: ID }); + console.log('BOOKMARKS: removed folder ' + ID); + } +}; + +window.plugin.bookmarks.deleteMode = function () { + $('#bookmarksBox').removeClass('moveMode').toggleClass('deleteMode'); +}; + +window.plugin.bookmarks.moveMode = function () { + $('#bookmarksBox').removeClass('deleteMode').toggleClass('moveMode'); +}; + +window.plugin.bookmarks.mobileSortIDb = ''; +window.plugin.bookmarks.mobileSortIDf = ''; +window.plugin.bookmarks.dialogMobileSort = function (type, elem) { + window.plugin.bookmarks.mobileSortIDb = $(elem).parent('li.bkmrk').attr('id'); + window.plugin.bookmarks.mobileSortIDf = $(elem).parent('li.bkmrk').parent('ul').parent('li.bookmarkFolder').attr('id'); + + if (type === 'maps') { + type = 1; + } else if (type === 'portals') { + type = 2; + } + + window.dialog({ + html: window.plugin.bookmarks.dialogLoadListFolders('bookmarksDialogMobileSort', 'window.plugin.bookmarks.mobileSort', true, type), + dialogClass: 'ui-dialog-bkmrksSet-copy', + id: 'plugin-bookmarks-move-bookmark', + title: 'Bookmarks - Move Bookmark', + }); +}; + +window.plugin.bookmarks.mobileSort = function (elem) { + var type = $(elem).data('type'); + var idBkmrk = window.plugin.bookmarks.mobileSortIDb; + var newFold = $(elem).data('id'); + var oldFold = window.plugin.bookmarks.mobileSortIDf; + + var Bkmrk = window.plugin.bookmarks.bkmrksObj[type][oldFold].bkmrk[idBkmrk]; + + delete window.plugin.bookmarks.bkmrksObj[type][oldFold].bkmrk[idBkmrk]; + + window.plugin.bookmarks.bkmrksObj[type][newFold].bkmrk[idBkmrk] = Bkmrk; + + window.plugin.bookmarks.saveStorage(); + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { target: 'bookmarks', action: 'sort' }); + window.plugin.bookmarks.mobileSortIDf = newFold; + console.log(`Move Bookmarks ${type} ID:${idBkmrk} from folder ID:${oldFold} to folder ID:${newFold}`); +}; + +window.plugin.bookmarks.onSearch = function (query) { + var term = query.term.toLowerCase(); + + $.each(window.plugin.bookmarks.bkmrksObj.maps, function (id, folder) { + $.each(folder.bkmrk, function (id, bookmark) { + if (bookmark.label.toLowerCase().indexOf(term) === -1) return; + + query.addResult({ + title: window.escapeHtmlSpecialChars(bookmark.label), + description: `Map in folder "${window.escapeHtmlSpecialChars(folder.label)}"`, + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADYSURBVCiRldExLoRxEAXw33xZ4QIKB9BQWPl0yFKJA4hCo1CqXMABVA6gcgkFnc7/s7VQaByAgoYdhU3sZr9NmGSaee/NvJeJUkr6R3WgrusYm/ajJ7zr5t3ouGmarFrXpFPpuA2aFDSxIWxjXz/mWy25jx3hEAsqS0NsFi/68YxHlXPK8MKbGwR6GN06g0XhwYrrX0tb+enJAS5b8pzp5gk5GM+wl1/C1YQgfEwPPbA+JN3iAgMsTxeEOWlXNzet5pHKGl7HOKWUzEx/6VJKdvj54IT3KfUNvrNZ/jYm+uoAAAAASUVORK5CYII=', + position: L.latLng(bookmark.latlng.split(',')), + zoom: bookmark.z, + onSelected: window.plugin.bookmarks.onSearchResultSelected, + }); + }); + }); + + $.each(window.plugin.bookmarks.bkmrksObj.portals, function (id, folder) { + $.each(folder.bkmrk, function (id, bookmark) { + if (bookmark.label.toLowerCase().indexOf(term) === -1) return; + + query.addResult({ + title: window.escapeHtmlSpecialChars(bookmark.label), + description: `Bookmark in folder "${window.escapeHtmlSpecialChars(folder.label)}"`, + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADFSURBVCiRrdEtTkMBEATgb19C0gsgEEgMFW2TooAURzgACoNAorgAB6jqAVBcghBQYPmxBAQeBYYQ4C2CJvTlPUibMG53ZpKZ3chMs6Bo3N7EwG2sTG9Ih9J+ExW1SFexpnCBNyzq5VPdcB2bwi4WsIz5Mf+OR9wrjHTy9DvSi3MEBhNimMOScKfj7KfDRn54sIPjhj5D3Twgy2rp7fwUTuotvU6O1SuVVseiSxyhRPt3Q2hJW7q5rpd7Cn08VyT/8+k/8AVY7Dd1pA43RAAAAABJRU5ErkJggg==', + position: L.latLng(bookmark.latlng.split(',')), + guid: bookmark.guid, + onSelected: window.plugin.bookmarks.onSearchResultSelected, + }); + }); + }); +}; + +window.plugin.bookmarks.onSearchResultSelected = function (result, event) { + if (result.guid) { + // portal + var guid = result.guid; + if (event.type === 'dblclick') window.zoomToAndShowPortal(guid, result.position); + else if (window.portals[guid]) window.renderPortalDetails(guid); + else window.selectPortalByLatLng(result.position); + } else if (result.zoom) { + // map + window.map.setView(result.position, result.zoom); + } + return true; // prevent default behavior +}; + +/** *************************************************************************************************************************************************************/ + +// Saved the new sort of the folders (in the localStorage) +window.plugin.bookmarks.sortFolder = function (typeList) { + var keyType = typeList.replace('bkmrk_', ''); + + var newArr = {}; + $(`#${typeList} li.bookmarkFolder`).each(function () { + var idFold = $(this).attr('id'); + newArr[idFold] = window.plugin.bookmarks.bkmrksObj[keyType][idFold]; + }); + window.plugin.bookmarks.bkmrksObj[keyType] = newArr; + window.plugin.bookmarks.saveStorage(); + + window.runHooks('pluginBkmrksEdit', { target: 'folder', action: 'sort' }); + console.log('BOOKMARKS: sorted folder'); +}; + +// Saved the new sort of the bookmarks (in the localStorage) +window.plugin.bookmarks.sortBookmark = function (typeList) { + var keyType = typeList.replace('bkmrk_', ''); + var newArr = {}; + + $(`#${typeList} li.bookmarkFolder`).each(function () { + var idFold = $(this).attr('id'); + newArr[idFold] = window.plugin.bookmarks.bkmrksObj[keyType][idFold]; + newArr[idFold].bkmrk = {}; + }); + + $(`#${typeList} li.bkmrk`).each(function () { + window.plugin.bookmarks.loadStorage(); + + var idFold = $(this).parent().parent('li').attr('id'); + var id = $(this).attr('id'); + + var list = window.plugin.bookmarks.bkmrksObj[keyType]; + for (var idFoldersOrigin in list) { + for (var idBkmrk in list[idFoldersOrigin]['bkmrk']) { + if (idBkmrk === id) { + newArr[idFold].bkmrk[id] = window.plugin.bookmarks.bkmrksObj[keyType][idFoldersOrigin].bkmrk[id]; + } + } + } + }); + window.plugin.bookmarks.bkmrksObj[keyType] = newArr; + window.plugin.bookmarks.saveStorage(); + window.runHooks('pluginBkmrksEdit', { target: 'bookmarks', action: 'sort' }); + console.log('BOOKMARKS: sorted bookmark (portal/map)'); +}; + +window.plugin.bookmarks.jquerySortableScript = function () { + $('.bookmarkList > ul').sortable({ + items: 'li.bookmarkFolder:not(.othersBookmarks)', + handle: '.bookmarksAnchor', + placeholder: 'sortable-placeholder', + helper: 'clone', // fix accidental click in firefox + forcePlaceholderSize: true, + update: function (event, ui) { + var typeList = ui.item.parent().parent('.bookmarkList').attr('id'); + window.plugin.bookmarks.sortFolder(typeList); + }, + }); + + $('.bookmarkList ul li ul').sortable({ + items: 'li.bkmrk', + connectWith: '.bookmarkList ul ul', + handle: '.bookmarksLink', + placeholder: 'sortable-placeholder', + helper: 'clone', // fix accidental click in firefox + forcePlaceholderSize: true, + update: function (event, ui) { + var typeList = ui.item.parent().parent().parent().parent('.bookmarkList').attr('id'); + window.plugin.bookmarks.sortBookmark(typeList); + }, + }); +}; + +/** ************************************************************************************************************************************************************/ +/** OPTIONS ****************************************************************************************************************************************************/ +/** ************************************************************************************************************************************************************/ +// Manual import, export and reset data +window.plugin.bookmarks.manualOpt = function () { + window.dialog({ + html: window.plugin.bookmarks.htmlSetbox, + dialogClass: 'ui-dialog-bkmrksSet', + id: 'plugin-bookmarks-options', + title: 'Bookmarks Options', + }); + + window.runHooks('pluginBkmrksOpenOpt'); +}; + +window.plugin.bookmarks.optAlert = function (message) { + $('.ui-dialog-bkmrksSet .ui-dialog-buttonset').prepend('

    ' + message + '

    '); + $('.bkrmks-alert').delay(2500).fadeOut(); +}; + +window.plugin.bookmarks.optCopy = function () { + if (window.isApp && window.app.shareString) { + return window.app.shareString(window.plugin.bookmarks.escapeUnicode(localStorage[window.plugin.bookmarks.KEY_STORAGE])); + } else { + window.dialog({ + html: + '

    Select all and press CTRL+C to copy it.

    ', + dialogClass: 'ui-dialog-bkmrksSet-copy', + id: 'plugin-bookmarks-export', + title: 'Bookmarks Export', + }); + } +}; + +window.plugin.bookmarks.optExport = function () { + var data = localStorage[window.plugin.bookmarks.KEY_STORAGE]; + window.saveFile(data, 'IITC-bookmarks.json', 'application/json'); +}; + +window.plugin.bookmarks.optPaste = function () { + var promptAction = prompt('Press CTRL+V to paste it.', ''); + if (promptAction !== null && promptAction !== '') { + try { + JSON.parse(promptAction); // try to parse JSON first + localStorage[window.plugin.bookmarks.KEY_STORAGE] = promptAction; + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { target: 'all', action: 'import' }); + console.log('BOOKMARKS: reset and imported bookmarks'); + window.plugin.bookmarks.optAlert('Successful. '); + } catch (e) { + console.warn('BOOKMARKS: failed to import data: ' + e); + window.plugin.bookmarks.optAlert('Import failed '); + } + } +}; + +window.plugin.bookmarks.optImport = function () { + L.FileListLoader.loadFiles({ accept: 'application/json' }).on('load', function (e) { + try { + JSON.parse(e.reader.result); // try to parse JSON first + localStorage[window.plugin.bookmarks.KEY_STORAGE] = e.reader.result; + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { target: 'all', action: 'import' }); + console.log('BOOKMARKS: reset and imported bookmarks'); + window.plugin.bookmarks.optAlert('Successful. '); + } catch (e) { + console.warn('BOOKMARKS: failed to import data: ' + e); + window.plugin.bookmarks.optAlert('Import failed '); + } + }); +}; + +window.plugin.bookmarks.optReset = function () { + var promptAction = confirm('All bookmarks will be deleted. Are you sure?', ''); + if (promptAction) { + delete localStorage[window.plugin.bookmarks.KEY_STORAGE]; + window.plugin.bookmarks.createStorage(); + window.plugin.bookmarks.loadStorage(); + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { target: 'all', action: 'reset' }); + console.log('BOOKMARKS: reset all bookmarks'); + window.plugin.bookmarks.optAlert('Successful. '); + } +}; + +window.plugin.bookmarks.optBox = function (command) { + if (!window.isApp) { + switch (command) { + case 'save': + var boxX = parseInt($('#bookmarksBox').css('top')); + var boxY = parseInt($('#bookmarksBox').css('left')); + window.plugin.bookmarks.statusBox.pos = { x: boxX, y: boxY }; + window.plugin.bookmarks.saveStorageBox(); + window.plugin.bookmarks.optAlert('Position acquired. '); + break; + case 'reset': + $('#bookmarksBox').css({ top: 100, left: 100 }); + window.plugin.bookmarks.optBox('save'); + break; + } + } else { + window.plugin.bookmarks.optAlert('Only IITC desktop. '); + } +}; + +window.plugin.bookmarks.dialogLoadListFolders = function (idBox, clickAction, showOthersF, scanType /* 0 = maps&portals; 1 = maps; 2 = portals*/) { + var list = JSON.parse(localStorage[window.plugin.bookmarks.KEY_STORAGE]); + var listHTML = ''; + var foldHTML = ''; + var elemGenericFolder = ''; + + // For each type and folder + for (var type in list) { + if (scanType === 0 || (scanType === 1 && type === 'maps') || (scanType === 2 && type === 'portals')) { + listHTML += '

    ' + type + ':

    '; + + for (var idFolders in list[type]) { + var label = list[type][idFolders]['label']; + + // Create a folder + foldHTML = `
    ${label}
    `; + + if (idFolders !== window.plugin.bookmarks.KEY_OTHER_BKMRK) { + listHTML += foldHTML; + } else { + if (showOthersF === true) { + elemGenericFolder = foldHTML; + } + } + } + } + listHTML += elemGenericFolder; + elemGenericFolder = ''; + } + + // Append all folders + var r = `
    ${listHTML}
    `; + return r; +}; + +window.plugin.bookmarks.renameFolder = function (elem) { + var type = $(elem).data('type'); + var idFold = $(elem).data('id'); + + var promptAction = prompt('Insert a new name.', ''); + if (promptAction !== null && promptAction !== '') { + try { + var newName = window.plugin.bookmarks.escapeHtml(promptAction); + + window.plugin.bookmarks.bkmrksObj[type][idFold].label = newName; + $('#bookmarksDialogRenameF #' + idFold).text(newName); + window.plugin.bookmarks.saveStorage(); + window.plugin.bookmarks.refreshBkmrks(); + window.runHooks('pluginBkmrksEdit', { target: 'all', action: 'import' }); + + console.log('BOOKMARKS: renamed bookmarks folder'); + window.plugin.bookmarks.optAlert('Successful. '); + } catch (e) { + console.warn('BOOKMARKS: failed to rename folder: ' + e); + window.plugin.bookmarks.optAlert('Rename failed '); + } + } +}; + +window.plugin.bookmarks.optRenameF = function () { + window.dialog({ + html: window.plugin.bookmarks.dialogLoadListFolders('bookmarksDialogRenameF', 'window.plugin.bookmarks.renameFolder', false, 0), + dialogClass: 'ui-dialog-bkmrksSet-copy', + id: 'plugin-bookmarks-rename-folder', + title: 'Bookmarks Rename Folder', + }); +}; + +/** ************************************************************************************************************************************************************/ +/** AUTO DRAW **************************************************************************************************************************************************/ +/** ************************************************************************************************************************************************************/ +window.plugin.bookmarks.dialogDrawer = function () { + window.dialog({ + html: window.plugin.bookmarks.dialogLoadList, + dialogClass: 'ui-dialog-autodrawer', + id: 'plugin-bookmarks-move-bookmark', + title: 'Bookmarks - Auto Draw', + buttons: { + DRAW: function () { + window.plugin.bookmarks.draw(0); + }, + 'DRAW&VIEW': function () { + window.plugin.bookmarks.draw(1); + }, + }, + }); + window.plugin.bookmarks.autoDrawOnSelect(); +}; + +window.plugin.bookmarks.draw = function (view) { + var latlngs = []; + $('#bkmrksAutoDrawer a.bkmrk.selected').each(function (i) { + var tt = $(this).data('latlng'); + latlngs[i] = tt; + }); + + if (latlngs.length >= 2 && latlngs.length <= 3) { + // TODO: add an API to draw-tools rather than assuming things about its internals + + var layer, layerType; + if (latlngs.length === 2) { + layer = L.geodesicPolyline(latlngs, window.plugin.drawTools.lineOptions); + layerType = 'polyline'; + } else { + layer = L.geodesicPolygon(latlngs, window.plugin.drawTools.polygonOptions); + layerType = 'polygon'; + } + + window.map.fire('draw:created', { + layer: layer, + layerType: layerType, + }); + + if ($('#bkmrkClearSelection').prop('checked')) $('#bkmrksAutoDrawer a.bkmrk.selected').removeClass('selected'); + + if (window.plugin.bookmarks.isSmart) { + window.show('map'); + } + + // Shown the layer if it is hidden + if (!window.map.hasLayer(window.plugin.drawTools.drawnItems)) { + window.map.addLayer(window.plugin.drawTools.drawnItems); + } + + if (view) { + window.map.fitBounds(layer.getBounds()); + } + } +}; + +window.plugin.bookmarks.autoDrawOnSelect = function () { + var latlngs = []; + $('#bkmrksAutoDrawer a.bkmrk.selected').each(function (i) { + var tt = $(this).data('latlng'); + latlngs[i] = tt; + }); + + var text = 'You must select 2 or 3 portals!'; + var color = 'red'; + + function distanceElement(distance) { + var text = window.formatDistance(distance); + return distance >= 200000 ? '' + text + '' : text; + } + + if (latlngs.length === 2) { + var distance = L.latLng(latlngs[0]).distanceTo(latlngs[1]); + text = 'Distance between portals: ' + distanceElement(distance); + color = ''; + } else if (latlngs.length === 3) { + var distances = latlngs.map(function (ll1, i, latlngs) { + var ll2 = latlngs[(i + 1) % 3]; + return distanceElement(L.latLng(ll1).distanceTo(ll2)); + }); + text = 'Distances: ' + distances.join(', '); + color = ''; + } + + $('#bkmrksAutoDrawer p').html(text).css('color', color); +}; + +window.plugin.bookmarks.dialogLoadList = function () { + var r = 'The "Draw Tools" plugin is required.'; + + if (!window.plugin.bookmarks || !window.plugin.drawTools) { + $('.ui-dialog-autodrawer .ui-dialog-buttonset .ui-button:not(:first)').hide(); + } else { + var portalsList = JSON.parse(localStorage[window.plugin.bookmarks.KEY_STORAGE]); + var element = ''; + var elementTemp = ''; + var elemGenericFolder = ''; + + // For each folder + var list = portalsList.portals; + for (var idFolders in list) { + var folders = list[idFolders]; + + // Create a label and a anchor for the sortable + var folderLabel = `${folders['label']}`; + + // Create a folder + elementTemp = `
    ${folderLabel}
    `; + + // For each bookmark + var fold = folders['bkmrk']; + for (var idBkmrk in fold) { + var bkmrk = fold[idBkmrk]; + var label = bkmrk['label']; + var latlng = bkmrk['latlng']; + + // Create the bookmark + elementTemp += `${label}`; + } + elementTemp += '
    '; + + if (idFolders !== window.plugin.bookmarks.KEY_OTHER_BKMRK) { + element += elementTemp; + } else { + elemGenericFolder += elementTemp; + } + } + element += elemGenericFolder; + + // Append all folders and bookmarks + r = + '
    ' + + '' + + '

    You must select 2 or 3 portals!

    ' + + '
    ' + + element + + '
    ' + + '
    '; + } + return r; +}; + +/** ************************************************************************************************************************************************************/ +/** SYNC *******************************************************************************************************************************************************/ +/** ************************************************************************************************************************************************************/ +// Delay the syncing to group a few updates in a single request +window.plugin.bookmarks.delaySync = function () { + if (!window.plugin.bookmarks.enableSync || !window.plugin.bookmarks.IsDefaultStorageKey) return; + clearTimeout(window.plugin.bookmarks.delaySync.timer); + window.plugin.bookmarks.delaySync.timer = setTimeout(function () { + window.plugin.bookmarks.delaySync.timer = null; + window.plugin.bookmarks.syncNow(); + }, window.plugin.bookmarks.SYNC_DELAY); +}; + +// Store the updateQueue in updatingQueue and upload +window.plugin.bookmarks.syncNow = function () { + if (!window.plugin.bookmarks.enableSync || !window.plugin.bookmarks.IsDefaultStorageKey) return; + $.extend(window.plugin.bookmarks.updatingQueue, window.plugin.bookmarks.updateQueue); + window.plugin.bookmarks.updateQueue = {}; + window.plugin.bookmarks.storeLocal(window.plugin.bookmarks.UPDATING_QUEUE); + window.plugin.bookmarks.storeLocal(window.plugin.bookmarks.UPDATE_QUEUE); + + window.plugin.sync.updateMap('bookmarks', window.plugin.bookmarks.KEY.field, Object.keys(window.plugin.bookmarks.updatingQueue)); +}; + +// Call after IITC and all plugin loaded +window.plugin.bookmarks.registerFieldForSyncing = function () { + if (!window.plugin.sync) return; + window.plugin.sync.registerMapForSync( + 'bookmarks', + window.plugin.bookmarks.KEY.field, + window.plugin.bookmarks.syncCallback, + window.plugin.bookmarks.syncInitialized + ); +}; + +// Call after local or remote change uploaded +window.plugin.bookmarks.syncCallback = function (pluginName, fieldName, e, fullUpdated) { + if (fieldName === window.plugin.bookmarks.KEY.field) { + window.plugin.bookmarks.storeLocal(window.plugin.bookmarks.KEY); + // All data is replaced if other client update the data during this client offline, + if (fullUpdated) { + window.plugin.bookmarks.refreshBkmrks(); + window.plugin.bookmarks.resetAllStars(); + window.runHooks('pluginBkmrksSyncEnd', { target: 'all', action: 'sync' }); + console.log('BOOKMARKS: synchronized all from drive after offline'); + return; + } + + if (!e) return; + if (e.isLocal) { + // Update pushed successfully, remove it from updatingQueue + delete window.plugin.bookmarks.updatingQueue[e.property]; + console.log('BOOKMARKS: synchronized to drive'); + } else { + // Remote update + delete window.plugin.bookmarks.updateQueue[e.property]; + window.plugin.bookmarks.storeLocal(window.plugin.bookmarks.UPDATE_QUEUE); + window.plugin.bookmarks.refreshBkmrks(); + window.plugin.bookmarks.resetAllStars(); + window.runHooks('pluginBkmrksSyncEnd', { target: 'all', action: 'sync' }); + console.log('BOOKMARKS: synchronized all from remote'); + } + } +}; + +// syncing of the field is initialized, upload all queued update +window.plugin.bookmarks.syncInitialized = function (pluginName, fieldName) { + if (fieldName === window.plugin.bookmarks.KEY.field) { + window.plugin.bookmarks.enableSync = true; + if (Object.keys(window.plugin.bookmarks.updateQueue).length > 0) { + window.plugin.bookmarks.delaySync(); + } + } +}; + +window.plugin.bookmarks.storeLocal = function (mapping) { + if (typeof window.plugin.bookmarks[mapping.field] !== 'undefined' && window.plugin.bookmarks[mapping.field] !== null) { + localStorage[mapping.key] = JSON.stringify(window.plugin.bookmarks[mapping.field]); + } else { + localStorage.removeItem(mapping.key); + } +}; + +window.plugin.bookmarks.loadLocal = function (mapping) { + var objectJSON = localStorage[mapping.key]; + if (!objectJSON) return; + window.plugin.bookmarks[mapping.field] = mapping.convertFunc ? mapping.convertFunc(JSON.parse(objectJSON)) : JSON.parse(objectJSON); +}; + +window.plugin.bookmarks.syncBkmrks = function () { + window.plugin.bookmarks.loadLocal(window.plugin.bookmarks.KEY); + + window.plugin.bookmarks.updateQueue = window.plugin.bookmarks.bkmrksObj; + window.plugin.bookmarks.storeLocal(window.plugin.bookmarks.UPDATE_QUEUE); + + window.plugin.bookmarks.delaySync(); + window.plugin.bookmarks.loadLocal(window.plugin.bookmarks.KEY); // switch back to active storage related to KEY +}; + +/** ************************************************************************************************************************************************************/ +/** HIGHLIGHTER ************************************************************************************************************************************************/ +/** ************************************************************************************************************************************************************/ +window.plugin.bookmarks.highlight = function (data) { + var guid = data.portal.options.ent[0]; + if (window.plugin.bookmarks.findByGuid(guid)) { + data.portal.setStyle({ fillColor: 'red' }); + } +}; + +window.plugin.bookmarks.highlightRefresh = function (data) { + if (window._current_highlighter === 'Bookmarked Portals') { + if ( + data.action === 'sync' || + data.target === 'portal' || + (data.target === 'folder' && data.action === 'remove') || + (data.target === 'all' && data.action === 'import') || + (data.target === 'all' && data.action === 'reset') || + (data.target === 'all' && data.action === 'MPEswitch') + ) { + window.changePortalHighlights('Bookmarked Portals'); + } + } +}; + +/** ************************************************************************************************************************************************************/ +/** BOOKMARKED PORTALS LAYER ***********************************************************************************************************************************/ +/** ************************************************************************************************************************************************************/ +window.plugin.bookmarks.addAllStars = function () { + var list = window.plugin.bookmarks.bkmrksObj.portals; + + for (var idFolders in list) { + for (var idBkmrks in list[idFolders]['bkmrk']) { + var latlng = list[idFolders]['bkmrk'][idBkmrks].latlng.split(','); + var guid = list[idFolders]['bkmrk'][idBkmrks].guid; + var lbl = list[idFolders]['bkmrk'][idBkmrks].label; + window.plugin.bookmarks.addStar(guid, latlng, lbl); + } + } +}; + +window.plugin.bookmarks.resetAllStars = function () { + for (const guid in window.plugin.bookmarks.starLayers) { + var starInLayer = window.plugin.bookmarks.starLayers[guid]; + window.plugin.bookmarks.starLayerGroup.removeLayer(starInLayer); + delete window.plugin.bookmarks.starLayers[guid]; + } + window.plugin.bookmarks.addAllStars(); + console.log('resetAllStars done'); +}; + +window.plugin.bookmarks.addStar = function (guid, latlng, lbl) { + var star = L.marker(latlng, { + title: lbl, + icon: L.icon({ + iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAoCAMAAACo9wirAAAAzFBMVEXbuTR9YwDd0T+ulyry2VDt53/s5Jv34a+tmUbJtGRtVwAtJADg14j304viyIZeSwDXyH5IOQD07JLPvnhqVQBuWACmkjpgTQBxWwC4p1WchikRDQCPdxeBaAQTDwADAgAOCwAAAAAAAAAAAAALCQAAAAAAAAAAAAD9+Jb97IT+3VT9+WH+0i3993D+80793Yr/yXL+5Tr+6XL+zEP/z2X+vSX+zB/+52L/qw393SX89xr9+yv/siP/vUf9+Dv+20P+rgH+xQv+vgH+twNAe/b8AAAAKHRSTlPywvLj+vn4/OLrqFb0/PSS8Hb77meB4FGX5t1A2dUkEy8HDQoaAwEAoclaPQAAAo1JREFUeF5VkeeyozAMRrPphdwkdIxpBu7SSe/llvd/p5VxlMx+PxiMzhxJuPX3mc/PzzgIQwYJwyCGoyggwMuRav6BmKrLGgQBrBvUkuQkkSWLGh5DooX1UNWlXplDyp6kq5EXCqL1qlu9srpfIPeq7FmaIBCIA9WScyjznC/3XOZEEAsABIFBoH6GUrt9v5zPQEAXFoCiAWLPkUqon+8lIbl4kRwfmggABFZSXXh9PhbopUos1fWCBoAJnT4MCMMlZLHQE/Hed/wIFA3gkiSvqjLtK45tO/N+WlZVnhA7YgKAFZK83dGJM5v4rmFSonfaeWotoEcDhJqSVl06W0A5gviTGelWqTJzvZADcWjO11XVIzPbjzzGPFfT4bxWTD8SAANgtTrIQ8eF3QPmDOXDarWej/mUL+C0SgfU91gcR3TAj42BNQDMAMDpwBfzwjCi/cMJgNFMGGBIY5g+AACnrWl+NJay0+mRDhcAiDVdKsOnzJqZRFGoZo74SaYTXDPwzPnhUTw6dCSnaT2inUdRHD7GNgIxM/QUvrW6WfZ4ZFm3VRSiA4sFEEaOlBX7oij2EP4ssqlj413wHqq13uy/X9lvYAcbb5P3cGmd7b9/nvneZzWdNB0EAApN2RZvoNjCvfKb4IBQ+LTe7H5+RXabmjwFCISw6Xb3i8D2oxEgwMdkBgHF7xeEC/SF34yIAN90/LHdHb94QCB2REBsauj17cqB660eogAB8bOE4vgUsBiBt2J6ux6P19v0OQECb8V8y4G34D+AKwgoQEBQgMBLYSrL222pjHEFBN4KOl0up/wWUIAA/k5tNBg014gCBPB3+s5g0EwYYB2Bl8KyND9CAQJvgnmq6kYeXxHzDyATueNnvZcYAAAAAElFTkSuQmCC', + iconAnchor: [15, 40], + iconSize: [30, 40], + }), + }); + window.registerMarkerForOMS(star); + star.on('spiderfiedclick', function () { + window.renderPortalDetails(guid); + }); + + window.plugin.bookmarks.starLayers[guid] = star; + star.addTo(window.plugin.bookmarks.starLayerGroup); +}; + +window.plugin.bookmarks.editStar = function (data) { + if (data.target === 'portal') { + if (data.action === 'add') { + var guid = data.guid; + var latlng = window.portals[guid].getLatLng(); + var lbl = window.portals[guid].options.data.title; + window.plugin.bookmarks.addStar(guid, latlng, lbl); + } else if (data.action === 'remove') { + var starInLayer = window.plugin.bookmarks.starLayers[data.guid]; + window.plugin.bookmarks.starLayerGroup.removeLayer(starInLayer); + delete window.plugin.bookmarks.starLayers[data.guid]; + } + } else if ((data.target === 'all' && (data.action === 'import' || data.action === 'reset')) || (data.target === 'folder' && data.action === 'remove')) { + window.plugin.bookmarks.resetAllStars(); + } +}; + +/** ************************************************************************************************************************************************************/ + +window.plugin.bookmarks.setupPortalsList = function () { + function onBookmarkChanged(data) { + console.log(data, data.target, data.guid); + + if (data.target === 'portal' && data.guid) { + if (window.plugin.bookmarks.findByGuid(data.guid)) $('[data-list-bookmark="' + data.guid + '"]').addClass('favorite'); + else $('[data-list-bookmark="' + data.guid + '"]').removeClass('favorite'); + } else { + $('[data-list-bookmark]').each(function (i, element) { + var guid = element.getAttribute('data-list-bookmark'); + if (window.plugin.bookmarks.findByGuid(guid)) $(element).addClass('favorite'); + else $(element).removeClass('favorite'); + }); + } + } + + window.addHook('pluginBkmrksEdit', onBookmarkChanged); + window.addHook('pluginBkmrksSyncEnd', onBookmarkChanged); + + window.plugin.portalslist.fields.unshift({ + // insert at first column + title: '', + value: function (portal) { + return portal.options.guid; + }, // we store the guid, but implement a custom comparator so the list does sort properly without closing and reopening the dialog + sort: function (guidA, guidB) { + var infoA = window.plugin.bookmarks.findByGuid(guidA); + var infoB = window.plugin.bookmarks.findByGuid(guidB); + if (infoA && !infoB) return 1; + if (infoB && !infoA) return -1; + return 0; + }, + format: function (cell, portal, guid) { + $(cell).addClass('portal-list-bookmark').attr('data-list-bookmark', guid); + + // for some reason, jQuery removes event listeners when the list is sorted. Therefore we use DOM's addEventListener + $('') + .appendTo(cell)[0] + .addEventListener( + 'click', + function () { + if (window.plugin.bookmarks.findByGuid(guid)) { + window.plugin.bookmarks.switchStarPortal(guid); + } else { + window.plugin.bookmarks.addPortalBookmarkByMarker(portal, true); + } + }, + false + ); + + if (window.plugin.bookmarks.findByGuid(guid)) cell.className += ' favorite'; + }, + }); +}; + +window.plugin.bookmarks.setupContent = function () { + window.plugin.bookmarks.htmlBoxTrigger = + '[-] Bookmarks'; + window.plugin.bookmarks.htmlBkmrksBox = + '
    ' + + '
    ' + + ' -' + + '
    ...
    ' + + ' Show/Hide "X" button' + + '
    ' + + '
    ' + + '
    Maps
    ' + + '
    Portals
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + ' ' + + ' + Map' + + ' + Folder' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + ' ' + + ' + Folder' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + + window.plugin.bookmarks.htmlDisabledMessage = '
    Plugin Bookmarks disabled*.
    '; + window.plugin.bookmarks.htmlStar = + ''; + window.plugin.bookmarks.htmlMoveBtn = + 'Show/Hide "Move" button'; + + var actions = ''; + actions += 'Reset bookmarks'; + actions += 'Copy bookmarks'; + actions += 'Paste bookmarks'; + + actions += 'Import bookmarks'; + actions += 'Export bookmarks'; + + actions += 'Rename Folder'; + if (!window.isApp) { + actions += 'Save box position'; + actions += 'Reset box position'; + } + window.plugin.bookmarks.htmlSetbox = '
    ' + actions + '
    '; +}; + +/** ************************************************************************************************************************************************************/ +window.plugin.bookmarks.initMPE = function () { + window.plugin.mpe.setMultiProjects({ + namespace: 'bookmarks', + title: 'Bookmarks for Maps and Portals', + fa: 'fa-bookmark', + defaultKey: 'plugin-bookmarks', + func_setKey: function (newKey) { + window.plugin.bookmarks.KEY_STORAGE = newKey; + window.plugin.bookmarks.KEY.key = newKey; + }, + func_pre: function () { + // disable sync + window.plugin.bookmarks.IsDefaultStorageKey = false; + }, + func_post: function () { + // Delete all Markers (stared portals) + for (var guid in window.plugin.bookmarks.starLayers) { + var starInLayer = window.plugin.bookmarks.starLayers[guid]; + window.plugin.bookmarks.starLayerGroup.removeLayer(starInLayer); + delete window.plugin.bookmarks.starLayers[guid]; + } + // Create Storage if not exist + window.plugin.bookmarks.createStorage(); + // Load Storage + window.plugin.bookmarks.loadStorage(); + // window.plugin.bookmarks.saveStorage(); + + // Delete and Regenerate Bookmark Lists + window.plugin.bookmarks.refreshBkmrks(); + + // Add Markers (stared portals) + window.plugin.bookmarks.addAllStars(); + + // Refresh Highlighter + window.plugin.bookmarks.highlightRefresh({ target: 'all', action: 'MPEswitch' }); + + // enable sync if default storage + window.plugin.bookmarks.IsDefaultStorageKey = this.defaultKey === this.currKey; + }, + }); +}; + +/** ************************************************************************************************************************************************************/ + +var setup = function () { + window.plugin.bookmarks.isSmart = window.isSmartphone(); + + // HOOKS: + // - pluginBkmrksEdit: fired when a bookmarks/folder is removed, added or sorted, + // also when a folder is opened/closed. + // - pluginBkmrksOpenOpt: fired when the "Bookmarks Options" panel is opened + // (you can add new options); + // - pluginBkmrksSyncEnd: fired when the sync is finished; + + // If the storage not exists or is a old version + window.plugin.bookmarks.createStorage(); + window.plugin.bookmarks.upgradeToNewStorage(); + + // Load data from localStorage + window.plugin.bookmarks.loadStorage(); + window.plugin.bookmarks.loadStorageBox(); + window.plugin.bookmarks.setupContent(); + window.plugin.bookmarks.setupCSS(); + + if (!window.plugin.bookmarks.isSmart) { + $('body').append(window.plugin.bookmarks.htmlBoxTrigger + window.plugin.bookmarks.htmlBkmrksBox); + $('#bookmarksBox').draggable({ handle: '.handle', containment: 'window' }); + $( + '#bookmarksBox #bookmarksMin , #bookmarksBox ul li, #bookmarksBox ul li a, #bookmarksBox ul li a span, #bookmarksBox h5, #bookmarksBox .addForm a' + ).disableSelection(); + $('#bookmarksBox').css({ top: window.plugin.bookmarks.statusBox.pos.x, left: window.plugin.bookmarks.statusBox.pos.y }); + } else { + $('body').append(window.plugin.bookmarks.htmlBkmrksBox); + $('#bookmarksBox').css('display', 'none').addClass('mobile'); + + if (window.useAppPanes()) window.app.addPane('plugin-bookmarks', 'Bookmarks', 'ic_action_star'); + window.addHook('paneChanged', window.plugin.bookmarks.onPaneChanged); + } + IITC.toolbox.addButton({ + label: 'Bookmarks Opt', + action: window.plugin.bookmarks.manualOpt, + }); + IITC.toolbox.addButton({ + label: 'Auto draw', + title: 'Draw lines/triangles between bookmarked portals [q]', + action: window.plugin.bookmarks.dialogDrawer, + accesskey: 'q', + }); + + if (window.plugin.bookmarks.isSmart) { + // $('#bookmarksBox.mobile #topBar').prepend(window.plugin.bookmarks.htmlCallSetBox+window.plugin.bookmarks.htmlCalldrawBox); // wonk in progress + $('#bookmarksBox.mobile #topBar').append(window.plugin.bookmarks.htmlMoveBtn); + } + + window.plugin.bookmarks.loadList('maps'); + window.plugin.bookmarks.loadList('portals'); + window.plugin.bookmarks.jquerySortableScript(); + + if (window.plugin.bookmarks.statusBox['show'] === 0) { + window.plugin.bookmarks.switchStatusBkmrksBox(0); + } + if (window.plugin.bookmarks.statusBox['page'] === 1) { + $('#bookmarksBox h5.bkmrk_portals').trigger('click'); + } + + window.addHook('portalSelected', window.plugin.bookmarks.onPortalSelected); + window.addHook('search', window.plugin.bookmarks.onSearch); + + // Sync + window.addHook('pluginBkmrksEdit', window.plugin.bookmarks.syncBkmrks); + window.plugin.bookmarks.registerFieldForSyncing(); + + // Highlighter - bookmarked portals + window.addHook('pluginBkmrksEdit', window.plugin.bookmarks.highlightRefresh); + window.addHook('pluginBkmrksSyncEnd', window.plugin.bookmarks.highlightRefresh); + window.addPortalHighlighter('Bookmarked Portals', window.plugin.bookmarks.highlight); + + // Layer - Bookmarked portals + window.plugin.bookmarks.starLayerGroup = L.layerGroup(); + window.layerChooser.addOverlay(window.plugin.bookmarks.starLayerGroup, 'Bookmarked Portals', { default: false }); + window.plugin.bookmarks.addAllStars(); + window.addHook('pluginBkmrksEdit', window.plugin.bookmarks.editStar); + window.addHook('pluginBkmrksSyncEnd', window.plugin.bookmarks.resetAllStars); + + if (window.plugin.portalslist) { + window.plugin.bookmarks.setupPortalsList(); + } + // Initilaize MPE-Support only if MPE-Module is available + if (window.plugin.mpe !== undefined) { + window.plugin.bookmarks.initMPE(); + } +}; +// moved setupCSS to the end to improve readability of built script +window.plugin.bookmarks.setupCSS = function () { + $('' + ); +}; + +// --------------------------------------------------------------------------------- +// EXPORT (POLYS AS LINES) +// --------------------------------------------------------------------------------- +window.plugin.drawTools.getDrawAsLines = function () { + var rawDraw = JSON.parse(window.localStorage[window.plugin.drawTools.KEY_STORAGE]); + var draw = []; + + for (const i in rawDraw) { + var elemDraw = rawDraw[i]; + + if (elemDraw.type === 'polygon') { + var convElemDraw = {}; + convElemDraw.color = elemDraw.color; + convElemDraw.type = 'polyline'; + convElemDraw.latLngs = []; + + for (const j in elemDraw.latLngs) { + var ll = elemDraw.latLngs[j]; + convElemDraw.latLngs.push(ll); + } + convElemDraw.latLngs.push(elemDraw.latLngs[0]); + + draw.push(convElemDraw); + } else { + draw.push(elemDraw); + } + } + + return JSON.stringify(draw); +}; + +// --------------------------------------------------------------------------------- +// EMPTY POLYGONS (EMPTY DRAWN FIELDS) +// --------------------------------------------------------------------------------- +window.plugin.drawTools.EDFstatus = false; + +window.plugin.drawTools.initEDF = function () { + // window.addHook('pluginDrawTools', window.plugin.drawTools.edf.hookManagement); + window.addHook('iitcLoaded', function () { + if (window.plugin.drawTools.EDFstatus) { + window.plugin.drawTools.toggleOpacityOpt(); + window.plugin.drawTools.clearAndDraw(); + } + }); +}; + +window.plugin.drawTools.toggleEDF = function () { + var status = window.plugin.drawTools.EDFstatus; + status = Boolean(!status); + window.plugin.drawTools.EDFstatus = status; +}; + +window.plugin.drawTools.toggleOpacityOpt = function () { + if (window.plugin.drawTools.EDFstatus) { + window.plugin.drawTools.polygonOptions.fillOpacity = 0.0; + window.plugin.drawTools.polygonOptions.interactive = false; + } else { + window.plugin.drawTools.polygonOptions.fillOpacity = 0.2; + window.plugin.drawTools.polygonOptions.interactive = true; + } +}; + +window.plugin.drawTools.clearAndDraw = function () { + window.plugin.drawTools.drawnItems.clearLayers(); + window.plugin.drawTools.load(); + console.log('DRAWTOOLS: reset all drawn items'); +}; + +window.plugin.drawTools.edfStatusToggle = function () { + window.plugin.drawTools.toggleEDF(); + window.plugin.drawTools.toggleOpacityOpt(); + window.plugin.drawTools.clearAndDraw(); +}; + +// --------------------------------------------------------------------------------- +// MPE - MULTI PROJECTS EXTENSION +// --------------------------------------------------------------------------------- +window.plugin.drawTools.initMPE = function () { + // Not launch the code if the MPE plugin there isn't. + if (!window.plugin.mpe) { + return; + } + + // The MPE function to set a MultiProjects type + window.plugin.mpe.setMultiProjects({ + namespace: 'drawTools', + title: 'Draw Tools Layer', + // Font awesome css class + fa: 'fa-pencil', + // Function to change a localstorage key + func_setKey: function (newKey) { + window.plugin.drawTools.KEY_STORAGE = newKey; + }, + // Native value of localstorage key + defaultKey: 'plugin-draw-tools-layer', + // This function is run before the localstorage key change + func_pre: function () {}, + // This function is run after the localstorage key change + func_post: function () { + window.plugin.drawTools.drawnItems.clearLayers(); + window.plugin.drawTools.load(); + console.log('DRAWTOOLS: reset all drawn items (func_post)'); + + if (window.plugin.crossLinks) { + window.plugin.crossLinks.checkAllLinks(); + + if (window.plugin.destroyedLinks) { + window.plugin.destroyedLinks.cross.removeCrossAll(); + } + } + + // Code to: + // hide/remove elements from DOM, layers, variables, etc... + // load data from window.localStorage[window.plugin.myPlugin.KEY_STORAGE] + // show/add/draw elements in the DOM, layers, variables, etc... + }, + }); +}; + +var cachedFilters; +window.plugin.drawTools.getLocationFilters = function () { + if (cachedFilters) { + return cachedFilters; + } + var filters; + if (!window.map.hasLayer(window.plugin.drawTools.drawnItems)) { + return []; + } + var markers = []; + var polygons = []; + window.plugin.drawTools.drawnItems.eachLayer(function (layer) { + if (layer instanceof L.GeodesicPolygon) { + polygons.push(layer); + } else if (layer instanceof L.Marker) { + markers.push(layer); + } + }); + markers = markers.filter(function (marker) { + return marker._icon._leaflet_pos; + }); + polygons = polygons.filter(function (poly) { + return poly._rings && poly._rings.length; + }); + polygons = polygons.map(function (poly) { + return poly._rings[0]; + }); + filters = polygons.map(function (poly) { + return function (portal) { + // portal|Point|LatLng + var point; + if ('_point' in portal || portal instanceof L.CircleMarker) { + point = portal._point; + if (!point) { + return false; + } + } else if ('x' in portal) { + point = portal; + } else if ('lat' in portal) { + point = window.map.latLngToLayerPoint(portal); + } else if (portal.getLatLng) { + point = window.map.latLngToLayerPoint(portal.getLatLng()); + } + if ( + markers.some(function (marker) { + return marker._icon._leaflet_pos.equals(point); + }) + ) { + return false; + } + return window.pnpoly(poly, point); + }; + }); + cachedFilters = filters; + return filters; +}; + +function setup() { + loadExternals(); // initialize leaflet + window.plugin.drawTools.boot(); // initialize drawtools + window.plugin.drawTools.initMPE(); // register to MPE if available + window.plugin.drawTools.initEDF(); // initialize empty drawn fields + + var filterEvents = new L.Evented(); + window.map.on('draw:created draw:edited draw:deleted', function (e) { + filterEvents.fire('changed', { originalEvent: e }); + }); + filterEvents.on('changed', function () { + cachedFilters = null; + }); + window.plugin.drawTools.filterEvents = filterEvents; +} +setup.priority = 'high'; + +function loadExternals() { + try { + // https://github.com/Leaflet/Leaflet.draw + // eslint-disable-next-line + // *** included: external/leaflet.draw-src.js *** +/* + Leaflet.draw 1.0.4, a plugin that adds drawing and editing tools to Leaflet powered maps. + (c) 2012-2017, Jacob Toye, Jon West, Smartrak, Leaflet + + https://github.com/Leaflet/Leaflet.draw + http://leafletjs.com + */ +(function (window, document, undefined) {/** + * Leaflet.draw assumes that you have already included the Leaflet library. + */ +L.drawVersion = "1.0.4"; +/** + * @class L.Draw + * @aka Draw + * + * + * To add the draw toolbar set the option drawControl: true in the map options. + * + * @example + * ```js + * var map = L.map('map', {drawControl: true}).setView([51.505, -0.09], 13); + * + * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { + * attribution: '© OpenStreetMap contributors' + * }).addTo(map); + * ``` + * + * ### Adding the edit toolbar + * To use the edit toolbar you must initialise the Leaflet.draw control and manually add it to the map. + * + * ```js + * var map = L.map('map').setView([51.505, -0.09], 13); + * + * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { + * attribution: '© OpenStreetMap contributors' + * }).addTo(map); + * + * // FeatureGroup is to store editable layers + * var drawnItems = new L.FeatureGroup(); + * map.addLayer(drawnItems); + * + * var drawControl = new L.Control.Draw({ + * edit: { + * featureGroup: drawnItems + * } + * }); + * map.addControl(drawControl); + * ``` + * + * The key here is the featureGroup option. This tells the plugin which FeatureGroup contains the layers that + * should be editable. The featureGroup can contain 0 or more features with geometry types Point, LineString, and Polygon. + * Leaflet.draw does not work with multigeometry features such as MultiPoint, MultiLineString, MultiPolygon, + * or GeometryCollection. If you need to add multigeometry features to the draw plugin, convert them to a + * FeatureCollection of non-multigeometries (Points, LineStrings, or Polygons). + */ +L.Draw = {}; + +/** + * @class L.drawLocal + * @aka L.drawLocal + * + * The core toolbar class of the API — it is used to create the toolbar ui + * + * @example + * ```js + * var modifiedDraw = L.drawLocal.extend({ + * draw: { + * toolbar: { + * buttons: { + * polygon: 'Draw an awesome polygon' + * } + * } + * } + * }); + * ``` + * + * The default state for the control is the draw toolbar just below the zoom control. + * This will allow map users to draw vectors and markers. + * **Please note the edit toolbar is not enabled by default.** + */ +L.drawLocal = { + // format: { + // numeric: { + // delimiters: { + // thousands: ',', + // decimal: '.' + // } + // } + // }, + draw: { + toolbar: { + // #TODO: this should be reorganized where actions are nested in actions + // ex: actions.undo or actions.cancel + actions: { + title: 'Cancel drawing', + text: 'Cancel' + }, + finish: { + title: 'Finish drawing', + text: 'Finish' + }, + undo: { + title: 'Delete last point drawn', + text: 'Delete last point' + }, + buttons: { + polyline: 'Draw a polyline', + polygon: 'Draw a polygon', + rectangle: 'Draw a rectangle', + circle: 'Draw a circle', + marker: 'Draw a marker', + circlemarker: 'Draw a circlemarker' + } + }, + handlers: { + circle: { + tooltip: { + start: 'Click and drag to draw circle.' + }, + radius: 'Radius' + }, + circlemarker: { + tooltip: { + start: 'Click map to place circle marker.' + } + }, + marker: { + tooltip: { + start: 'Click map to place marker.' + } + }, + polygon: { + tooltip: { + start: 'Click to start drawing shape.', + cont: 'Click to continue drawing shape.', + end: 'Click first point to close this shape.' + } + }, + polyline: { + error: 'Error: shape edges cannot cross!', + tooltip: { + start: 'Click to start drawing line.', + cont: 'Click to continue drawing line.', + end: 'Click last point to finish line.' + } + }, + rectangle: { + tooltip: { + start: 'Click and drag to draw rectangle.' + } + }, + simpleshape: { + tooltip: { + end: 'Release mouse to finish drawing.' + } + } + } + }, + edit: { + toolbar: { + actions: { + save: { + title: 'Save changes', + text: 'Save' + }, + cancel: { + title: 'Cancel editing, discards all changes', + text: 'Cancel' + }, + clearAll: { + title: 'Clear all layers', + text: 'Clear All' + } + }, + buttons: { + edit: 'Edit layers', + editDisabled: 'No layers to edit', + remove: 'Delete layers', + removeDisabled: 'No layers to delete' + } + }, + handlers: { + edit: { + tooltip: { + text: 'Drag handles or markers to edit features.', + subtext: 'Click cancel to undo changes.' + } + }, + remove: { + tooltip: { + text: 'Click on a feature to remove.' + } + } + } + } +}; + + + +/** + * ### Events + * Once you have successfully added the Leaflet.draw plugin to your map you will want to respond to the different + * actions users can initiate. The following events will be triggered on the map: + * + * @class L.Draw.Event + * @aka Draw.Event + * + * Use `L.Draw.Event.EVENTNAME` constants to ensure events are correct. + * + * @example + * ```js + * map.on(L.Draw.Event.CREATED; function (e) { + * var type = e.layerType, + * layer = e.layer; + * + * if (type === 'marker') { + * // Do marker specific actions + * } + * + * // Do whatever else you need to. (save to db; add to map etc) + * map.addLayer(layer); + *}); + * ``` + */ +L.Draw.Event = {}; +/** + * @event draw:created: PolyLine; Polygon; Rectangle; Circle; Marker | String + * + * Layer that was just created. + * The type of layer this is. One of: `polyline`; `polygon`; `rectangle`; `circle`; `marker` + * Triggered when a new vector or marker has been created. + * + */ +L.Draw.Event.CREATED = 'draw:created'; + +/** + * @event draw:edited: LayerGroup + * + * List of all layers just edited on the map. + * + * + * Triggered when layers in the FeatureGroup; initialised with the plugin; have been edited and saved. + * + * @example + * ```js + * map.on('draw:edited', function (e) { + * var layers = e.layers; + * layers.eachLayer(function (layer) { + * //do whatever you want; most likely save back to db + * }); + * }); + * ``` + */ +L.Draw.Event.EDITED = 'draw:edited'; + +/** + * @event draw:deleted: LayerGroup + * + * List of all layers just removed from the map. + * + * Triggered when layers have been removed (and saved) from the FeatureGroup. + */ +L.Draw.Event.DELETED = 'draw:deleted'; + +/** + * @event draw:drawstart: String + * + * The type of layer this is. One of:`polyline`; `polygon`; `rectangle`; `circle`; `marker` + * + * Triggered when the user has chosen to draw a particular vector or marker. + */ +L.Draw.Event.DRAWSTART = 'draw:drawstart'; + +/** + * @event draw:drawstop: String + * + * The type of layer this is. One of: `polyline`; `polygon`; `rectangle`; `circle`; `marker` + * + * Triggered when the user has finished a particular vector or marker. + */ + +L.Draw.Event.DRAWSTOP = 'draw:drawstop'; + +/** + * @event draw:drawvertex: LayerGroup + * + * List of all layers just being added from the map. + * + * Triggered when a vertex is created on a polyline or polygon. + */ +L.Draw.Event.DRAWVERTEX = 'draw:drawvertex'; + +/** + * @event draw:editstart: String + * + * The type of edit this is. One of: `edit` + * + * Triggered when the user starts edit mode by clicking the edit tool button. + */ + +L.Draw.Event.EDITSTART = 'draw:editstart'; + +/** + * @event draw:editmove: ILayer + * + * Layer that was just moved. + * + * Triggered as the user moves a rectangle; circle or marker. + */ +L.Draw.Event.EDITMOVE = 'draw:editmove'; + +/** + * @event draw:editresize: ILayer + * + * Layer that was just moved. + * + * Triggered as the user resizes a rectangle or circle. + */ +L.Draw.Event.EDITRESIZE = 'draw:editresize'; + +/** + * @event draw:editvertex: LayerGroup + * + * List of all layers just being edited from the map. + * + * Triggered when a vertex is edited on a polyline or polygon. + */ +L.Draw.Event.EDITVERTEX = 'draw:editvertex'; + +/** + * @event draw:editstop: String + * + * The type of edit this is. One of: `edit` + * + * Triggered when the user has finshed editing (edit mode) and saves edits. + */ +L.Draw.Event.EDITSTOP = 'draw:editstop'; + +/** + * @event draw:deletestart: String + * + * The type of edit this is. One of: `remove` + * + * Triggered when the user starts remove mode by clicking the remove tool button. + */ +L.Draw.Event.DELETESTART = 'draw:deletestart'; + +/** + * @event draw:deletestop: String + * + * The type of edit this is. One of: `remove` + * + * Triggered when the user has finished removing shapes (remove mode) and saves. + */ +L.Draw.Event.DELETESTOP = 'draw:deletestop'; + +/** + * @event draw:toolbaropened: String + * + * Triggered when a toolbar is opened. + */ +L.Draw.Event.TOOLBAROPENED = 'draw:toolbaropened'; + +/** + * @event draw:toolbarclosed: String + * + * Triggered when a toolbar is closed. + */ +L.Draw.Event.TOOLBARCLOSED = 'draw:toolbarclosed'; + +/** + * @event draw:markercontext: String + * + * Triggered when a marker is right clicked. + */ +L.Draw.Event.MARKERCONTEXT = 'draw:markercontext'; + + +L.Draw = L.Draw || {}; + +/** + * @class L.Draw.Feature + * @aka Draw.Feature + */ +L.Draw.Feature = L.Handler.extend({ + + // @method initialize(): void + initialize: function (map, options) { + this._map = map; + this._container = map._container; + this._overlayPane = map._panes.overlayPane; + this._popupPane = map._panes.popupPane; + + // Merge default shapeOptions options with custom shapeOptions + if (options && options.shapeOptions) { + options.shapeOptions = L.Util.extend({}, this.options.shapeOptions, options.shapeOptions); + } + L.setOptions(this, options); + + var version = L.version.split('.'); + //If Version is >= 1.2.0 + if (parseInt(version[0], 10) === 1 && parseInt(version[1], 10) >= 2) { + L.Draw.Feature.include(L.Evented.prototype); + } else { + L.Draw.Feature.include(L.Mixin.Events); + } + }, + + // @method enable(): void + // Enables this handler + enable: function () { + if (this._enabled) { + return; + } + + L.Handler.prototype.enable.call(this); + + this.fire('enabled', {handler: this.type}); + + this._map.fire(L.Draw.Event.DRAWSTART, {layerType: this.type}); + }, + + // @method disable(): void + disable: function () { + if (!this._enabled) { + return; + } + + L.Handler.prototype.disable.call(this); + + this._map.fire(L.Draw.Event.DRAWSTOP, {layerType: this.type}); + + this.fire('disabled', {handler: this.type}); + }, + + // @method addHooks(): void + // Add's event listeners to this handler + addHooks: function () { + var map = this._map; + + if (map) { + L.DomUtil.disableTextSelection(); + + map.getContainer().focus(); + + this._tooltip = new L.Draw.Tooltip(this._map); + + L.DomEvent.on(this._container, 'keyup', this._cancelDrawing, this); + } + }, + + // @method removeHooks(): void + // Removes event listeners from this handler + removeHooks: function () { + if (this._map) { + L.DomUtil.enableTextSelection(); + + this._tooltip.dispose(); + this._tooltip = null; + + L.DomEvent.off(this._container, 'keyup', this._cancelDrawing, this); + } + }, + + // @method setOptions(object): void + // Sets new options to this handler + setOptions: function (options) { + L.setOptions(this, options); + }, + + _fireCreatedEvent: function (layer) { + this._map.fire(L.Draw.Event.CREATED, {layer: layer, layerType: this.type}); + }, + + // Cancel drawing when the escape key is pressed + _cancelDrawing: function (e) { + if (e.keyCode === 27) { + this._map.fire('draw:canceled', {layerType: this.type}); + this.disable(); + } + } +}); + + + +/** + * @class L.Draw.Polyline + * @aka Draw.Polyline + * @inherits L.Draw.Feature + */ +L.Draw.Polyline = L.Draw.Feature.extend({ + statics: { + TYPE: 'polyline' + }, + + Poly: L.Polyline, + + options: { + allowIntersection: true, + repeatMode: false, + drawError: { + color: '#b00b00', + timeout: 2500 + }, + icon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon' + }), + touchIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon' + }), + guidelineDistance: 20, + maxGuideLineLength: 4000, + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: false, + clickable: true + }, + metric: true, // Whether to use the metric measurement system or imperial + feet: true, // When not metric, to use feet instead of yards for display. + nautic: false, // When not metric, not feet use nautic mile for display + showLength: true, // Whether to display distance in the tooltip + zIndexOffset: 2000, // This should be > than the highest z-index any map layers + factor: 1, // To change distance calculation + maxPoints: 0 // Once this number of points are placed, finish shape + }, + + // @method initialize(): void + initialize: function (map, options) { + // if touch, switch to touch icon + if (L.Browser.touch) { + this.options.icon = this.options.touchIcon; + } + + // Need to set this here to ensure the correct message is used. + this.options.drawError.message = L.drawLocal.draw.handlers.polyline.error; + + // Merge default drawError options with custom options + if (options && options.drawError) { + options.drawError = L.Util.extend({}, this.options.drawError, options.drawError); + } + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Polyline.TYPE; + + L.Draw.Feature.prototype.initialize.call(this, map, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this); + if (this._map) { + this._markers = []; + + this._markerGroup = new L.LayerGroup(); + this._map.addLayer(this._markerGroup); + + this._poly = new L.Polyline([], this.options.shapeOptions); + + this._tooltip.updateContent(this._getTooltipText()); + + // Make a transparent marker that will used to catch click events. These click + // events will create the vertices. We need to do this so we can ensure that + // we can create vertices over other map layers (markers, vector layers). We + // also do not want to trigger any click handlers of objects we are clicking on + // while drawing. + if (!this._mouseMarker) { + this._mouseMarker = L.marker(this._map.getCenter(), { + icon: L.divIcon({ + className: 'leaflet-mouse-marker', + iconAnchor: [20, 20], + iconSize: [40, 40] + }), + opacity: 0, + zIndexOffset: this.options.zIndexOffset + }); + } + + this._mouseMarker + .on('mouseout', this._onMouseOut, this) + .on('mousemove', this._onMouseMove, this) // Necessary to prevent 0.8 stutter + .on('mousedown', this._onMouseDown, this) + .on('mouseup', this._onMouseUp, this) // Necessary for 0.8 compatibility + .addTo(this._map); + + this._map + .on('mouseup', this._onMouseUp, this) // Necessary for 0.7 compatibility + .on('mousemove', this._onMouseMove, this) + .on('zoomlevelschange', this._onZoomEnd, this) + .on('touchstart', this._onTouch, this) + .on('zoomend', this._onZoomEnd, this); + + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this); + + this._clearHideErrorTimeout(); + + this._cleanUpShape(); + + // remove markers from map + this._map.removeLayer(this._markerGroup); + delete this._markerGroup; + delete this._markers; + + this._map.removeLayer(this._poly); + delete this._poly; + + this._mouseMarker + .off('mousedown', this._onMouseDown, this) + .off('mouseout', this._onMouseOut, this) + .off('mouseup', this._onMouseUp, this) + .off('mousemove', this._onMouseMove, this); + this._map.removeLayer(this._mouseMarker); + delete this._mouseMarker; + + // clean up DOM + this._clearGuides(); + + this._map + .off('mouseup', this._onMouseUp, this) + .off('mousemove', this._onMouseMove, this) + .off('zoomlevelschange', this._onZoomEnd, this) + .off('zoomend', this._onZoomEnd, this) + .off('touchstart', this._onTouch, this) + .off('click', this._onTouch, this); + }, + + // @method deleteLastVertex(): void + // Remove the last vertex from the polyline, removes polyline from map if only one point exists. + deleteLastVertex: function () { + if (this._markers.length <= 1) { + return; + } + + var lastMarker = this._markers.pop(), + poly = this._poly, + // Replaces .spliceLatLngs() + latlngs = poly.getLatLngs(), + latlng = latlngs.splice(-1, 1)[0]; + this._poly.setLatLngs(latlngs); + + this._markerGroup.removeLayer(lastMarker); + + if (poly.getLatLngs().length < 2) { + this._map.removeLayer(poly); + } + + this._vertexChanged(latlng, false); + }, + + // @method addVertex(): void + // Add a vertex to the end of the polyline + addVertex: function (latlng) { + var markersLength = this._markers.length; + // markersLength must be greater than or equal to 2 before intersections can occur + if (markersLength >= 2 && !this.options.allowIntersection && this._poly.newLatLngIntersects(latlng)) { + this._showErrorTooltip(); + return; + } + else if (this._errorShown) { + this._hideErrorTooltip(); + } + + this._markers.push(this._createMarker(latlng)); + + this._poly.addLatLng(latlng); + + if (this._poly.getLatLngs().length === 2) { + this._map.addLayer(this._poly); + } + + this._vertexChanged(latlng, true); + }, + + // @method completeShape(): void + // Closes the polyline between the first and last points + completeShape: function () { + if (this._markers.length <= 1 || !this._shapeIsValid()) { + return; + } + + this._fireCreatedEvent(); + this.disable(); + + if (this.options.repeatMode) { + this.enable(); + } + }, + + _finishShape: function () { + var latlngs = this._poly._defaultShape ? this._poly._defaultShape() : this._poly.getLatLngs(); + var intersects = this._poly.newLatLngIntersects(latlngs[latlngs.length - 1]); + + if ((!this.options.allowIntersection && intersects) || !this._shapeIsValid()) { + this._showErrorTooltip(); + return; + } + + this._fireCreatedEvent(); + this.disable(); + if (this.options.repeatMode) { + this.enable(); + } + }, + + // Called to verify the shape is valid when the user tries to finish it + // Return false if the shape is not valid + _shapeIsValid: function () { + return true; + }, + + _onZoomEnd: function () { + if (this._markers !== null) { + this._updateGuide(); + } + }, + + _onMouseMove: function (e) { + var newPos = this._map.mouseEventToLayerPoint(e.originalEvent); + var latlng = this._map.layerPointToLatLng(newPos); + + // Save latlng + // should this be moved to _updateGuide() ? + this._currentLatLng = latlng; + + this._updateTooltip(latlng); + + // Update the guide line + this._updateGuide(newPos); + + // Update the mouse marker position + this._mouseMarker.setLatLng(latlng); + + L.DomEvent.preventDefault(e.originalEvent); + }, + + _vertexChanged: function (latlng, added) { + this._map.fire(L.Draw.Event.DRAWVERTEX, {layers: this._markerGroup}); + this._updateFinishHandler(); + + this._updateRunningMeasure(latlng, added); + + this._clearGuides(); + + this._updateTooltip(); + }, + + _onMouseDown: function (e) { + if (!this._clickHandled && !this._touchHandled && !this._disableMarkers) { + this._onMouseMove(e); + this._clickHandled = true; + this._disableNewMarkers(); + var originalEvent = e.originalEvent; + var clientX = originalEvent.clientX; + var clientY = originalEvent.clientY; + this._startPoint.call(this, clientX, clientY); + } + }, + + _startPoint: function (clientX, clientY) { + this._mouseDownOrigin = L.point(clientX, clientY); + }, + + _onMouseUp: function (e) { + var originalEvent = e.originalEvent; + var clientX = originalEvent.clientX; + var clientY = originalEvent.clientY; + this._endPoint.call(this, clientX, clientY, e); + this._clickHandled = null; + }, + + _endPoint: function (clientX, clientY, e) { + if (this._mouseDownOrigin) { + var dragCheckDistance = L.point(clientX, clientY) + .distanceTo(this._mouseDownOrigin); + var lastPtDistance = this._calculateFinishDistance(e.latlng); + if (this.options.maxPoints > 1 && this.options.maxPoints == this._markers.length + 1) { + this.addVertex(e.latlng); + this._finishShape(); + } else if (lastPtDistance < 10 && L.Browser.touch) { + this._finishShape(); + } else if (Math.abs(dragCheckDistance) < 9 * (window.devicePixelRatio || 1)) { + this.addVertex(e.latlng); + } + this._enableNewMarkers(); // after a short pause, enable new markers + } + this._mouseDownOrigin = null; + }, + + // ontouch prevented by clickHandled flag because some browsers fire both click/touch events, + // causing unwanted behavior + _onTouch: function (e) { + var originalEvent = e.originalEvent; + var clientX; + var clientY; + if (originalEvent.touches && originalEvent.touches[0] && !this._clickHandled && !this._touchHandled && !this._disableMarkers) { + clientX = originalEvent.touches[0].clientX; + clientY = originalEvent.touches[0].clientY; + this._disableNewMarkers(); + this._touchHandled = true; + this._startPoint.call(this, clientX, clientY); + this._endPoint.call(this, clientX, clientY, e); + this._touchHandled = null; + } + this._clickHandled = null; + }, + + _onMouseOut: function () { + if (this._tooltip) { + this._tooltip._onMouseOut.call(this._tooltip); + } + }, + + // calculate if we are currently within close enough distance + // of the closing point (first point for shapes, last point for lines) + // this is semi-ugly code but the only reliable way i found to get the job done + // note: calculating point.distanceTo between mouseDownOrigin and last marker did NOT work + _calculateFinishDistance: function (potentialLatLng) { + var lastPtDistance; + if (this._markers.length > 0) { + var finishMarker; + if (this.type === L.Draw.Polyline.TYPE) { + finishMarker = this._markers[this._markers.length - 1]; + } else if (this.type === L.Draw.Polygon.TYPE) { + finishMarker = this._markers[0]; + } else { + return Infinity; + } + var lastMarkerPoint = this._map.latLngToContainerPoint(finishMarker.getLatLng()), + potentialMarker = new L.Marker(potentialLatLng, { + icon: this.options.icon, + zIndexOffset: this.options.zIndexOffset * 2 + }); + var potentialMarkerPint = this._map.latLngToContainerPoint(potentialMarker.getLatLng()); + lastPtDistance = lastMarkerPoint.distanceTo(potentialMarkerPint); + } else { + lastPtDistance = Infinity; + } + return lastPtDistance; + }, + + _updateFinishHandler: function () { + var markerCount = this._markers.length; + // The last marker should have a click handler to close the polyline + if (markerCount > 1) { + this._markers[markerCount - 1].on('click', this._finishShape, this); + } + + // Remove the old marker click handler (as only the last point should close the polyline) + if (markerCount > 2) { + this._markers[markerCount - 2].off('click', this._finishShape, this); + } + }, + + _createMarker: function (latlng) { + var marker = new L.Marker(latlng, { + icon: this.options.icon, + zIndexOffset: this.options.zIndexOffset * 2 + }); + + this._markerGroup.addLayer(marker); + + return marker; + }, + + _updateGuide: function (newPos) { + var markerCount = this._markers ? this._markers.length : 0; + + if (markerCount > 0) { + newPos = newPos || this._map.latLngToLayerPoint(this._currentLatLng); + + // draw the guide line + this._clearGuides(); + this._drawGuide( + this._map.latLngToLayerPoint(this._markers[markerCount - 1].getLatLng()), + newPos + ); + } + }, + + _updateTooltip: function (latLng) { + var text = this._getTooltipText(); + + if (latLng) { + this._tooltip.updatePosition(latLng); + } + + if (!this._errorShown) { + this._tooltip.updateContent(text); + } + }, + + _drawGuide: function (pointA, pointB) { + var length = Math.floor(Math.sqrt(Math.pow((pointB.x - pointA.x), 2) + Math.pow((pointB.y - pointA.y), 2))), + guidelineDistance = this.options.guidelineDistance, + maxGuideLineLength = this.options.maxGuideLineLength, + // Only draw a guideline with a max length + i = length > maxGuideLineLength ? length - maxGuideLineLength : guidelineDistance, + fraction, + dashPoint, + dash; + + //create the guides container if we haven't yet + if (!this._guidesContainer) { + this._guidesContainer = L.DomUtil.create('div', 'leaflet-draw-guides', this._overlayPane); + } + + //draw a dash every GuildeLineDistance + for (; i < length; i += this.options.guidelineDistance) { + //work out fraction along line we are + fraction = i / length; + + //calculate new x,y point + dashPoint = { + x: Math.floor((pointA.x * (1 - fraction)) + (fraction * pointB.x)), + y: Math.floor((pointA.y * (1 - fraction)) + (fraction * pointB.y)) + }; + + //add guide dash to guide container + dash = L.DomUtil.create('div', 'leaflet-draw-guide-dash', this._guidesContainer); + dash.style.backgroundColor = + !this._errorShown ? this.options.shapeOptions.color : this.options.drawError.color; + + L.DomUtil.setPosition(dash, dashPoint); + } + }, + + _updateGuideColor: function (color) { + if (this._guidesContainer) { + for (var i = 0, l = this._guidesContainer.childNodes.length; i < l; i++) { + this._guidesContainer.childNodes[i].style.backgroundColor = color; + } + } + }, + + // removes all child elements (guide dashes) from the guides container + _clearGuides: function () { + if (this._guidesContainer) { + while (this._guidesContainer.firstChild) { + this._guidesContainer.removeChild(this._guidesContainer.firstChild); + } + } + }, + + _getTooltipText: function () { + var showLength = this.options.showLength, + labelText, distanceStr; + if (this._markers.length === 0) { + labelText = { + text: L.drawLocal.draw.handlers.polyline.tooltip.start + }; + } else { + distanceStr = showLength ? this._getMeasurementString() : ''; + + if (this._markers.length === 1) { + labelText = { + text: L.drawLocal.draw.handlers.polyline.tooltip.cont, + subtext: distanceStr + }; + } else { + labelText = { + text: L.drawLocal.draw.handlers.polyline.tooltip.end, + subtext: distanceStr + }; + } + } + return labelText; + }, + + _updateRunningMeasure: function (latlng, added) { + var markersLength = this._markers.length, + previousMarkerIndex, distance; + + if (this._markers.length === 1) { + this._measurementRunningTotal = 0; + } else { + previousMarkerIndex = markersLength - (added ? 2 : 1); + + // Calculate the distance based on the version + if (L.GeometryUtil.isVersion07x()) { + distance = latlng.distanceTo(this._markers[previousMarkerIndex].getLatLng()) * (this.options.factor || 1); + } else { + distance = this._map.distance(latlng, this._markers[previousMarkerIndex].getLatLng()) * (this.options.factor || 1); + } + + this._measurementRunningTotal += distance * (added ? 1 : -1); + } + }, + + _getMeasurementString: function () { + var currentLatLng = this._currentLatLng, + previousLatLng = this._markers[this._markers.length - 1].getLatLng(), + distance; + + // Calculate the distance from the last fixed point to the mouse position based on the version + if (L.GeometryUtil.isVersion07x()) { + distance = previousLatLng && currentLatLng && currentLatLng.distanceTo ? this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0; + } else { + distance = previousLatLng && currentLatLng ? this._measurementRunningTotal + this._map.distance(currentLatLng, previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0; + } + + return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic, this.options.precision); + }, + + _showErrorTooltip: function () { + this._errorShown = true; + + // Update tooltip + this._tooltip + .showAsError() + .updateContent({text: this.options.drawError.message}); + + // Update shape + this._updateGuideColor(this.options.drawError.color); + this._poly.setStyle({color: this.options.drawError.color}); + + // Hide the error after 2 seconds + this._clearHideErrorTimeout(); + this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorTooltip, this), this.options.drawError.timeout); + }, + + _hideErrorTooltip: function () { + this._errorShown = false; + + this._clearHideErrorTimeout(); + + // Revert tooltip + this._tooltip + .removeError() + .updateContent(this._getTooltipText()); + + // Revert shape + this._updateGuideColor(this.options.shapeOptions.color); + this._poly.setStyle({color: this.options.shapeOptions.color}); + }, + + _clearHideErrorTimeout: function () { + if (this._hideErrorTimeout) { + clearTimeout(this._hideErrorTimeout); + this._hideErrorTimeout = null; + } + }, + + // disable new markers temporarily; + // this is to prevent duplicated touch/click events in some browsers + _disableNewMarkers: function () { + this._disableMarkers = true; + }, + + // see _disableNewMarkers + _enableNewMarkers: function () { + setTimeout(function () { + this._disableMarkers = false; + }.bind(this), 50); + }, + + _cleanUpShape: function () { + if (this._markers.length > 1) { + this._markers[this._markers.length - 1].off('click', this._finishShape, this); + } + }, + + _fireCreatedEvent: function () { + var poly = new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions); + L.Draw.Feature.prototype._fireCreatedEvent.call(this, poly); + } +}); + + + +/** + * @class L.Draw.Polygon + * @aka Draw.Polygon + * @inherits L.Draw.Polyline + */ +L.Draw.Polygon = L.Draw.Polyline.extend({ + statics: { + TYPE: 'polygon' + }, + + Poly: L.Polygon, + + options: { + showArea: false, + showLength: false, + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: true, + fillColor: null, //same as color by default + fillOpacity: 0.2, + clickable: true + }, + // Whether to use the metric measurement system (truthy) or not (falsy). + // Also defines the units to use for the metric system as an array of + // strings (e.g. `['ha', 'm']`). + metric: true, + feet: true, // When not metric, to use feet instead of yards for display. + nautic: false, // When not metric, not feet use nautic mile for display + // Defines the precision for each type of unit (e.g. {km: 2, ft: 0} + precision: {} + }, + + // @method initialize(): void + initialize: function (map, options) { + L.Draw.Polyline.prototype.initialize.call(this, map, options); + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Polygon.TYPE; + }, + + _updateFinishHandler: function () { + var markerCount = this._markers.length; + + // The first marker should have a click handler to close the polygon + if (markerCount === 1) { + this._markers[0].on('click', this._finishShape, this); + } + + // Add and update the double click handler + if (markerCount > 2) { + this._markers[markerCount - 1].on('dblclick', this._finishShape, this); + // Only need to remove handler if has been added before + if (markerCount > 3) { + this._markers[markerCount - 2].off('dblclick', this._finishShape, this); + } + } + }, + + _getTooltipText: function () { + var text, subtext; + + if (this._markers.length === 0) { + text = L.drawLocal.draw.handlers.polygon.tooltip.start; + } else if (this._markers.length < 3) { + text = L.drawLocal.draw.handlers.polygon.tooltip.cont; + subtext = this._getMeasurementString(); + } else { + text = L.drawLocal.draw.handlers.polygon.tooltip.end; + subtext = this._getMeasurementString(); + } + + return { + text: text, + subtext: subtext + }; + }, + + _getMeasurementString: function () { + var area = this._area, + measurementString = ''; + + + if (!area && !this.options.showLength) { + return null; + } + + if (this.options.showLength) { + measurementString = L.Draw.Polyline.prototype._getMeasurementString.call(this); + } + + if (area) { + measurementString += '
    ' + L.GeometryUtil.readableArea(area, this.options.metric, this.options.precision); + } + + return measurementString; + }, + + _shapeIsValid: function () { + return this._markers.length >= 3; + }, + + _vertexChanged: function (latlng, added) { + var latLngs; + + // Check to see if we should show the area + if (!this.options.allowIntersection && this.options.showArea) { + latLngs = this._poly.getLatLngs(); + + this._area = L.GeometryUtil.geodesicArea(latLngs); + } + + L.Draw.Polyline.prototype._vertexChanged.call(this, latlng, added); + }, + + _cleanUpShape: function () { + var markerCount = this._markers.length; + + if (markerCount > 0) { + this._markers[0].off('click', this._finishShape, this); + + if (markerCount > 2) { + this._markers[markerCount - 1].off('dblclick', this._finishShape, this); + } + } + } +}); + + + +L.SimpleShape = {}; +/** + * @class L.Draw.SimpleShape + * @aka Draw.SimpleShape + * @inherits L.Draw.Feature + */ +L.Draw.SimpleShape = L.Draw.Feature.extend({ + options: { + repeatMode: false + }, + + // @method initialize(): void + initialize: function (map, options) { + this._endLabelText = L.drawLocal.draw.handlers.simpleshape.tooltip.end; + + L.Draw.Feature.prototype.initialize.call(this, map, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler. + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this); + if (this._map) { + this._mapDraggable = this._map.dragging.enabled(); + + if (this._mapDraggable) { + this._map.dragging.disable(); + } + + //TODO refactor: move cursor to styles + this._container.style.cursor = 'crosshair'; + + this._tooltip.updateContent({text: this._initialLabelText}); + + this._map + .on('mousedown', this._onMouseDown, this) + .on('mousemove', this._onMouseMove, this) + .on('touchstart', this._onMouseDown, this) + .on('touchmove', this._onMouseMove, this); + + // we should prevent default, otherwise default behavior (scrolling) will fire, + // and that will cause document.touchend to fire and will stop the drawing + // (circle, rectangle) in touch mode. + // (update): we have to send passive now to prevent scroll, because by default it is {passive: true} now, which means, + // handler can't event.preventDefault + // check the news https://developers.google.com/web/updates/2016/06/passive-event-listeners + document.addEventListener('touchstart', L.DomEvent.preventDefault, {passive: false}); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this); + if (this._map) { + if (this._mapDraggable) { + this._map.dragging.enable(); + } + + //TODO refactor: move cursor to styles + this._container.style.cursor = ''; + + this._map + .off('mousedown', this._onMouseDown, this) + .off('mousemove', this._onMouseMove, this) + .off('touchstart', this._onMouseDown, this) + .off('touchmove', this._onMouseMove, this); + + L.DomEvent.off(document, 'mouseup', this._onMouseUp, this); + L.DomEvent.off(document, 'touchend', this._onMouseUp, this); + + document.removeEventListener('touchstart', L.DomEvent.preventDefault); + + // If the box element doesn't exist they must not have moved the mouse, so don't need to destroy/return + if (this._shape) { + this._map.removeLayer(this._shape); + delete this._shape; + } + } + this._isDrawing = false; + }, + + _getTooltipText: function () { + return { + text: this._endLabelText + }; + }, + + _onMouseDown: function (e) { + this._isDrawing = true; + this._startLatLng = e.latlng; + + L.DomEvent + .on(document, 'mouseup', this._onMouseUp, this) + .on(document, 'touchend', this._onMouseUp, this) + .preventDefault(e.originalEvent); + }, + + _onMouseMove: function (e) { + var latlng = e.latlng; + + this._tooltip.updatePosition(latlng); + if (this._isDrawing) { + this._tooltip.updateContent(this._getTooltipText()); + this._drawShape(latlng); + } + }, + + _onMouseUp: function () { + if (this._shape) { + this._fireCreatedEvent(); + } + + this.disable(); + if (this.options.repeatMode) { + this.enable(); + } + } +}); + + + +/** + * @class L.Draw.Rectangle + * @aka Draw.Rectangle + * @inherits L.Draw.SimpleShape + */ +L.Draw.Rectangle = L.Draw.SimpleShape.extend({ + statics: { + TYPE: 'rectangle' + }, + + options: { + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: true, + fillColor: null, //same as color by default + fillOpacity: 0.2, + clickable: true + }, + showArea: true, //Whether to show the area in the tooltip + metric: true // Whether to use the metric measurement system or imperial + }, + + // @method initialize(): void + initialize: function (map, options) { + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Rectangle.TYPE; + + this._initialLabelText = L.drawLocal.draw.handlers.rectangle.tooltip.start; + + L.Draw.SimpleShape.prototype.initialize.call(this, map, options); + }, + + // @method disable(): void + disable: function () { + if (!this._enabled) { + return; + } + + this._isCurrentlyTwoClickDrawing = false; + L.Draw.SimpleShape.prototype.disable.call(this); + }, + + _onMouseUp: function (e) { + if (!this._shape && !this._isCurrentlyTwoClickDrawing) { + this._isCurrentlyTwoClickDrawing = true; + return; + } + + // Make sure closing click is on map + if (this._isCurrentlyTwoClickDrawing && !_hasAncestor(e.target, 'leaflet-pane')) { + return; + } + + L.Draw.SimpleShape.prototype._onMouseUp.call(this); + }, + + _drawShape: function (latlng) { + if (!this._shape) { + this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, latlng), this.options.shapeOptions); + this._map.addLayer(this._shape); + } else { + this._shape.setBounds(new L.LatLngBounds(this._startLatLng, latlng)); + } + }, + + _fireCreatedEvent: function () { + var rectangle = new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions); + L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle); + }, + + _getTooltipText: function () { + var tooltipText = L.Draw.SimpleShape.prototype._getTooltipText.call(this), + shape = this._shape, + showArea = this.options.showArea, + latLngs, area, subtext; + + if (shape) { + latLngs = this._shape._defaultShape ? this._shape._defaultShape() : this._shape.getLatLngs(); + area = L.GeometryUtil.geodesicArea(latLngs); + subtext = showArea ? L.GeometryUtil.readableArea(area, this.options.metric) : ''; + } + + return { + text: tooltipText.text, + subtext: subtext + }; + } +}); + +function _hasAncestor(el, cls) { + while ((el = el.parentElement) && !el.classList.contains(cls)) { + ; + } + return el; +} + + + +/** + * @class L.Draw.Marker + * @aka Draw.Marker + * @inherits L.Draw.Feature + */ +L.Draw.Marker = L.Draw.Feature.extend({ + statics: { + TYPE: 'marker' + }, + + options: { + icon: new L.Icon.Default(), + repeatMode: false, + zIndexOffset: 2000 // This should be > than the highest z-index any markers + }, + + // @method initialize(): void + initialize: function (map, options) { + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Marker.TYPE; + + this._initialLabelText = L.drawLocal.draw.handlers.marker.tooltip.start; + + L.Draw.Feature.prototype.initialize.call(this, map, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler. + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this); + + if (this._map) { + this._tooltip.updateContent({text: this._initialLabelText}); + + // Same mouseMarker as in Draw.Polyline + if (!this._mouseMarker) { + this._mouseMarker = L.marker(this._map.getCenter(), { + icon: L.divIcon({ + className: 'leaflet-mouse-marker', + iconAnchor: [20, 20], + iconSize: [40, 40] + }), + opacity: 0, + zIndexOffset: this.options.zIndexOffset + }); + } + + this._mouseMarker + .on('click', this._onClick, this) + .addTo(this._map); + + this._map.on('mousemove', this._onMouseMove, this); + this._map.on('click', this._onTouch, this); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this); + + if (this._map) { + this._map + .off('click', this._onClick, this) + .off('click', this._onTouch, this); + if (this._marker) { + this._marker.off('click', this._onClick, this); + this._map + .removeLayer(this._marker); + delete this._marker; + } + + this._mouseMarker.off('click', this._onClick, this); + this._map.removeLayer(this._mouseMarker); + delete this._mouseMarker; + + this._map.off('mousemove', this._onMouseMove, this); + } + }, + + _onMouseMove: function (e) { + var latlng = e.latlng; + + this._tooltip.updatePosition(latlng); + this._mouseMarker.setLatLng(latlng); + + if (!this._marker) { + this._marker = this._createMarker(latlng); + // Bind to both marker and map to make sure we get the click event. + this._marker.on('click', this._onClick, this); + this._map + .on('click', this._onClick, this) + .addLayer(this._marker); + } + else { + latlng = this._mouseMarker.getLatLng(); + this._marker.setLatLng(latlng); + } + }, + + _createMarker: function (latlng) { + return new L.Marker(latlng, { + icon: this.options.icon, + zIndexOffset: this.options.zIndexOffset + }); + }, + + _onClick: function () { + this._fireCreatedEvent(); + + this.disable(); + if (this.options.repeatMode) { + this.enable(); + } + }, + + _onTouch: function (e) { + // called on click & tap, only really does any thing on tap + this._onMouseMove(e); // creates & places marker + this._onClick(); // permanently places marker & ends interaction + }, + + _fireCreatedEvent: function () { + var marker = new L.Marker.Touch(this._marker.getLatLng(), {icon: this.options.icon}); + L.Draw.Feature.prototype._fireCreatedEvent.call(this, marker); + } +}); + + + +/** + * @class L.Draw.CircleMarker + * @aka Draw.CircleMarker + * @inherits L.Draw.Marker + */ +L.Draw.CircleMarker = L.Draw.Marker.extend({ + statics: { + TYPE: 'circlemarker' + }, + + options: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: true, + fillColor: null, //same as color by default + fillOpacity: 0.2, + clickable: true, + zIndexOffset: 2000 // This should be > than the highest z-index any markers + }, + + // @method initialize(): void + initialize: function (map, options) { + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.CircleMarker.TYPE; + + this._initialLabelText = L.drawLocal.draw.handlers.circlemarker.tooltip.start; + + L.Draw.Feature.prototype.initialize.call(this, map, options); + }, + + + _fireCreatedEvent: function () { + var circleMarker = new L.CircleMarker(this._marker.getLatLng(), this.options); + L.Draw.Feature.prototype._fireCreatedEvent.call(this, circleMarker); + }, + + _createMarker: function (latlng) { + return new L.CircleMarker(latlng, this.options); + } +}); + + + +/** + * @class L.Draw.Circle + * @aka Draw.Circle + * @inherits L.Draw.SimpleShape + */ +L.Draw.Circle = L.Draw.SimpleShape.extend({ + statics: { + TYPE: 'circle' + }, + + options: { + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: true, + fillColor: null, //same as color by default + fillOpacity: 0.2, + clickable: true + }, + showRadius: true, + metric: true, // Whether to use the metric measurement system or imperial + feet: true, // When not metric, use feet instead of yards for display + nautic: false // When not metric, not feet use nautic mile for display + }, + + // @method initialize(): void + initialize: function (map, options) { + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Circle.TYPE; + + this._initialLabelText = L.drawLocal.draw.handlers.circle.tooltip.start; + + L.Draw.SimpleShape.prototype.initialize.call(this, map, options); + }, + + _drawShape: function (latlng) { + // Calculate the distance based on the version + if (L.GeometryUtil.isVersion07x()) { + var distance = this._startLatLng.distanceTo(latlng); + } else { + var distance = this._map.distance(this._startLatLng, latlng); + } + + if (!this._shape) { + this._shape = new L.Circle(this._startLatLng, distance, this.options.shapeOptions); + this._map.addLayer(this._shape); + } else { + this._shape.setRadius(distance); + } + }, + + _fireCreatedEvent: function () { + var circle = new L.Circle(this._startLatLng, this._shape.getRadius(), this.options.shapeOptions); + L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, circle); + }, + + _onMouseMove: function (e) { + var latlng = e.latlng, + showRadius = this.options.showRadius, + useMetric = this.options.metric, + radius; + + this._tooltip.updatePosition(latlng); + if (this._isDrawing) { + this._drawShape(latlng); + + // Get the new radius (rounded to 1 dp) + radius = this._shape.getRadius().toFixed(1); + + var subtext = ''; + if (showRadius) { + subtext = L.drawLocal.draw.handlers.circle.radius + ': ' + + L.GeometryUtil.readableDistance(radius, useMetric, this.options.feet, this.options.nautic); + } + this._tooltip.updateContent({ + text: this._endLabelText, + subtext: subtext + }); + } + } +}); + + + +L.Edit = L.Edit || {}; + +/** + * @class L.Edit.Marker + * @aka Edit.Marker + */ +L.Edit.Marker = L.Handler.extend({ + // @method initialize(): void + initialize: function (marker, options) { + this._marker = marker; + L.setOptions(this, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + var marker = this._marker; + + marker.dragging.enable(); + marker.on('dragend', this._onDragEnd, marker); + this._toggleMarkerHighlight(); + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + var marker = this._marker; + + marker.dragging.disable(); + marker.off('dragend', this._onDragEnd, marker); + this._toggleMarkerHighlight(); + }, + + _onDragEnd: function (e) { + var layer = e.target; + layer.edited = true; + this._map.fire(L.Draw.Event.EDITMOVE, {layer: layer}); + }, + + _toggleMarkerHighlight: function () { + var icon = this._marker._icon; + + // Don't do anything if this layer is a marker but doesn't have an icon. Markers + // should usually have icons. If using Leaflet.draw with Leaflet.markercluster there + // is a chance that a marker doesn't. + if (!icon) { + return; + } + + // This is quite naughty, but I don't see another way of doing it. (short of setting a new icon) + icon.style.display = 'none'; + + if (L.DomUtil.hasClass(icon, 'leaflet-edit-marker-selected')) { + L.DomUtil.removeClass(icon, 'leaflet-edit-marker-selected'); + // Offset as the border will make the icon move. + this._offsetMarker(icon, -4); + + } else { + L.DomUtil.addClass(icon, 'leaflet-edit-marker-selected'); + // Offset as the border will make the icon move. + this._offsetMarker(icon, 4); + } + + icon.style.display = ''; + }, + + _offsetMarker: function (icon, offset) { + var iconMarginTop = parseInt(icon.style.marginTop, 10) - offset, + iconMarginLeft = parseInt(icon.style.marginLeft, 10) - offset; + + icon.style.marginTop = iconMarginTop + 'px'; + icon.style.marginLeft = iconMarginLeft + 'px'; + } +}); + +L.Marker.addInitHook(function () { + if (L.Edit.Marker) { + this.editing = new L.Edit.Marker(this); + + if (this.options.editable) { + this.editing.enable(); + } + } +}); + + + +L.Edit = L.Edit || {}; + +/** + * @class L.Edit.Polyline + * @aka L.Edit.Poly + * @aka Edit.Poly + */ +L.Edit.Poly = L.Handler.extend({ + // @method initialize(): void + initialize: function (poly) { + + this.latlngs = [poly._latlngs]; + if (poly._holes) { + this.latlngs = this.latlngs.concat(poly._holes); + } + + this._poly = poly; + + this._poly.on('revert-edited', this._updateLatLngs, this); + }, + + // Compatibility method to normalize Poly* objects + // between 0.7.x and 1.0+ + _defaultShape: function () { + if (!L.Polyline._flat) { + return this._poly._latlngs; + } + return L.Polyline._flat(this._poly._latlngs) ? this._poly._latlngs : this._poly._latlngs[0]; + }, + + _eachVertexHandler: function (callback) { + for (var i = 0; i < this._verticesHandlers.length; i++) { + callback(this._verticesHandlers[i]); + } + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + this._initHandlers(); + this._eachVertexHandler(function (handler) { + handler.addHooks(); + }); + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + this._eachVertexHandler(function (handler) { + handler.removeHooks(); + }); + }, + + // @method updateMarkers(): void + // Fire an update for each vertex handler + updateMarkers: function () { + this._eachVertexHandler(function (handler) { + handler.updateMarkers(); + }); + }, + + _initHandlers: function () { + this._verticesHandlers = []; + for (var i = 0; i < this.latlngs.length; i++) { + this._verticesHandlers.push(new L.Edit.PolyVerticesEdit(this._poly, this.latlngs[i], this._poly.options.poly)); + } + }, + + _updateLatLngs: function (e) { + this.latlngs = [e.layer._latlngs]; + if (e.layer._holes) { + this.latlngs = this.latlngs.concat(e.layer._holes); + } + } + +}); + +/** + * @class L.Edit.PolyVerticesEdit + * @aka Edit.PolyVerticesEdit + */ +L.Edit.PolyVerticesEdit = L.Handler.extend({ + options: { + icon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon' + }), + touchIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon' + }), + drawError: { + color: '#b00b00', + timeout: 1000 + } + + + }, + + // @method intialize(): void + initialize: function (poly, latlngs, options) { + // if touch, switch to touch icon + if (L.Browser.touch) { + this.options.icon = this.options.touchIcon; + } + this._poly = poly; + + if (options && options.drawError) { + options.drawError = L.Util.extend({}, this.options.drawError, options.drawError); + } + + this._latlngs = latlngs; + + L.setOptions(this, options); + }, + + // Compatibility method to normalize Poly* objects + // between 0.7.x and 1.0+ + _defaultShape: function () { + if (!L.Polyline._flat) { + return this._latlngs; + } + return L.Polyline._flat(this._latlngs) ? this._latlngs : this._latlngs[0]; + }, + + // @method addHooks(): void + // Add listener hooks to this handler. + addHooks: function () { + var poly = this._poly; + var path = poly._path; + + if (!(poly instanceof L.Polygon)) { + poly.options.fill = false; + if (poly.options.editing) { + poly.options.editing.fill = false; + } + } + + if (path) { + if (poly.options.editing && poly.options.editing.className) { + if (poly.options.original.className) { + poly.options.original.className.split(' ').forEach(function (className) { + L.DomUtil.removeClass(path, className); + }); + } + poly.options.editing.className.split(' ').forEach(function (className) { + L.DomUtil.addClass(path, className); + }); + } + } + + poly.setStyle(poly.options.editing); + + if (this._poly._map) { + + this._map = this._poly._map; // Set map + + if (!this._markerGroup) { + this._initMarkers(); + } + this._poly._map.addLayer(this._markerGroup); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + var poly = this._poly; + var path = poly._path; + + if (path) { + if (poly.options.editing && poly.options.editing.className) { + poly.options.editing.className.split(' ').forEach(function (className) { + L.DomUtil.removeClass(path, className); + }); + if (poly.options.original.className) { + poly.options.original.className.split(' ').forEach(function (className) { + L.DomUtil.addClass(path, className); + }); + } + } + } + + poly.setStyle(poly.options.original); + + if (poly._map) { + poly._map.removeLayer(this._markerGroup); + delete this._markerGroup; + delete this._markers; + } + }, + + // @method updateMarkers(): void + // Clear markers and update their location + updateMarkers: function () { + this._markerGroup.clearLayers(); + this._initMarkers(); + }, + + _initMarkers: function () { + if (!this._markerGroup) { + this._markerGroup = new L.LayerGroup(); + } + this._markers = []; + + var latlngs = this._defaultShape(), + i, j, len, marker; + + for (i = 0, len = latlngs.length; i < len; i++) { + + marker = this._createMarker(latlngs[i], i); + marker.on('click', this._onMarkerClick, this); + marker.on('contextmenu', this._onContextMenu, this); + this._markers.push(marker); + } + + var markerLeft, markerRight; + + for (i = 0, j = len - 1; i < len; j = i++) { + if (i === 0 && !(L.Polygon && (this._poly instanceof L.Polygon))) { + continue; + } + + markerLeft = this._markers[j]; + markerRight = this._markers[i]; + + this._createMiddleMarker(markerLeft, markerRight); + this._updatePrevNext(markerLeft, markerRight); + } + }, + + _createMarker: function (latlng, index) { + // Extending L.Marker in TouchEvents.js to include touch. + var marker = new L.Marker.Touch(latlng, { + draggable: true, + icon: this.options.icon, + }); + + marker._origLatLng = latlng; + marker._index = index; + + marker + .on('dragstart', this._onMarkerDragStart, this) + .on('drag', this._onMarkerDrag, this) + .on('dragend', this._fireEdit, this) + .on('touchmove', this._onTouchMove, this) + .on('touchend', this._fireEdit, this) + .on('MSPointerMove', this._onTouchMove, this) + .on('MSPointerUp', this._fireEdit, this); + + this._markerGroup.addLayer(marker); + + return marker; + }, + + _onMarkerDragStart: function () { + this._poly.fire('editstart'); + }, + + _spliceLatLngs: function () { + var latlngs = this._defaultShape(); + var removed = [].splice.apply(latlngs, arguments); + this._poly._convertLatLngs(latlngs, true); + this._poly.redraw(); + return removed; + }, + + _removeMarker: function (marker) { + var i = marker._index; + + this._markerGroup.removeLayer(marker); + this._markers.splice(i, 1); + this._spliceLatLngs(i, 1); + this._updateIndexes(i, -1); + + marker + .off('dragstart', this._onMarkerDragStart, this) + .off('drag', this._onMarkerDrag, this) + .off('dragend', this._fireEdit, this) + .off('touchmove', this._onMarkerDrag, this) + .off('touchend', this._fireEdit, this) + .off('click', this._onMarkerClick, this) + .off('MSPointerMove', this._onTouchMove, this) + .off('MSPointerUp', this._fireEdit, this); + }, + + _fireEdit: function () { + this._poly.edited = true; + this._poly.fire('edit'); + this._poly._map.fire(L.Draw.Event.EDITVERTEX, {layers: this._markerGroup, poly: this._poly}); + }, + + _onMarkerDrag: function (e) { + var marker = e.target; + var poly = this._poly; + + var oldOrigLatLng = L.LatLngUtil.cloneLatLng(marker._origLatLng); + L.extend(marker._origLatLng, marker._latlng); + if (poly.options.poly) { + var tooltip = poly._map._editTooltip; // Access the tooltip + + // If we don't allow intersections and the polygon intersects + if (!poly.options.poly.allowIntersection && poly.intersects()) { + L.extend(marker._origLatLng, oldOrigLatLng); + marker.setLatLng(oldOrigLatLng); + var originalColor = poly.options.color; + poly.setStyle({color: this.options.drawError.color}); + if (tooltip) { + tooltip.updateContent({ + text: L.drawLocal.draw.handlers.polyline.error + }); + } + + // Reset everything back to normal after a second + setTimeout(function () { + poly.setStyle({color: originalColor}); + if (tooltip) { + tooltip.updateContent({ + text: L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext + }); + } + }, 1000); + } + } + + if (marker._middleLeft) { + marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker)); + } + if (marker._middleRight) { + marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next)); + } + + //refresh the bounds when draging + this._poly._bounds._southWest = L.latLng(Infinity, Infinity); + this._poly._bounds._northEast = L.latLng(-Infinity, -Infinity); + var latlngs = this._poly.getLatLngs(); + this._poly._convertLatLngs(latlngs, true); + this._poly.redraw(); + this._poly.fire('editdrag'); + }, + + _onMarkerClick: function (e) { + + var minPoints = L.Polygon && (this._poly instanceof L.Polygon) ? 4 : 3, + marker = e.target; + + // If removing this point would create an invalid polyline/polygon don't remove + if (this._defaultShape().length < minPoints) { + return; + } + + // remove the marker + this._removeMarker(marker); + + // update prev/next links of adjacent markers + this._updatePrevNext(marker._prev, marker._next); + + // remove ghost markers near the removed marker + if (marker._middleLeft) { + this._markerGroup.removeLayer(marker._middleLeft); + } + if (marker._middleRight) { + this._markerGroup.removeLayer(marker._middleRight); + } + + // create a ghost marker in place of the removed one + if (marker._prev && marker._next) { + this._createMiddleMarker(marker._prev, marker._next); + + } else if (!marker._prev) { + marker._next._middleLeft = null; + + } else if (!marker._next) { + marker._prev._middleRight = null; + } + + this._fireEdit(); + }, + + _onContextMenu: function (e) { + var marker = e.target; + var poly = this._poly; + this._poly._map.fire(L.Draw.Event.MARKERCONTEXT, {marker: marker, layers: this._markerGroup, poly: this._poly}); + L.DomEvent.stopPropagation; + }, + + _onTouchMove: function (e) { + + var layerPoint = this._map.mouseEventToLayerPoint(e.originalEvent.touches[0]), + latlng = this._map.layerPointToLatLng(layerPoint), + marker = e.target; + + L.extend(marker._origLatLng, latlng); + + if (marker._middleLeft) { + marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker)); + } + if (marker._middleRight) { + marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next)); + } + + this._poly.redraw(); + this.updateMarkers(); + }, + + _updateIndexes: function (index, delta) { + this._markerGroup.eachLayer(function (marker) { + if (marker._index > index) { + marker._index += delta; + } + }); + }, + + _createMiddleMarker: function (marker1, marker2) { + var latlng = this._getMiddleLatLng(marker1, marker2), + marker = this._createMarker(latlng), + onClick, + onDragStart, + onDragEnd; + + marker.setOpacity(0.6); + + marker1._middleRight = marker2._middleLeft = marker; + + onDragStart = function () { + marker.off('touchmove', onDragStart, this); + var i = marker2._index; + + marker._index = i; + + marker + .off('click', onClick, this) + .on('click', this._onMarkerClick, this); + + latlng.lat = marker.getLatLng().lat; + latlng.lng = marker.getLatLng().lng; + this._spliceLatLngs(i, 0, latlng); + this._markers.splice(i, 0, marker); + + marker.setOpacity(1); + + this._updateIndexes(i, 1); + marker2._index++; + this._updatePrevNext(marker1, marker); + this._updatePrevNext(marker, marker2); + + this._poly.fire('editstart'); + }; + + onDragEnd = function () { + marker.off('dragstart', onDragStart, this); + marker.off('dragend', onDragEnd, this); + marker.off('touchmove', onDragStart, this); + + this._createMiddleMarker(marker1, marker); + this._createMiddleMarker(marker, marker2); + }; + + onClick = function () { + onDragStart.call(this); + onDragEnd.call(this); + this._fireEdit(); + }; + + marker + .on('click', onClick, this) + .on('dragstart', onDragStart, this) + .on('dragend', onDragEnd, this) + .on('touchmove', onDragStart, this); + + this._markerGroup.addLayer(marker); + }, + + _updatePrevNext: function (marker1, marker2) { + if (marker1) { + marker1._next = marker2; + } + if (marker2) { + marker2._prev = marker1; + } + }, + + _getMiddleLatLng: function (marker1, marker2) { + var map = this._poly._map, + p1 = map.project(marker1.getLatLng()), + p2 = map.project(marker2.getLatLng()); + + return map.unproject(p1._add(p2)._divideBy(2)); + } +}); + +L.Polyline.addInitHook(function () { + + // Check to see if handler has already been initialized. This is to support versions of Leaflet that still have L.Handler.PolyEdit + if (this.editing) { + return; + } + + if (L.Edit.Poly) { + + this.editing = new L.Edit.Poly(this); + + if (this.options.editable) { + this.editing.enable(); + } + } + + this.on('add', function () { + if (this.editing && this.editing.enabled()) { + this.editing.addHooks(); + } + }); + + this.on('remove', function () { + if (this.editing && this.editing.enabled()) { + this.editing.removeHooks(); + } + }); +}); + + + +L.Edit = L.Edit || {}; +/** + * @class L.Edit.SimpleShape + * @aka Edit.SimpleShape + */ +L.Edit.SimpleShape = L.Handler.extend({ + options: { + moveIcon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-move' + }), + resizeIcon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-resize' + }), + touchMoveIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-move leaflet-touch-icon' + }), + touchResizeIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-resize leaflet-touch-icon' + }), + }, + + // @method intialize(): void + initialize: function (shape, options) { + // if touch, switch to touch icon + if (L.Browser.touch) { + this.options.moveIcon = this.options.touchMoveIcon; + this.options.resizeIcon = this.options.touchResizeIcon; + } + + this._shape = shape; + L.Util.setOptions(this, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + var shape = this._shape; + if (this._shape._map) { + this._map = this._shape._map; + shape.setStyle(shape.options.editing); + + if (shape._map) { + this._map = shape._map; + if (!this._markerGroup) { + this._initMarkers(); + } + this._map.addLayer(this._markerGroup); + } + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + var shape = this._shape; + + shape.setStyle(shape.options.original); + + if (shape._map) { + this._unbindMarker(this._moveMarker); + + for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { + this._unbindMarker(this._resizeMarkers[i]); + } + this._resizeMarkers = null; + + this._map.removeLayer(this._markerGroup); + delete this._markerGroup; + } + + this._map = null; + }, + + // @method updateMarkers(): void + // Remove the edit markers from this layer + updateMarkers: function () { + this._markerGroup.clearLayers(); + this._initMarkers(); + }, + + _initMarkers: function () { + if (!this._markerGroup) { + this._markerGroup = new L.LayerGroup(); + } + + // Create center marker + this._createMoveMarker(); + + // Create edge marker + this._createResizeMarker(); + }, + + _createMoveMarker: function () { + // Children override + }, + + _createResizeMarker: function () { + // Children override + }, + + _createMarker: function (latlng, icon) { + // Extending L.Marker in TouchEvents.js to include touch. + var marker = new L.Marker.Touch(latlng, { + draggable: true, + icon: icon, + zIndexOffset: 10 + }); + + this._bindMarker(marker); + + this._markerGroup.addLayer(marker); + + return marker; + }, + + _bindMarker: function (marker) { + marker + .on('dragstart', this._onMarkerDragStart, this) + .on('drag', this._onMarkerDrag, this) + .on('dragend', this._onMarkerDragEnd, this) + .on('touchstart', this._onTouchStart, this) + .on('touchmove', this._onTouchMove, this) + .on('MSPointerMove', this._onTouchMove, this) + .on('touchend', this._onTouchEnd, this) + .on('MSPointerUp', this._onTouchEnd, this); + }, + + _unbindMarker: function (marker) { + marker + .off('dragstart', this._onMarkerDragStart, this) + .off('drag', this._onMarkerDrag, this) + .off('dragend', this._onMarkerDragEnd, this) + .off('touchstart', this._onTouchStart, this) + .off('touchmove', this._onTouchMove, this) + .off('MSPointerMove', this._onTouchMove, this) + .off('touchend', this._onTouchEnd, this) + .off('MSPointerUp', this._onTouchEnd, this); + }, + + _onMarkerDragStart: function (e) { + var marker = e.target; + marker.setOpacity(0); + + this._shape.fire('editstart'); + }, + + _fireEdit: function () { + this._shape.edited = true; + this._shape.fire('edit'); + }, + + _onMarkerDrag: function (e) { + var marker = e.target, + latlng = marker.getLatLng(); + + if (marker === this._moveMarker) { + this._move(latlng); + } else { + this._resize(latlng); + } + + this._shape.redraw(); + this._shape.fire('editdrag'); + }, + + _onMarkerDragEnd: function (e) { + var marker = e.target; + marker.setOpacity(1); + + this._fireEdit(); + }, + + _onTouchStart: function (e) { + L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this, e); + + if (typeof(this._getCorners) === 'function') { + // Save a reference to the opposite point + var corners = this._getCorners(), + marker = e.target, + currentCornerIndex = marker._cornerIndex; + + marker.setOpacity(0); + + // Copyed from Edit.Rectangle.js line 23 _onMarkerDragStart() + // Latlng is null otherwise. + this._oppositeCorner = corners[(currentCornerIndex + 2) % 4]; + this._toggleCornerMarkers(0, currentCornerIndex); + } + + this._shape.fire('editstart'); + }, + + _onTouchMove: function (e) { + var layerPoint = this._map.mouseEventToLayerPoint(e.originalEvent.touches[0]), + latlng = this._map.layerPointToLatLng(layerPoint), + marker = e.target; + + if (marker === this._moveMarker) { + this._move(latlng); + } else { + this._resize(latlng); + } + + this._shape.redraw(); + + // prevent touchcancel in IOS + // e.preventDefault(); + return false; + }, + + _onTouchEnd: function (e) { + var marker = e.target; + marker.setOpacity(1); + this.updateMarkers(); + this._fireEdit(); + }, + + _move: function () { + // Children override + }, + + _resize: function () { + // Children override + } +}); + + + +L.Edit = L.Edit || {}; +/** + * @class L.Edit.Rectangle + * @aka Edit.Rectangle + * @inherits L.Edit.SimpleShape + */ +L.Edit.Rectangle = L.Edit.SimpleShape.extend({ + _createMoveMarker: function () { + var bounds = this._shape.getBounds(), + center = bounds.getCenter(); + + this._moveMarker = this._createMarker(center, this.options.moveIcon); + }, + + _createResizeMarker: function () { + var corners = this._getCorners(); + + this._resizeMarkers = []; + + for (var i = 0, l = corners.length; i < l; i++) { + this._resizeMarkers.push(this._createMarker(corners[i], this.options.resizeIcon)); + // Monkey in the corner index as we will need to know this for dragging + this._resizeMarkers[i]._cornerIndex = i; + } + }, + + _onMarkerDragStart: function (e) { + L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this, e); + + // Save a reference to the opposite point + var corners = this._getCorners(), + marker = e.target, + currentCornerIndex = marker._cornerIndex; + + this._oppositeCorner = corners[(currentCornerIndex + 2) % 4]; + + this._toggleCornerMarkers(0, currentCornerIndex); + }, + + _onMarkerDragEnd: function (e) { + var marker = e.target, + bounds, center; + + // Reset move marker position to the center + if (marker === this._moveMarker) { + bounds = this._shape.getBounds(); + center = bounds.getCenter(); + + marker.setLatLng(center); + } + + this._toggleCornerMarkers(1); + + this._repositionCornerMarkers(); + + L.Edit.SimpleShape.prototype._onMarkerDragEnd.call(this, e); + }, + + _move: function (newCenter) { + var latlngs = this._shape._defaultShape ? this._shape._defaultShape() : this._shape.getLatLngs(), + bounds = this._shape.getBounds(), + center = bounds.getCenter(), + offset, newLatLngs = []; + + // Offset the latlngs to the new center + for (var i = 0, l = latlngs.length; i < l; i++) { + offset = [latlngs[i].lat - center.lat, latlngs[i].lng - center.lng]; + newLatLngs.push([newCenter.lat + offset[0], newCenter.lng + offset[1]]); + } + + this._shape.setLatLngs(newLatLngs); + + // Reposition the resize markers + this._repositionCornerMarkers(); + + this._map.fire(L.Draw.Event.EDITMOVE, {layer: this._shape}); + }, + + _resize: function (latlng) { + var bounds; + + // Update the shape based on the current position of this corner and the opposite point + this._shape.setBounds(L.latLngBounds(latlng, this._oppositeCorner)); + + // Reposition the move marker + bounds = this._shape.getBounds(); + this._moveMarker.setLatLng(bounds.getCenter()); + + this._map.fire(L.Draw.Event.EDITRESIZE, {layer: this._shape}); + }, + + _getCorners: function () { + var bounds = this._shape.getBounds(), + nw = bounds.getNorthWest(), + ne = bounds.getNorthEast(), + se = bounds.getSouthEast(), + sw = bounds.getSouthWest(); + + return [nw, ne, se, sw]; + }, + + _toggleCornerMarkers: function (opacity) { + for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { + this._resizeMarkers[i].setOpacity(opacity); + } + }, + + _repositionCornerMarkers: function () { + var corners = this._getCorners(); + + for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { + this._resizeMarkers[i].setLatLng(corners[i]); + } + } +}); + +L.Rectangle.addInitHook(function () { + if (L.Edit.Rectangle) { + this.editing = new L.Edit.Rectangle(this); + + if (this.options.editable) { + this.editing.enable(); + } + } +}); + + + +L.Edit = L.Edit || {}; +/** + * @class L.Edit.CircleMarker + * @aka Edit.Circle + * @inherits L.Edit.SimpleShape + */ +L.Edit.CircleMarker = L.Edit.SimpleShape.extend({ + _createMoveMarker: function () { + var center = this._shape.getLatLng(); + + this._moveMarker = this._createMarker(center, this.options.moveIcon); + }, + + _createResizeMarker: function () { + // To avoid an undefined check in L.Edit.SimpleShape.removeHooks + this._resizeMarkers = []; + }, + + _move: function (latlng) { + if (this._resizeMarkers.length) { + var resizemarkerPoint = this._getResizeMarkerPoint(latlng); + // Move the resize marker + this._resizeMarkers[0].setLatLng(resizemarkerPoint); + } + + // Move the circle + this._shape.setLatLng(latlng); + + this._map.fire(L.Draw.Event.EDITMOVE, {layer: this._shape}); + }, +}); + +L.CircleMarker.addInitHook(function () { + if (L.Edit.CircleMarker) { + this.editing = new L.Edit.CircleMarker(this); + + if (this.options.editable) { + this.editing.enable(); + } + } + + this.on('add', function () { + if (this.editing && this.editing.enabled()) { + this.editing.addHooks(); + } + }); + + this.on('remove', function () { + if (this.editing && this.editing.enabled()) { + this.editing.removeHooks(); + } + }); +}); + + + +L.Edit = L.Edit || {}; +/** + * @class L.Edit.Circle + * @aka Edit.Circle + * @inherits L.Edit.CircleMarker + */ +L.Edit.Circle = L.Edit.CircleMarker.extend({ + + _createResizeMarker: function () { + var center = this._shape.getLatLng(), + resizemarkerPoint = this._getResizeMarkerPoint(center); + + this._resizeMarkers = []; + this._resizeMarkers.push(this._createMarker(resizemarkerPoint, this.options.resizeIcon)); + }, + + _getResizeMarkerPoint: function (latlng) { + // From L.shape.getBounds() + var delta = this._shape._radius * Math.cos(Math.PI / 4), + point = this._map.project(latlng); + return this._map.unproject([point.x + delta, point.y - delta]); + }, + + _resize: function (latlng) { + var moveLatLng = this._moveMarker.getLatLng(); + + // Calculate the radius based on the version + if (L.GeometryUtil.isVersion07x()) { + radius = moveLatLng.distanceTo(latlng); + } else { + radius = this._map.distance(moveLatLng, latlng); + } + this._shape.setRadius(radius); + + if (this._map.editTooltip) { + this._map._editTooltip.updateContent({ + text: L.drawLocal.edit.handlers.edit.tooltip.subtext + '
    ' + L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.draw.handlers.circle.radius + ': ' + + L.GeometryUtil.readableDistance(radius, true, this.options.feet, this.options.nautic) + }); + } + + this._shape.setRadius(radius); + + this._map.fire(L.Draw.Event.EDITRESIZE, {layer: this._shape}); + } +}); + +L.Circle.addInitHook(function () { + if (L.Edit.Circle) { + this.editing = new L.Edit.Circle(this); + + if (this.options.editable) { + this.editing.enable(); + } + } +}); + + + +L.Map.mergeOptions({ + touchExtend: true +}); + +/** + * @class L.Map.TouchExtend + * @aka TouchExtend + */ +L.Map.TouchExtend = L.Handler.extend({ + + // @method initialize(): void + // Sets TouchExtend private accessor variables + initialize: function (map) { + this._map = map; + this._container = map._container; + this._pane = map._panes.overlayPane; + }, + + // @method addHooks(): void + // Adds dom listener events to the map container + addHooks: function () { + L.DomEvent.on(this._container, 'touchstart', this._onTouchStart, this); + L.DomEvent.on(this._container, 'touchend', this._onTouchEnd, this); + L.DomEvent.on(this._container, 'touchmove', this._onTouchMove, this); + if (this._detectIE()) { + L.DomEvent.on(this._container, 'MSPointerDown', this._onTouchStart, this); + L.DomEvent.on(this._container, 'MSPointerUp', this._onTouchEnd, this); + L.DomEvent.on(this._container, 'MSPointerMove', this._onTouchMove, this); + L.DomEvent.on(this._container, 'MSPointerCancel', this._onTouchCancel, this); + + } else { + L.DomEvent.on(this._container, 'touchcancel', this._onTouchCancel, this); + L.DomEvent.on(this._container, 'touchleave', this._onTouchLeave, this); + } + }, + + // @method removeHooks(): void + // Removes dom listener events from the map container + removeHooks: function () { + L.DomEvent.off(this._container, 'touchstart', this._onTouchStart, this); + L.DomEvent.off(this._container, 'touchend', this._onTouchEnd, this); + L.DomEvent.off(this._container, 'touchmove', this._onTouchMove, this); + if (this._detectIE()) { + L.DomEvent.off(this._container, 'MSPointerDown', this._onTouchStart, this); + L.DomEvent.off(this._container, 'MSPointerUp', this._onTouchEnd, this); + L.DomEvent.off(this._container, 'MSPointerMove', this._onTouchMove, this); + L.DomEvent.off(this._container, 'MSPointerCancel', this._onTouchCancel, this); + } else { + L.DomEvent.off(this._container, 'touchcancel', this._onTouchCancel, this); + L.DomEvent.off(this._container, 'touchleave', this._onTouchLeave, this); + } + }, + + _touchEvent: function (e, type) { + // #TODO: fix the pageX error that is do a bug in Android where a single touch triggers two click events + // _filterClick is what leaflet uses as a workaround. + // This is a problem with more things than just android. Another problem is touchEnd has no touches in + // its touch list. + var touchEvent = {}; + if (typeof e.touches !== 'undefined') { + if (!e.touches.length) { + return; + } + touchEvent = e.touches[0]; + } else if (e.pointerType === 'touch') { + touchEvent = e; + if (!this._filterClick(e)) { + return; + } + } else { + return; + } + + var containerPoint = this._map.mouseEventToContainerPoint(touchEvent), + layerPoint = this._map.mouseEventToLayerPoint(touchEvent), + latlng = this._map.layerPointToLatLng(layerPoint); + + this._map.fire(type, { + latlng: latlng, + layerPoint: layerPoint, + containerPoint: containerPoint, + pageX: touchEvent.pageX, + pageY: touchEvent.pageY, + originalEvent: e + }); + }, + + /** Borrowed from Leaflet and modified for bool ops **/ + _filterClick: function (e) { + var timeStamp = (e.timeStamp || e.originalEvent.timeStamp), + elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick); + + // are they closer together than 500ms yet more than 100ms? + // Android typically triggers them ~300ms apart while multiple listeners + // on the same event should be triggered far faster; + // or check if click is simulated on the element, and if it is, reject any non-simulated events + if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) { + L.DomEvent.stop(e); + return false; + } + L.DomEvent._lastClick = timeStamp; + return true; + }, + + _onTouchStart: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchstart'; + this._touchEvent(e, type); + + }, + + _onTouchEnd: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchend'; + this._touchEvent(e, type); + }, + + _onTouchCancel: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchcancel'; + if (this._detectIE()) { + type = 'pointercancel'; + } + this._touchEvent(e, type); + }, + + _onTouchLeave: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchleave'; + this._touchEvent(e, type); + }, + + _onTouchMove: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchmove'; + this._touchEvent(e, type); + }, + + _detectIE: function () { + var ua = window.navigator.userAgent; + + var msie = ua.indexOf('MSIE '); + if (msie > 0) { + // IE 10 or older => return version number + return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); + } + + var trident = ua.indexOf('Trident/'); + if (trident > 0) { + // IE 11 => return version number + var rv = ua.indexOf('rv:'); + return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); + } + + var edge = ua.indexOf('Edge/'); + if (edge > 0) { + // IE 12 => return version number + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); + } + + // other browser + return false; + } +}); + +L.Map.addInitHook('addHandler', 'touchExtend', L.Map.TouchExtend); + + +/** + * @class L.Marker.Touch + * @aka Marker.Touch + * + * This isn't full Touch support. This is just to get markers to also support dom touch events after creation + * #TODO: find a better way of getting markers to support touch. + */ +L.Marker.Touch = L.Marker.extend({ + + _initInteraction: function () { + if (!this.addInteractiveTarget) { + // 0.7.x support + return this._initInteractionLegacy(); + } + // TODO this may need be updated to re-add touch events for 1.0+ + return L.Marker.prototype._initInteraction.apply(this); + }, + + // This is an exact copy of https://github.com/Leaflet/Leaflet/blob/v0.7/src/layer/marker/Marker.js + // with the addition of the touch events + _initInteractionLegacy: function () { + + if (!this.options.clickable) { + return; + } + + // TODO refactor into something shared with Map/Path/etc. to DRY it up + + var icon = this._icon, + events = ['dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + 'touchstart', + 'touchend', + 'touchmove']; + if (this._detectIE) { + events.concat(['MSPointerDown', + 'MSPointerUp', + 'MSPointerMove', + 'MSPointerCancel']); + } else { + events.concat(['touchcancel']); + } + + L.DomUtil.addClass(icon, 'leaflet-clickable'); + L.DomEvent.on(icon, 'click', this._onMouseClick, this); + L.DomEvent.on(icon, 'keypress', this._onKeyPress, this); + + for (var i = 0; i < events.length; i++) { + L.DomEvent.on(icon, events[i], this._fireMouseEvent, this); + } + + if (L.Handler.MarkerDrag) { + this.dragging = new L.Handler.MarkerDrag(this); + + if (this.options.draggable) { + this.dragging.enable(); + } + } + }, + + _detectIE: function () { + var ua = window.navigator.userAgent; + + var msie = ua.indexOf('MSIE '); + if (msie > 0) { + // IE 10 or older => return version number + return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); + } + + var trident = ua.indexOf('Trident/'); + if (trident > 0) { + // IE 11 => return version number + var rv = ua.indexOf('rv:'); + return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); + } + + var edge = ua.indexOf('Edge/'); + if (edge > 0) { + // IE 12 => return version number + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); + } + + // other browser + return false; + } +}); + + + +/** + * @class L.LatLngUtil + * @aka LatLngUtil + */ +L.LatLngUtil = { + // Clones a LatLngs[], returns [][] + + // @method cloneLatLngs(LatLngs[]): L.LatLngs[] + // Clone the latLng point or points or nested points and return an array with those points + cloneLatLngs: function (latlngs) { + var clone = []; + for (var i = 0, l = latlngs.length; i < l; i++) { + // Check for nested array (Polyline/Polygon) + if (Array.isArray(latlngs[i])) { + clone.push(L.LatLngUtil.cloneLatLngs(latlngs[i])); + } else { + clone.push(this.cloneLatLng(latlngs[i])); + } + } + return clone; + }, + + // @method cloneLatLng(LatLng): L.LatLng + // Clone the latLng and return a new LatLng object. + cloneLatLng: function (latlng) { + return L.latLng(latlng.lat, latlng.lng); + } +}; + + + +(function () { + + var defaultPrecision = { + km: 2, + ha: 2, + m: 0, + mi: 2, + ac: 2, + yd: 0, + ft: 0, + nm: 2 + }; + + + /** + * @class L.GeometryUtil + * @aka GeometryUtil + */ + L.GeometryUtil = L.extend(L.GeometryUtil || {}, { + // Ported from the OpenLayers implementation. See https://github.com/openlayers/openlayers/blob/master/lib/OpenLayers/Geometry/LinearRing.js#L270 + + // @method geodesicArea(): number + geodesicArea: function (latLngs) { + var pointsCount = latLngs.length, + area = 0.0, + d2r = Math.PI / 180, + p1, p2; + + if (pointsCount > 2) { + for (var i = 0; i < pointsCount; i++) { + p1 = latLngs[i]; + p2 = latLngs[(i + 1) % pointsCount]; + area += ((p2.lng - p1.lng) * d2r) * + (2 + Math.sin(p1.lat * d2r) + Math.sin(p2.lat * d2r)); + } + area = area * 6378137.0 * 6378137.0 / 2.0; + } + + return Math.abs(area); + }, + + // @method formattedNumber(n, precision): string + // Returns n in specified number format (if defined) and precision + formattedNumber: function (n, precision) { + var formatted = parseFloat(n).toFixed(precision), + format = L.drawLocal.format && L.drawLocal.format.numeric, + delimiters = format && format.delimiters, + thousands = delimiters && delimiters.thousands, + decimal = delimiters && delimiters.decimal; + + if (thousands || decimal) { + var splitValue = formatted.split('.'); + formatted = thousands ? splitValue[0].replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1' + thousands) : splitValue[0]; + decimal = decimal || '.'; + if (splitValue.length > 1) { + formatted = formatted + decimal + splitValue[1]; + } + } + + return formatted; + }, + + // @method readableArea(area, isMetric, precision): string + // Returns a readable area string in yards or metric. + // The value will be rounded as defined by the precision option object. + readableArea: function (area, isMetric, precision) { + var areaStr, + units, + precision = L.Util.extend({}, defaultPrecision, precision); + + if (isMetric) { + units = ['ha', 'm']; + type = typeof isMetric; + if (type === 'string') { + units = [isMetric]; + } else if (type !== 'boolean') { + units = isMetric; + } + + if (area >= 1000000 && units.indexOf('km') !== -1) { + areaStr = L.GeometryUtil.formattedNumber(area * 0.000001, precision['km']) + ' km²'; + } else if (area >= 10000 && units.indexOf('ha') !== -1) { + areaStr = L.GeometryUtil.formattedNumber(area * 0.0001, precision['ha']) + ' ha'; + } else { + areaStr = L.GeometryUtil.formattedNumber(area, precision['m']) + ' m²'; + } + } else { + area /= 0.836127; // Square yards in 1 meter + + if (area >= 3097600) { //3097600 square yards in 1 square mile + areaStr = L.GeometryUtil.formattedNumber(area / 3097600, precision['mi']) + ' mi²'; + } else if (area >= 4840) { //4840 square yards in 1 acre + areaStr = L.GeometryUtil.formattedNumber(area / 4840, precision['ac']) + ' acres'; + } else { + areaStr = L.GeometryUtil.formattedNumber(area, precision['yd']) + ' yd²'; + } + } + + return areaStr; + }, + + // @method readableDistance(distance, units): string + // Converts a metric distance to one of [ feet, nauticalMile, metric or yards ] string + // + // @alternative + // @method readableDistance(distance, isMetric, useFeet, isNauticalMile, precision): string + // Converts metric distance to distance string. + // The value will be rounded as defined by the precision option object. + readableDistance: function (distance, isMetric, isFeet, isNauticalMile, precision) { + var distanceStr, + units, + precision = L.Util.extend({}, defaultPrecision, precision); + + if (isMetric) { + units = typeof isMetric == 'string' ? isMetric : 'metric'; + } else if (isFeet) { + units = 'feet'; + } else if (isNauticalMile) { + units = 'nauticalMile'; + } else { + units = 'yards'; + } + + switch (units) { + case 'metric': + // show metres when distance is < 1km, then show km + if (distance > 1000) { + distanceStr = L.GeometryUtil.formattedNumber(distance / 1000, precision['km']) + ' km'; + } else { + distanceStr = L.GeometryUtil.formattedNumber(distance, precision['m']) + ' m'; + } + break; + case 'feet': + distance *= 1.09361 * 3; + distanceStr = L.GeometryUtil.formattedNumber(distance, precision['ft']) + ' ft'; + + break; + case 'nauticalMile': + distance *= 0.53996; + distanceStr = L.GeometryUtil.formattedNumber(distance / 1000, precision['nm']) + ' nm'; + break; + case 'yards': + default: + distance *= 1.09361; + + if (distance > 1760) { + distanceStr = L.GeometryUtil.formattedNumber(distance / 1760, precision['mi']) + ' miles'; + } else { + distanceStr = L.GeometryUtil.formattedNumber(distance, precision['yd']) + ' yd'; + } + break; + } + return distanceStr; + }, + + // @method isVersion07x(): boolean + // Returns true if the Leaflet version is 0.7.x, false otherwise. + isVersion07x: function () { + var version = L.version.split('.'); + //If Version is == 0.7.* + return parseInt(version[0], 10) === 0 && parseInt(version[1], 10) === 7; + }, + }); + +})(); + + + +/** + * @class L.LineUtil + * @aka Util + * @aka L.Utils + */ +L.Util.extend(L.LineUtil, { + + // @method segmentsIntersect(): boolean + // Checks to see if two line segments intersect. Does not handle degenerate cases. + // http://compgeom.cs.uiuc.edu/~jeffe/teaching/373/notes/x06-sweepline.pdf + segmentsIntersect: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2, /*Point*/ p3) { + return this._checkCounterclockwise(p, p2, p3) !== + this._checkCounterclockwise(p1, p2, p3) && + this._checkCounterclockwise(p, p1, p2) !== + this._checkCounterclockwise(p, p1, p3); + }, + + // check to see if points are in counterclockwise order + _checkCounterclockwise: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { + return (p2.y - p.y) * (p1.x - p.x) > (p1.y - p.y) * (p2.x - p.x); + } +}); + + + +/** + * @class L.Polyline + * @aka Polyline + */ +L.Polyline.include({ + + // @method intersects(): boolean + // Check to see if this polyline has any linesegments that intersect. + // NOTE: does not support detecting intersection for degenerate cases. + intersects: function () { + var points = this._getProjectedPoints(), + len = points ? points.length : 0, + i, p, p1; + + if (this._tooFewPointsForIntersection()) { + return false; + } + + for (i = len - 1; i >= 3; i--) { + p = points[i - 1]; + p1 = points[i]; + + + if (this._lineSegmentsIntersectsRange(p, p1, i - 2)) { + return true; + } + } + + return false; + }, + + // @method newLatLngIntersects(): boolean + // Check for intersection if new latlng was added to this polyline. + // NOTE: does not support detecting intersection for degenerate cases. + newLatLngIntersects: function (latlng, skipFirst) { + // Cannot check a polyline for intersecting lats/lngs when not added to the map + if (!this._map) { + return false; + } + + return this.newPointIntersects(this._map.latLngToLayerPoint(latlng), skipFirst); + }, + + // @method newPointIntersects(): boolean + // Check for intersection if new point was added to this polyline. + // newPoint must be a layer point. + // NOTE: does not support detecting intersection for degenerate cases. + newPointIntersects: function (newPoint, skipFirst) { + var points = this._getProjectedPoints(), + len = points ? points.length : 0, + lastPoint = points ? points[len - 1] : null, + // The previous previous line segment. Previous line segment doesn't need testing. + maxIndex = len - 2; + + if (this._tooFewPointsForIntersection(1)) { + return false; + } + + return this._lineSegmentsIntersectsRange(lastPoint, newPoint, maxIndex, skipFirst ? 1 : 0); + }, + + // Polylines with 2 sides can only intersect in cases where points are collinear (we don't support detecting these). + // Cannot have intersection when < 3 line segments (< 4 points) + _tooFewPointsForIntersection: function (extraPoints) { + var points = this._getProjectedPoints(), + len = points ? points.length : 0; + // Increment length by extraPoints if present + len += extraPoints || 0; + + return !points || len <= 3; + }, + + // Checks a line segment intersections with any line segments before its predecessor. + // Don't need to check the predecessor as will never intersect. + _lineSegmentsIntersectsRange: function (p, p1, maxIndex, minIndex) { + var points = this._getProjectedPoints(), + p2, p3; + + minIndex = minIndex || 0; + + // Check all previous line segments (beside the immediately previous) for intersections + for (var j = maxIndex; j > minIndex; j--) { + p2 = points[j - 1]; + p3 = points[j]; + + if (L.LineUtil.segmentsIntersect(p, p1, p2, p3)) { + return true; + } + } + + return false; + }, + + _getProjectedPoints: function () { + if (!this._defaultShape) { + return this._originalPoints; + } + var points = [], + _shape = this._defaultShape(); + + for (var i = 0; i < _shape.length; i++) { + points.push(this._map.latLngToLayerPoint(_shape[i])); + } + return points; + } +}); + + + +/** + * @class L.Polygon + * @aka Polygon + */ +L.Polygon.include({ + + // @method intersects(): boolean + // Checks a polygon for any intersecting line segments. Ignores holes. + intersects: function () { + var polylineIntersects, + points = this._getProjectedPoints(), + len, firstPoint, lastPoint, maxIndex; + + if (this._tooFewPointsForIntersection()) { + return false; + } + + polylineIntersects = L.Polyline.prototype.intersects.call(this); + + // If already found an intersection don't need to check for any more. + if (polylineIntersects) { + return true; + } + + len = points.length; + firstPoint = points[0]; + lastPoint = points[len - 1]; + maxIndex = len - 2; + + // Check the line segment between last and first point. Don't need to check the first line segment (minIndex = 1) + return this._lineSegmentsIntersectsRange(lastPoint, firstPoint, maxIndex, 1); + } +}); + + + +/** + * @class L.Control.Draw + * @aka L.Draw + */ +L.Control.Draw = L.Control.extend({ + + // Options + options: { + position: 'topleft', + draw: {}, + edit: false + }, + + // @method initialize(): void + // Initializes draw control, toolbars from the options + initialize: function (options) { + if (L.version < '0.7') { + throw new Error('Leaflet.draw 0.2.3+ requires Leaflet 0.7.0+. Download latest from https://github.com/Leaflet/Leaflet/'); + } + + L.Control.prototype.initialize.call(this, options); + + var toolbar; + + this._toolbars = {}; + + // Initialize toolbars + if (L.DrawToolbar && this.options.draw) { + toolbar = new L.DrawToolbar(this.options.draw); + + this._toolbars[L.DrawToolbar.TYPE] = toolbar; + + // Listen for when toolbar is enabled + this._toolbars[L.DrawToolbar.TYPE].on('enable', this._toolbarEnabled, this); + } + + if (L.EditToolbar && this.options.edit) { + toolbar = new L.EditToolbar(this.options.edit); + + this._toolbars[L.EditToolbar.TYPE] = toolbar; + + // Listen for when toolbar is enabled + this._toolbars[L.EditToolbar.TYPE].on('enable', this._toolbarEnabled, this); + } + L.toolbar = this; //set global var for editing the toolbar + }, + + // @method onAdd(): container + // Adds the toolbar container to the map + onAdd: function (map) { + var container = L.DomUtil.create('div', 'leaflet-draw'), + addedTopClass = false, + topClassName = 'leaflet-draw-toolbar-top', + toolbarContainer; + + for (var toolbarId in this._toolbars) { + if (this._toolbars.hasOwnProperty(toolbarId)) { + toolbarContainer = this._toolbars[toolbarId].addToolbar(map); + + if (toolbarContainer) { + // Add class to the first toolbar to remove the margin + if (!addedTopClass) { + if (!L.DomUtil.hasClass(toolbarContainer, topClassName)) { + L.DomUtil.addClass(toolbarContainer.childNodes[0], topClassName); + } + addedTopClass = true; + } + + container.appendChild(toolbarContainer); + } + } + } + + return container; + }, + + // @method onRemove(): void + // Removes the toolbars from the map toolbar container + onRemove: function () { + for (var toolbarId in this._toolbars) { + if (this._toolbars.hasOwnProperty(toolbarId)) { + this._toolbars[toolbarId].removeToolbar(); + } + } + }, + + // @method setDrawingOptions(options): void + // Sets options to all toolbar instances + setDrawingOptions: function (options) { + for (var toolbarId in this._toolbars) { + if (this._toolbars[toolbarId] instanceof L.DrawToolbar) { + this._toolbars[toolbarId].setOptions(options); + } + } + }, + + _toolbarEnabled: function (e) { + var enabledToolbar = e.target; + + for (var toolbarId in this._toolbars) { + if (this._toolbars[toolbarId] !== enabledToolbar) { + this._toolbars[toolbarId].disable(); + } + } + } +}); + +L.Map.mergeOptions({ + drawControlTooltips: true, + drawControl: false +}); + +L.Map.addInitHook(function () { + if (this.options.drawControl) { + this.drawControl = new L.Control.Draw(); + this.addControl(this.drawControl); + } +}); + + + +/** + * @class L.Draw.Toolbar + * @aka Toolbar + * + * The toolbar class of the API — it is used to create the ui + * This will be depreciated + * + * @example + * + * ```js + * var toolbar = L.Toolbar(); + * toolbar.addToolbar(map); + * ``` + * + * ### Disabling a toolbar + * + * If you do not want a particular toolbar in your app you can turn it off by setting the toolbar to false. + * + * ```js + * var drawControl = new L.Control.Draw({ + * draw: false, + * edit: { + * featureGroup: editableLayers + * } + * }); + * ``` + * + * ### Disabling a toolbar item + * + * If you want to turn off a particular toolbar item, set it to false. The following disables drawing polygons and + * markers. It also turns off the ability to edit layers. + * + * ```js + * var drawControl = new L.Control.Draw({ + * draw: { + * polygon: false, + * marker: false + * }, + * edit: { + * featureGroup: editableLayers, + * edit: false + * } + * }); + * ``` + */ +L.Toolbar = L.Class.extend({ + // @section Methods for modifying the toolbar + + // @method initialize(options): void + // Toolbar constructor + initialize: function (options) { + L.setOptions(this, options); + + this._modes = {}; + this._actionButtons = []; + this._activeMode = null; + + var version = L.version.split('.'); + //If Version is >= 1.2.0 + if (parseInt(version[0], 10) === 1 && parseInt(version[1], 10) >= 2) { + L.Toolbar.include(L.Evented.prototype); + } else { + L.Toolbar.include(L.Mixin.Events); + } + }, + + // @method enabled(): boolean + // Gets a true/false of whether the toolbar is enabled + enabled: function () { + return this._activeMode !== null; + }, + + // @method disable(): void + // Disables the toolbar + disable: function () { + if (!this.enabled()) { + return; + } + + this._activeMode.handler.disable(); + }, + + // @method addToolbar(map): L.DomUtil + // Adds the toolbar to the map and returns the toolbar dom element + addToolbar: function (map) { + var container = L.DomUtil.create('div', 'leaflet-draw-section'), + buttonIndex = 0, + buttonClassPrefix = this._toolbarClass || '', + modeHandlers = this.getModeHandlers(map), + i; + + this._toolbarContainer = L.DomUtil.create('div', 'leaflet-draw-toolbar leaflet-bar'); + this._map = map; + + for (i = 0; i < modeHandlers.length; i++) { + if (modeHandlers[i].enabled) { + this._initModeHandler( + modeHandlers[i].handler, + this._toolbarContainer, + buttonIndex++, + buttonClassPrefix, + modeHandlers[i].title + ); + } + } + + // if no buttons were added, do not add the toolbar + if (!buttonIndex) { + return; + } + + // Save button index of the last button, -1 as we would have ++ after the last button + this._lastButtonIndex = --buttonIndex; + + // Create empty actions part of the toolbar + this._actionsContainer = L.DomUtil.create('ul', 'leaflet-draw-actions'); + + // Add draw and cancel containers to the control container + container.appendChild(this._toolbarContainer); + container.appendChild(this._actionsContainer); + + return container; + }, + + // @method removeToolbar(): void + // Removes the toolbar and drops the handler event listeners + removeToolbar: function () { + // Dispose each handler + for (var handlerId in this._modes) { + if (this._modes.hasOwnProperty(handlerId)) { + // Unbind handler button + this._disposeButton( + this._modes[handlerId].button, + this._modes[handlerId].handler.enable, + this._modes[handlerId].handler + ); + + // Make sure is disabled + this._modes[handlerId].handler.disable(); + + // Unbind handler + this._modes[handlerId].handler + .off('enabled', this._handlerActivated, this) + .off('disabled', this._handlerDeactivated, this); + } + } + this._modes = {}; + + // Dispose the actions toolbar + for (var i = 0, l = this._actionButtons.length; i < l; i++) { + this._disposeButton( + this._actionButtons[i].button, + this._actionButtons[i].callback, + this + ); + } + this._actionButtons = []; + this._actionsContainer = null; + }, + + _initModeHandler: function (handler, container, buttonIndex, classNamePredix, buttonTitle) { + var type = handler.type; + + this._modes[type] = {}; + + this._modes[type].handler = handler; + + this._modes[type].button = this._createButton({ + type: type, + title: buttonTitle, + className: classNamePredix + '-' + type, + container: container, + callback: this._modes[type].handler.enable, + context: this._modes[type].handler + }); + + this._modes[type].buttonIndex = buttonIndex; + + this._modes[type].handler + .on('enabled', this._handlerActivated, this) + .on('disabled', this._handlerDeactivated, this); + }, + + /* Detect iOS based on browser User Agent, based on: + * http://stackoverflow.com/a/9039885 */ + _detectIOS: function () { + var iOS = (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream); + return iOS; + }, + + _createButton: function (options) { + + var link = L.DomUtil.create('a', options.className || '', options.container); + // Screen reader tag + var sr = L.DomUtil.create('span', 'sr-only', options.container); + + link.href = '#'; + link.appendChild(sr); + + if (options.title) { + link.title = options.title; + sr.innerHTML = options.title; + } + + if (options.text) { + link.innerHTML = options.text; + sr.innerHTML = options.text; + } + + /* iOS does not use click events */ + var buttonEvent = this._detectIOS() ? 'touchstart' : 'click'; + + L.DomEvent + .on(link, 'click', L.DomEvent.stopPropagation) + .on(link, 'mousedown', L.DomEvent.stopPropagation) + .on(link, 'dblclick', L.DomEvent.stopPropagation) + .on(link, 'touchstart', L.DomEvent.stopPropagation) + .on(link, 'click', L.DomEvent.preventDefault) + .on(link, buttonEvent, options.callback, options.context); + + return link; + }, + + _disposeButton: function (button, callback) { + /* iOS does not use click events */ + var buttonEvent = this._detectIOS() ? 'touchstart' : 'click'; + + L.DomEvent + .off(button, 'click', L.DomEvent.stopPropagation) + .off(button, 'mousedown', L.DomEvent.stopPropagation) + .off(button, 'dblclick', L.DomEvent.stopPropagation) + .off(button, 'touchstart', L.DomEvent.stopPropagation) + .off(button, 'click', L.DomEvent.preventDefault) + .off(button, buttonEvent, callback); + }, + + _handlerActivated: function (e) { + // Disable active mode (if present) + this.disable(); + + // Cache new active feature + this._activeMode = this._modes[e.handler]; + + L.DomUtil.addClass(this._activeMode.button, 'leaflet-draw-toolbar-button-enabled'); + + this._showActionsToolbar(); + + this.fire('enable'); + }, + + _handlerDeactivated: function () { + this._hideActionsToolbar(); + + L.DomUtil.removeClass(this._activeMode.button, 'leaflet-draw-toolbar-button-enabled'); + + this._activeMode = null; + + this.fire('disable'); + }, + + _createActions: function (handler) { + var container = this._actionsContainer, + buttons = this.getActions(handler), + l = buttons.length, + li, di, dl, button; + + // Dispose the actions toolbar (todo: dispose only not used buttons) + for (di = 0, dl = this._actionButtons.length; di < dl; di++) { + this._disposeButton(this._actionButtons[di].button, this._actionButtons[di].callback); + } + this._actionButtons = []; + + // Remove all old buttons + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + for (var i = 0; i < l; i++) { + if ('enabled' in buttons[i] && !buttons[i].enabled) { + continue; + } + + li = L.DomUtil.create('li', '', container); + + button = this._createButton({ + title: buttons[i].title, + text: buttons[i].text, + container: li, + callback: buttons[i].callback, + context: buttons[i].context + }); + + this._actionButtons.push({ + button: button, + callback: buttons[i].callback + }); + } + }, + + _showActionsToolbar: function () { + var buttonIndex = this._activeMode.buttonIndex, + lastButtonIndex = this._lastButtonIndex, + toolbarPosition = this._activeMode.button.offsetTop - 1; + + // Recreate action buttons on every click + this._createActions(this._activeMode.handler); + + // Correctly position the cancel button + this._actionsContainer.style.top = toolbarPosition + 'px'; + + if (buttonIndex === 0) { + L.DomUtil.addClass(this._toolbarContainer, 'leaflet-draw-toolbar-notop'); + L.DomUtil.addClass(this._actionsContainer, 'leaflet-draw-actions-top'); + } + + if (buttonIndex === lastButtonIndex) { + L.DomUtil.addClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); + L.DomUtil.addClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); + } + + this._actionsContainer.style.display = 'block'; + this._map.fire(L.Draw.Event.TOOLBAROPENED); + }, + + _hideActionsToolbar: function () { + this._actionsContainer.style.display = 'none'; + + L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-notop'); + L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); + L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-top'); + L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); + this._map.fire(L.Draw.Event.TOOLBARCLOSED); + } +}); + + + +L.Draw = L.Draw || {}; +/** + * @class L.Draw.Tooltip + * @aka Tooltip + * + * The tooltip class — it is used to display the tooltip while drawing + * This will be depreciated + * + * @example + * + * ```js + * var tooltip = L.Draw.Tooltip(); + * ``` + * + */ +L.Draw.Tooltip = L.Class.extend({ + + // @section Methods for modifying draw state + + // @method initialize(map): void + // Tooltip constructor + initialize: function (map) { + this._map = map; + this._popupPane = map._panes.popupPane; + this._visible = false; + + this._container = map.options.drawControlTooltips ? + L.DomUtil.create('div', 'leaflet-draw-tooltip', this._popupPane) : null; + this._singleLineLabel = false; + + this._map.on('mouseout', this._onMouseOut, this); + }, + + // @method dispose(): void + // Remove Tooltip DOM and unbind events + dispose: function () { + this._map.off('mouseout', this._onMouseOut, this); + + if (this._container) { + this._popupPane.removeChild(this._container); + this._container = null; + } + }, + + // @method updateContent(labelText): this + // Changes the tooltip text to string in function call + updateContent: function (labelText) { + if (!this._container) { + return this; + } + labelText.subtext = labelText.subtext || ''; + + // update the vertical position (only if changed) + if (labelText.subtext.length === 0 && !this._singleLineLabel) { + L.DomUtil.addClass(this._container, 'leaflet-draw-tooltip-single'); + this._singleLineLabel = true; + } + else if (labelText.subtext.length > 0 && this._singleLineLabel) { + L.DomUtil.removeClass(this._container, 'leaflet-draw-tooltip-single'); + this._singleLineLabel = false; + } + + this._container.innerHTML = + (labelText.subtext.length > 0 ? + '' + labelText.subtext + '' + '
    ' : '') + + '' + labelText.text + ''; + + if (!labelText.text && !labelText.subtext) { + this._visible = false; + this._container.style.visibility = 'hidden'; + } else { + this._visible = true; + this._container.style.visibility = 'inherit'; + } + + return this; + }, + + // @method updatePosition(latlng): this + // Changes the location of the tooltip + updatePosition: function (latlng) { + var pos = this._map.latLngToLayerPoint(latlng), + tooltipContainer = this._container; + + if (this._container) { + if (this._visible) { + tooltipContainer.style.visibility = 'inherit'; + } + L.DomUtil.setPosition(tooltipContainer, pos); + } + + return this; + }, + + // @method showAsError(): this + // Applies error class to tooltip + showAsError: function () { + if (this._container) { + L.DomUtil.addClass(this._container, 'leaflet-error-draw-tooltip'); + } + return this; + }, + + // @method removeError(): this + // Removes the error class from the tooltip + removeError: function () { + if (this._container) { + L.DomUtil.removeClass(this._container, 'leaflet-error-draw-tooltip'); + } + return this; + }, + + _onMouseOut: function () { + if (this._container) { + this._container.style.visibility = 'hidden'; + } + } +}); + + + +/** + * @class L.DrawToolbar + * @aka Toolbar + */ +L.DrawToolbar = L.Toolbar.extend({ + + statics: { + TYPE: 'draw' + }, + + options: { + polyline: {}, + polygon: {}, + rectangle: {}, + circle: {}, + marker: {}, + circlemarker: {} + }, + + // @method initialize(): void + initialize: function (options) { + // Ensure that the options are merged correctly since L.extend is only shallow + for (var type in this.options) { + if (this.options.hasOwnProperty(type)) { + if (options[type]) { + options[type] = L.extend({}, this.options[type], options[type]); + } + } + } + + this._toolbarClass = 'leaflet-draw-draw'; + L.Toolbar.prototype.initialize.call(this, options); + }, + + // @method getModeHandlers(): object + // Get mode handlers information + getModeHandlers: function (map) { + return [ + { + enabled: this.options.polyline, + handler: new L.Draw.Polyline(map, this.options.polyline), + title: L.drawLocal.draw.toolbar.buttons.polyline + }, + { + enabled: this.options.polygon, + handler: new L.Draw.Polygon(map, this.options.polygon), + title: L.drawLocal.draw.toolbar.buttons.polygon + }, + { + enabled: this.options.rectangle, + handler: new L.Draw.Rectangle(map, this.options.rectangle), + title: L.drawLocal.draw.toolbar.buttons.rectangle + }, + { + enabled: this.options.circle, + handler: new L.Draw.Circle(map, this.options.circle), + title: L.drawLocal.draw.toolbar.buttons.circle + }, + { + enabled: this.options.marker, + handler: new L.Draw.Marker(map, this.options.marker), + title: L.drawLocal.draw.toolbar.buttons.marker + }, + { + enabled: this.options.circlemarker, + handler: new L.Draw.CircleMarker(map, this.options.circlemarker), + title: L.drawLocal.draw.toolbar.buttons.circlemarker + } + ]; + }, + + // @method getActions(): object + // Get action information + getActions: function (handler) { + return [ + { + enabled: handler.completeShape, + title: L.drawLocal.draw.toolbar.finish.title, + text: L.drawLocal.draw.toolbar.finish.text, + callback: handler.completeShape, + context: handler + }, + { + enabled: handler.deleteLastVertex, + title: L.drawLocal.draw.toolbar.undo.title, + text: L.drawLocal.draw.toolbar.undo.text, + callback: handler.deleteLastVertex, + context: handler + }, + { + title: L.drawLocal.draw.toolbar.actions.title, + text: L.drawLocal.draw.toolbar.actions.text, + callback: this.disable, + context: this + } + ]; + }, + + // @method setOptions(): void + // Sets the options to the toolbar + setOptions: function (options) { + L.setOptions(this, options); + + for (var type in this._modes) { + if (this._modes.hasOwnProperty(type) && options.hasOwnProperty(type)) { + this._modes[type].handler.setOptions(options[type]); + } + } + } +}); + + + +/*L.Map.mergeOptions({ + editControl: true + });*/ +/** + * @class L.EditToolbar + * @aka EditToolbar + */ +L.EditToolbar = L.Toolbar.extend({ + statics: { + TYPE: 'edit' + }, + + options: { + edit: { + selectedPathOptions: { + dashArray: '10, 10', + + fill: true, + fillColor: '#fe57a1', + fillOpacity: 0.1, + + // Whether to user the existing layers color + maintainColor: false + } + }, + remove: {}, + poly: null, + featureGroup: null /* REQUIRED! TODO: perhaps if not set then all layers on the map are selectable? */ + }, + + // @method intialize(): void + initialize: function (options) { + // Need to set this manually since null is an acceptable value here + if (options.edit) { + if (typeof options.edit.selectedPathOptions === 'undefined') { + options.edit.selectedPathOptions = this.options.edit.selectedPathOptions; + } + options.edit.selectedPathOptions = L.extend({}, this.options.edit.selectedPathOptions, options.edit.selectedPathOptions); + } + + if (options.remove) { + options.remove = L.extend({}, this.options.remove, options.remove); + } + + if (options.poly) { + options.poly = L.extend({}, this.options.poly, options.poly); + } + + this._toolbarClass = 'leaflet-draw-edit'; + L.Toolbar.prototype.initialize.call(this, options); + + this._selectedFeatureCount = 0; + }, + + // @method getModeHandlers(): object + // Get mode handlers information + getModeHandlers: function (map) { + var featureGroup = this.options.featureGroup; + return [ + { + enabled: this.options.edit, + handler: new L.EditToolbar.Edit(map, { + featureGroup: featureGroup, + selectedPathOptions: this.options.edit.selectedPathOptions, + poly: this.options.poly + }), + title: L.drawLocal.edit.toolbar.buttons.edit + }, + { + enabled: this.options.remove, + handler: new L.EditToolbar.Delete(map, { + featureGroup: featureGroup + }), + title: L.drawLocal.edit.toolbar.buttons.remove + } + ]; + }, + + // @method getActions(): object + // Get actions information + getActions: function (handler) { + var actions = [ + { + title: L.drawLocal.edit.toolbar.actions.save.title, + text: L.drawLocal.edit.toolbar.actions.save.text, + callback: this._save, + context: this + }, + { + title: L.drawLocal.edit.toolbar.actions.cancel.title, + text: L.drawLocal.edit.toolbar.actions.cancel.text, + callback: this.disable, + context: this + } + ]; + + if (handler.removeAllLayers) { + actions.push({ + title: L.drawLocal.edit.toolbar.actions.clearAll.title, + text: L.drawLocal.edit.toolbar.actions.clearAll.text, + callback: this._clearAllLayers, + context: this + }); + } + + return actions; + }, + + // @method addToolbar(map): L.DomUtil + // Adds the toolbar to the map + addToolbar: function (map) { + var container = L.Toolbar.prototype.addToolbar.call(this, map); + + this._checkDisabled(); + + this.options.featureGroup.on('layeradd layerremove', this._checkDisabled, this); + + return container; + }, + + // @method removeToolbar(): void + // Removes the toolbar from the map + removeToolbar: function () { + this.options.featureGroup.off('layeradd layerremove', this._checkDisabled, this); + + L.Toolbar.prototype.removeToolbar.call(this); + }, + + // @method disable(): void + // Disables the toolbar + disable: function () { + if (!this.enabled()) { + return; + } + + this._activeMode.handler.revertLayers(); + + L.Toolbar.prototype.disable.call(this); + }, + + _save: function () { + this._activeMode.handler.save(); + if (this._activeMode) { + this._activeMode.handler.disable(); + } + }, + + _clearAllLayers: function () { + this._activeMode.handler.removeAllLayers(); + if (this._activeMode) { + this._activeMode.handler.disable(); + } + }, + + _checkDisabled: function () { + var featureGroup = this.options.featureGroup, + hasLayers = featureGroup.getLayers().length !== 0, + button; + + if (this.options.edit) { + button = this._modes[L.EditToolbar.Edit.TYPE].button; + + if (hasLayers) { + L.DomUtil.removeClass(button, 'leaflet-disabled'); + } else { + L.DomUtil.addClass(button, 'leaflet-disabled'); + } + + button.setAttribute( + 'title', + hasLayers ? + L.drawLocal.edit.toolbar.buttons.edit + : L.drawLocal.edit.toolbar.buttons.editDisabled + ); + } + + if (this.options.remove) { + button = this._modes[L.EditToolbar.Delete.TYPE].button; + + if (hasLayers) { + L.DomUtil.removeClass(button, 'leaflet-disabled'); + } else { + L.DomUtil.addClass(button, 'leaflet-disabled'); + } + + button.setAttribute( + 'title', + hasLayers ? + L.drawLocal.edit.toolbar.buttons.remove + : L.drawLocal.edit.toolbar.buttons.removeDisabled + ); + } + } +}); + + + +/** + * @class L.EditToolbar.Edit + * @aka EditToolbar.Edit + */ +L.EditToolbar.Edit = L.Handler.extend({ + statics: { + TYPE: 'edit' + }, + + // @method intialize(): void + initialize: function (map, options) { + L.Handler.prototype.initialize.call(this, map); + + L.setOptions(this, options); + + // Store the selectable layer group for ease of access + this._featureGroup = options.featureGroup; + + if (!(this._featureGroup instanceof L.FeatureGroup)) { + throw new Error('options.featureGroup must be a L.FeatureGroup'); + } + + this._uneditedLayerProps = {}; + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.EditToolbar.Edit.TYPE; + + var version = L.version.split('.'); + //If Version is >= 1.2.0 + if (parseInt(version[0], 10) === 1 && parseInt(version[1], 10) >= 2) { + L.EditToolbar.Edit.include(L.Evented.prototype); + } else { + L.EditToolbar.Edit.include(L.Mixin.Events); + } + }, + + // @method enable(): void + // Enable the edit toolbar + enable: function () { + if (this._enabled || !this._hasAvailableLayers()) { + return; + } + this.fire('enabled', {handler: this.type}); + //this disable other handlers + + this._map.fire(L.Draw.Event.EDITSTART, {handler: this.type}); + //allow drawLayer to be updated before beginning edition. + + L.Handler.prototype.enable.call(this); + this._featureGroup + .on('layeradd', this._enableLayerEdit, this) + .on('layerremove', this._disableLayerEdit, this); + }, + + // @method disable(): void + // Disable the edit toolbar + disable: function () { + if (!this._enabled) { + return; + } + this._featureGroup + .off('layeradd', this._enableLayerEdit, this) + .off('layerremove', this._disableLayerEdit, this); + L.Handler.prototype.disable.call(this); + this._map.fire(L.Draw.Event.EDITSTOP, {handler: this.type}); + this.fire('disabled', {handler: this.type}); + }, + + // @method addHooks(): void + // Add listener hooks for this handler + addHooks: function () { + var map = this._map; + + if (map) { + map.getContainer().focus(); + + this._featureGroup.eachLayer(this._enableLayerEdit, this); + + this._tooltip = new L.Draw.Tooltip(this._map); + this._tooltip.updateContent({ + text: L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext + }); + + // Quickly access the tooltip to update for intersection checking + map._editTooltip = this._tooltip; + + this._updateTooltip(); + + this._map + .on('mousemove', this._onMouseMove, this) + .on('touchmove', this._onMouseMove, this) + .on('MSPointerMove', this._onMouseMove, this) + .on(L.Draw.Event.EDITVERTEX, this._updateTooltip, this); + } + }, + + // @method removeHooks(): void + // Remove listener hooks for this handler + removeHooks: function () { + if (this._map) { + // Clean up selected layers. + this._featureGroup.eachLayer(this._disableLayerEdit, this); + + // Clear the backups of the original layers + this._uneditedLayerProps = {}; + + this._tooltip.dispose(); + this._tooltip = null; + + this._map + .off('mousemove', this._onMouseMove, this) + .off('touchmove', this._onMouseMove, this) + .off('MSPointerMove', this._onMouseMove, this) + .off(L.Draw.Event.EDITVERTEX, this._updateTooltip, this); + } + }, + + // @method revertLayers(): void + // Revert each layer's geometry changes + revertLayers: function () { + this._featureGroup.eachLayer(function (layer) { + this._revertLayer(layer); + }, this); + }, + + // @method save(): void + // Save the layer geometries + save: function () { + var editedLayers = new L.LayerGroup(); + this._featureGroup.eachLayer(function (layer) { + if (layer.edited) { + editedLayers.addLayer(layer); + layer.edited = false; + } + }); + this._map.fire(L.Draw.Event.EDITED, {layers: editedLayers}); + }, + + _backupLayer: function (layer) { + var id = L.Util.stamp(layer); + + if (!this._uneditedLayerProps[id]) { + // Polyline, Polygon or Rectangle + if (layer instanceof L.Polyline || layer instanceof L.Polygon || layer instanceof L.Rectangle) { + this._uneditedLayerProps[id] = { + latlngs: L.LatLngUtil.cloneLatLngs(layer.getLatLngs()) + }; + } else if (layer instanceof L.Circle) { + this._uneditedLayerProps[id] = { + latlng: L.LatLngUtil.cloneLatLng(layer.getLatLng()), + radius: layer.getRadius() + }; + } else if (layer instanceof L.Marker || layer instanceof L.CircleMarker) { // Marker + this._uneditedLayerProps[id] = { + latlng: L.LatLngUtil.cloneLatLng(layer.getLatLng()) + }; + } + } + }, + + _getTooltipText: function () { + return ({ + text: L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext + }); + }, + + _updateTooltip: function () { + this._tooltip.updateContent(this._getTooltipText()); + }, + + _revertLayer: function (layer) { + var id = L.Util.stamp(layer); + layer.edited = false; + if (this._uneditedLayerProps.hasOwnProperty(id)) { + // Polyline, Polygon or Rectangle + if (layer instanceof L.Polyline || layer instanceof L.Polygon || layer instanceof L.Rectangle) { + layer.setLatLngs(this._uneditedLayerProps[id].latlngs); + } else if (layer instanceof L.Circle) { + layer.setLatLng(this._uneditedLayerProps[id].latlng); + layer.setRadius(this._uneditedLayerProps[id].radius); + } else if (layer instanceof L.Marker || layer instanceof L.CircleMarker) { // Marker or CircleMarker + layer.setLatLng(this._uneditedLayerProps[id].latlng); + } + + layer.fire('revert-edited', {layer: layer}); + } + }, + + _enableLayerEdit: function (e) { + var layer = e.layer || e.target || e, + pathOptions, poly; + + // Back up this layer (if haven't before) + this._backupLayer(layer); + + if (this.options.poly) { + poly = L.Util.extend({}, this.options.poly); + layer.options.poly = poly; + } + + // Set different style for editing mode + if (this.options.selectedPathOptions) { + pathOptions = L.Util.extend({}, this.options.selectedPathOptions); + + // Use the existing color of the layer + if (pathOptions.maintainColor) { + pathOptions.color = layer.options.color; + pathOptions.fillColor = layer.options.fillColor; + } + + layer.options.original = L.extend({}, layer.options); + layer.options.editing = pathOptions; + + } + + if (layer instanceof L.Marker) { + if (layer.editing) { + layer.editing.enable(); + } + layer.dragging.enable(); + layer + .on('dragend', this._onMarkerDragEnd) + // #TODO: remove when leaflet finally fixes their draggable so it's touch friendly again. + .on('touchmove', this._onTouchMove, this) + .on('MSPointerMove', this._onTouchMove, this) + .on('touchend', this._onMarkerDragEnd, this) + .on('MSPointerUp', this._onMarkerDragEnd, this); + } else { + layer.editing.enable(); + } + }, + + _disableLayerEdit: function (e) { + var layer = e.layer || e.target || e; + + layer.edited = false; + if (layer.editing) { + layer.editing.disable(); + } + + delete layer.options.editing; + delete layer.options.original; + // Reset layer styles to that of before select + if (this._selectedPathOptions) { + if (layer instanceof L.Marker) { + this._toggleMarkerHighlight(layer); + } else { + // reset the layer style to what is was before being selected + layer.setStyle(layer.options.previousOptions); + // remove the cached options for the layer object + delete layer.options.previousOptions; + } + } + + if (layer instanceof L.Marker) { + layer.dragging.disable(); + layer + .off('dragend', this._onMarkerDragEnd, this) + .off('touchmove', this._onTouchMove, this) + .off('MSPointerMove', this._onTouchMove, this) + .off('touchend', this._onMarkerDragEnd, this) + .off('MSPointerUp', this._onMarkerDragEnd, this); + } else { + layer.editing.disable(); + } + }, + + _onMouseMove: function (e) { + this._tooltip.updatePosition(e.latlng); + }, + + _onMarkerDragEnd: function (e) { + var layer = e.target; + layer.edited = true; + this._map.fire(L.Draw.Event.EDITMOVE, {layer: layer}); + }, + + _onTouchMove: function (e) { + var touchEvent = e.originalEvent.changedTouches[0], + layerPoint = this._map.mouseEventToLayerPoint(touchEvent), + latlng = this._map.layerPointToLatLng(layerPoint); + e.target.setLatLng(latlng); + }, + + _hasAvailableLayers: function () { + return this._featureGroup.getLayers().length !== 0; + } +}); + + + +/** + * @class L.EditToolbar.Delete + * @aka EditToolbar.Delete + */ +L.EditToolbar.Delete = L.Handler.extend({ + statics: { + TYPE: 'remove' // not delete as delete is reserved in js + }, + + // @method intialize(): void + initialize: function (map, options) { + L.Handler.prototype.initialize.call(this, map); + + L.Util.setOptions(this, options); + + // Store the selectable layer group for ease of access + this._deletableLayers = this.options.featureGroup; + + if (!(this._deletableLayers instanceof L.FeatureGroup)) { + throw new Error('options.featureGroup must be a L.FeatureGroup'); + } + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.EditToolbar.Delete.TYPE; + + var version = L.version.split('.'); + //If Version is >= 1.2.0 + if (parseInt(version[0], 10) === 1 && parseInt(version[1], 10) >= 2) { + L.EditToolbar.Delete.include(L.Evented.prototype); + } else { + L.EditToolbar.Delete.include(L.Mixin.Events); + } + + }, + + // @method enable(): void + // Enable the delete toolbar + enable: function () { + if (this._enabled || !this._hasAvailableLayers()) { + return; + } + this.fire('enabled', {handler: this.type}); + + this._map.fire(L.Draw.Event.DELETESTART, {handler: this.type}); + + L.Handler.prototype.enable.call(this); + + this._deletableLayers + .on('layeradd', this._enableLayerDelete, this) + .on('layerremove', this._disableLayerDelete, this); + }, + + // @method disable(): void + // Disable the delete toolbar + disable: function () { + if (!this._enabled) { + return; + } + + this._deletableLayers + .off('layeradd', this._enableLayerDelete, this) + .off('layerremove', this._disableLayerDelete, this); + + L.Handler.prototype.disable.call(this); + + this._map.fire(L.Draw.Event.DELETESTOP, {handler: this.type}); + + this.fire('disabled', {handler: this.type}); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + var map = this._map; + + if (map) { + map.getContainer().focus(); + + this._deletableLayers.eachLayer(this._enableLayerDelete, this); + this._deletedLayers = new L.LayerGroup(); + + this._tooltip = new L.Draw.Tooltip(this._map); + this._tooltip.updateContent({text: L.drawLocal.edit.handlers.remove.tooltip.text}); + + this._map.on('mousemove', this._onMouseMove, this); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + if (this._map) { + this._deletableLayers.eachLayer(this._disableLayerDelete, this); + this._deletedLayers = null; + + this._tooltip.dispose(); + this._tooltip = null; + + this._map.off('mousemove', this._onMouseMove, this); + } + }, + + // @method revertLayers(): void + // Revert the deleted layers back to their prior state. + revertLayers: function () { + // Iterate of the deleted layers and add them back into the featureGroup + this._deletedLayers.eachLayer(function (layer) { + this._deletableLayers.addLayer(layer); + layer.fire('revert-deleted', {layer: layer}); + }, this); + }, + + // @method save(): void + // Save deleted layers + save: function () { + this._map.fire(L.Draw.Event.DELETED, {layers: this._deletedLayers}); + }, + + // @method removeAllLayers(): void + // Remove all delateable layers + removeAllLayers: function () { + // Iterate of the delateable layers and add remove them + this._deletableLayers.eachLayer(function (layer) { + this._removeLayer({layer: layer}); + }, this); + this.save(); + }, + + _enableLayerDelete: function (e) { + var layer = e.layer || e.target || e; + + layer.on('click', this._removeLayer, this); + }, + + _disableLayerDelete: function (e) { + var layer = e.layer || e.target || e; + + layer.off('click', this._removeLayer, this); + + // Remove from the deleted layers so we can't accidentally revert if the user presses cancel + this._deletedLayers.removeLayer(layer); + }, + + _removeLayer: function (e) { + var layer = e.layer || e.target || e; + + this._deletableLayers.removeLayer(layer); + + this._deletedLayers.addLayer(layer); + + layer.fire('deleted'); + }, + + _onMouseMove: function (e) { + this._tooltip.updatePosition(e.latlng); + }, + + _hasAvailableLayers: function () { + return this._deletableLayers.getLayers().length !== 0; + } +}); + + + +}(window, document)); +//# sourceMappingURL=leaflet.draw-src.map + +; + $('' + ); +}; + +setup.info = plugin_info; //add the script info data to the function as a property +if (typeof changelog !== 'undefined') setup.info.changelog = changelog; +if(!window.bootPlugins) window.bootPlugins = []; +window.bootPlugins.push(setup); +// if IITC has already booted, immediately run the 'setup' function +if(window.iitcLoaded && typeof setup === 'function') setup(); +} // wrapper end +// inject code into site context +var script = document.createElement('script'); +var info = {}; +if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) info.script = { version: GM_info.script.version, name: GM_info.script.name, description: GM_info.script.description }; +script.appendChild(document.createTextNode('('+ wrapper +')('+JSON.stringify(info)+');')); +(document.body || document.head || document.documentElement).appendChild(script); + diff --git a/static/build/artifact/PR790/plugins/portal-level-numbers.meta.js b/static/build/artifact/PR790/plugins/portal-level-numbers.meta.js new file mode 100644 index 0000000000..4d3029f8f1 --- /dev/null +++ b/static/build/artifact/PR790/plugins/portal-level-numbers.meta.js @@ -0,0 +1,15 @@ +// ==UserScript== +// @author rongou +// @name IITC plugin: Portal Level Numbers +// @category Layer +// @version 0.2.4.20241216.092245 +// @description Show portal level numbers on map. +// @id portal-level-numbers +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/portal-level-numbers.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/portal-level-numbers.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/portal-level-numbers.svg +// @grant none +// ==/UserScript== diff --git a/static/build/artifact/PR790/plugins/portal-level-numbers.user.js b/static/build/artifact/PR790/plugins/portal-level-numbers.user.js new file mode 100644 index 0000000000..bae9ced024 --- /dev/null +++ b/static/build/artifact/PR790/plugins/portal-level-numbers.user.js @@ -0,0 +1,227 @@ +// ==UserScript== +// @author rongou +// @name IITC plugin: Portal Level Numbers +// @category Layer +// @version 0.2.4.20241216.092245 +// @description Show portal level numbers on map. +// @id portal-level-numbers +// @namespace https://github.com/IITC-CE/ingress-intel-total-conversion +// @updateURL https://iitc.app/build/artifact/PR790/plugins/portal-level-numbers.meta.js +// @downloadURL https://iitc.app/build/artifact/PR790/plugins/portal-level-numbers.user.js +// @match https://intel.ingress.com/* +// @match https://intel-x.ingress.com/* +// @icon https://iitc.app/extras/plugin-icons/portal-level-numbers.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 = 'portal-level-numbers'; +//END PLUGIN AUTHORS NOTE + +/* exported setup, changelog --eslint */ +/* global L -- eslint */ + +var changelog = [ + { + version: '0.2.4', + changes: ['Refactoring: fix eslint'], + }, + { + version: '0.2.3', + changes: ['Version upgrade due to a change in the wrapper: plugin icons are now vectorized'], + }, + { + version: '0.2.2', + changes: ['Version upgrade due to a change in the wrapper: added plugin icon'], + }, +]; + +// use own namespace for plugin +window.plugin.portalLevelNumbers = function () {}; + +window.plugin.portalLevelNumbers.ICON_SIZE = 12; +window.plugin.portalLevelNumbers.MOBILE_SCALE = 1.5; + +window.plugin.portalLevelNumbers.levelLayers = {}; +window.plugin.portalLevelNumbers.levelLayerGroup = null; + +window.plugin.portalLevelNumbers.setupCSS = function () { + $('' + + '' + + '' + + // note: smartphone.css injection moved into code/smartphone.js + ''; + +// remove body element entirely to remove event listeners +document.body = document.createElement('body'); +document.body.innerHTML = + '
    Loading, please wait
    ' + + '' + + '' + + '' + + '' + + '
    ' + // enable scrolling for small screens + ' ' + + '
    ' + + '
    ' + + // avoid error by stock JS + '
    ' + + ''; + +/* ****************************************************************************************************************** */ + +/** + * 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($('