diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0e1c523
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+build
+**/.vscode
+**/__pycache__
+**/.DS_Store
diff --git a/.gitmodules b/.gitmodules
index c7c3356..017d56f 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,9 @@
[submodule "gps"]
path = gps
url = ../gps/
+[submodule "YOLO"]
+ path = YOLO
+ url = https://github.com/Sooner-Rover-Team/YOLO
+[submodule "src/autonomous/gps"]
+ path = src/autonomous/gps
+ url = https://github.com/Sooner-Rover-Team/gps
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..12398d7
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+cpython@3.11.3
diff --git a/Test.txt b/Notes/Test.txt
similarity index 100%
rename from Test.txt
rename to Notes/Test.txt
diff --git a/testDayChecklist.txt b/Notes/testDayChecklist.txt
similarity index 100%
rename from testDayChecklist.txt
rename to Notes/testDayChecklist.txt
diff --git a/YOLO b/YOLO
new file mode 160000
index 0000000..453f31d
--- /dev/null
+++ b/YOLO
@@ -0,0 +1 @@
+Subproject commit 453f31d6123fca20c8a80d1e0808dd589bfe7f59
diff --git a/examples/ARTrackerTest/config.ini b/examples/ARTrackerTest/config.ini
new file mode 100644
index 0000000..8b887c2
--- /dev/null
+++ b/examples/ARTrackerTest/config.ini
@@ -0,0 +1,26 @@
+[CONFIG]
+SWIFT_IP=10.0.0.222
+SWIFT_PORT=55556
+MBED_IP=10.0.0.101
+MBED_PORT=1001
+[ARTRACKER]
+#dpp is .040625 with logi
+DEGREES_PER_PIXEL=0.09375
+VDEGREES_PER_PIXEL = .125
+#Focal length was 1500 with logi
+FOCAL_LENGTH=435
+FOCAL_LENGTH30H=590
+FOCAL_LENGTH30V=470
+KNOWN_TAG_WIDTH=20
+FORMAT=XVID
+FRAME_WIDTH=1280
+FRAME_HEIGHT=720
+MAIN_CAMERA=2.3
+LEFT_CAMERA=2.4
+RIGHT_CAMERA=2.2
+[YOLO]
+#I am assuming that these are in the darknet folder
+WEIGHTS=soro.weights
+DATA=cfg/soro.data
+CFG=cfg/soro.cfg
+THRESHOLD=.25
diff --git a/examples/ARTrackerTest/newAr.py b/examples/ARTrackerTest/newAr.py
new file mode 100644
index 0000000..ee8c32d
--- /dev/null
+++ b/examples/ARTrackerTest/newAr.py
@@ -0,0 +1,84 @@
+import cv2
+import cv2.aruco as aruco
+import numpy as np
+import configparser
+import os
+
+
+def preprocess_image(image):
+ # Convert the image to grayscale
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+
+ # Enhance contrast using histogram equalization
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
+ equalized = clahe.apply(gray)
+
+ # Apply Gaussian blur to reduce noise
+ blurred = cv2.GaussianBlur(equalized, (5, 5), 0)
+
+ # Apply adaptive thresholding to segment the image
+ print(type(blurred))
+ thresholded = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 4)
+
+ return thresholded
+
+def configCam(cam, configFile):
+ # Open the config file
+ config = configparser.ConfigParser(allow_no_value=True)
+ if not config.read(configFile):
+ print(f"ERROR OPENING AR CONFIG:", end="")
+ if os.path.isabs(configFile):
+ print(configFile)
+ else:
+ print("{os.getcwd()}/{configFile}")
+ exit(-2)
+
+ # Set variables from the config file
+ degreesPerPixel = float(config['ARTRACKER']['DEGREES_PER_PIXEL'])
+ vDegreesPerPixel = float(config['ARTRACKER']['VDEGREES_PER_PIXEL'])
+ focalLength = float(config['ARTRACKER']['FOCAL_LENGTH'])
+ focalLength30H = float(config['ARTRACKER']['FOCAL_LENGTH30H'])
+ focalLength30V = float(config['ARTRACKER']['FOCAL_LENGTH30V'])
+ knownMarkerWidth = float(config['ARTRACKER']['KNOWN_TAG_WIDTH'])
+ format = config['ARTRACKER']['FORMAT']
+ frameWidth = int(config['ARTRACKER']['FRAME_WIDTH'])
+ frameHeight = int(config['ARTRACKER']['FRAME_HEIGHT'])
+
+ # Set the camera properties
+ cam.set(cv2.CAP_PROP_FRAME_HEIGHT, frameHeight)
+ cam.set(cv2.CAP_PROP_FRAME_WIDTH, frameWidth)
+ cam.set(cv2.CAP_PROP_BUFFERSIZE, 1) # greatly speeds up the program but the writer is a bit wack because of this
+ cam.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(format[0], format[1], format[2], format[3]))
+
+if __name__ == "__main__":
+ # Create the camera object
+ cam = cv2.VideoCapture(1)
+ configCam(cam, "config.ini")
+
+ # Create the aruco dictionary
+ markerDict = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
+
+ while True:
+ # Read the image from the camera and convert to grayscale
+ ret, frame = cam.read()
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+
+ # Detect the markers
+ print(type(gray))
+ adapted_thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 3)
+
+ # Display the image
+ cv2.imshow('adapted thresh', adapted_thresh)
+ cv2.waitKey(1)
+ print(adapted_thresh[:][0])
+ (corners, markerIDs, rejected) = aruco.detectMarkers(adapted_thresh, markerDict)
+ if len(corners) == 1 and markerIDs[0] == 1:
+ print(markerIDs)
+ else:
+ print("No markers found")
+ cv2.imshow('adapted thresh', adapted_thresh)
+ cv2.waitKey(1)
+
+ if cv2.waitKey(1) & 0xFF == ord('q'):
+ break
+
diff --git a/examples/ARTrackerTest/pictures/10m.jpg b/examples/ARTrackerTest/pictures/10m.jpg
new file mode 100644
index 0000000..8fbd945
Binary files /dev/null and b/examples/ARTrackerTest/pictures/10m.jpg differ
diff --git a/examples/ARTrackerTest/pictures/15m.jpg b/examples/ARTrackerTest/pictures/15m.jpg
new file mode 100644
index 0000000..6c91384
Binary files /dev/null and b/examples/ARTrackerTest/pictures/15m.jpg differ
diff --git a/examples/ARTrackerTest/pictures/5m.jpg b/examples/ARTrackerTest/pictures/5m.jpg
new file mode 100644
index 0000000..92edcc1
Binary files /dev/null and b/examples/ARTrackerTest/pictures/5m.jpg differ
diff --git a/examples/ARTrackerTest/testARpic.py b/examples/ARTrackerTest/testARpic.py
new file mode 100644
index 0000000..b220ea8
--- /dev/null
+++ b/examples/ARTrackerTest/testARpic.py
@@ -0,0 +1,41 @@
+import cv2
+import cv2.aruco as aruco
+import argparse
+
+# Parse the command line arguments
+parser = argparse.ArgumentParser(description="Test the ARTracker with a picture")
+parser.add_argument("file", help="The file to test the ARTracker with")
+args = parser.parse_args() # parse the arguments
+
+if __name__ == "__main__":
+ # Read the image from the file
+ if args.file.endswith(".jpg") or args.file.endswith(".png"):
+ image = cv2.imread(args.file)
+ else:
+ print("ERROR: File must be a jpg or png")
+ exit(-1)
+
+ # Show image
+ cv2.namedWindow('unprocessed image', cv2.WINDOW_KEEPRATIO)
+ cv2.imshow('unprocessed image', image)
+ cv2.resizeWindow('unprocessed image', image.shape[1] // 2, image.shape[0] // 2)
+
+ # Process the image
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+ adapted_thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 3)
+ cv2.namedWindow('adapted thresh', cv2.WINDOW_KEEPRATIO)
+ cv2.resizeWindow('adapted thresh', image.shape[1] // 2, image.shape[0] // 2)
+ cv2.imshow('adapted thresh', adapted_thresh)
+
+ # Detect the markers
+ markerDict = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
+ (corners, markerIDs, rejected) = aruco.detectMarkers(adapted_thresh, markerDict)
+ if (markerIDs is not None) :
+ print("Marker found")
+ print("Found IDs:" + markerIDs)
+ else:
+ print("No markers found")
+
+ if cv2.waitKey(0) & 0xFF == ord('q'):
+ exit(0)
+
diff --git a/examples/ARTrackerTest/testARvid.py b/examples/ARTrackerTest/testARvid.py
new file mode 100644
index 0000000..842a96f
--- /dev/null
+++ b/examples/ARTrackerTest/testARvid.py
@@ -0,0 +1,72 @@
+import cv2
+import cv2.aruco as aruco
+from time import sleep
+import argparse
+
+# Parse the command line arguments
+parser = argparse.ArgumentParser(description="Test the ARTracker with a video or picture")
+parser.add_argument("file", help="The file to test the ARTracker with")
+args = parser.parse_args() # parse the arguments
+
+if __name__ == "__main__":
+
+ # Import either a video or a picture
+ if args.file.endswith(".mp4") or args.file.endswith(".avi"):
+ file = cv2.VideoCapture(args.file)
+ else:
+ print("file must be a video, either .mp4 or .avi")
+ exit(-1)
+
+ # Play video or display picture
+ index = 0
+ markerTicks = 0
+ soonestFrame = 0
+ while True:
+ # Create the aruco dictionary
+ markerDict = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
+
+ # Capture and display the unprocessed frame
+ #sleep(1/45) # 30 fps NOTE: This does not accurately represent the actual fps of the video
+
+ ret, frame = file.read()
+ cv2.namedWindow('unprocessed frame', cv2.WINDOW_KEEPRATIO)
+ cv2.imshow('unprocessed frame', frame)
+ cv2.resizeWindow('unprocessed frame', frame.shape[1] // 2, frame.shape[0] // 2)
+
+ # Process the image
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+
+ # Process image by changing the contrast
+ """for i in range(40, 221, 60):
+ process_image = cv2.threshold(gray,i,255, cv2.THRESH_BINARY)[1]
+ (corners, markerIDs, rejected) = aruco.detectMarkers(process_image, markerDict)
+ if markerIDs is not None:
+ break
+ """
+
+ # Process image using Adaptive Thresholding
+ process_image = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 25, 3)
+
+ # Show the processed image
+ cv2.namedWindow('processed image', cv2.WINDOW_KEEPRATIO)
+ cv2.imshow('processed image', process_image)
+ cv2.resizeWindow('processed image', process_image.shape[1] // 2, process_image.shape[0] // 2)
+
+ # Detect the markers
+ (corners, markerIDs, rejected) = aruco.detectMarkers(process_image, markerDict)
+ if (markerIDs is not None) and (markerIDs[0] == 1):
+ if markerTicks == 0:
+ soonestFrame = index
+ print("Marker found")
+ print("Found IDs:" + str(markerIDs))
+ markerTicks += 1
+
+ print("Frame " + str(index))
+ index += 1
+ # Exit the program if the user presses 'q'
+ if cv2.waitKey(1) & 0xFF == ord('q'):
+ print("Number of times marker was found:" + str(markerTicks))
+ print("Soonest frame:" + str(soonestFrame))
+ break
+
+
diff --git a/examples/ARTrackerTest/videos/UtahAR.mp4 b/examples/ARTrackerTest/videos/UtahAR.mp4
new file mode 100644
index 0000000..b38a18f
Binary files /dev/null and b/examples/ARTrackerTest/videos/UtahAR.mp4 differ
diff --git a/gps b/gps
deleted file mode 160000
index f505c0d..0000000
--- a/gps
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit f505c0dea51cd1507a94f93d0c2ea903f8530e17
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ad28822
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,23 @@
+[project]
+name = "autonomous"
+version = "0.1.0"
+description = "Add a short description here"
+dependencies = [
+ "opencv-contrib-python==4.6.0.66",
+ "flask>=2.3.2",
+]
+readme = "README.md"
+requires-python = ">= 3.10"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.rye]
+managed = true
+
+[tool.hatch.metadata]
+allow-direct-references = true
+
+[tool.rye.workspace]
+members = ["examples", "gps", "libs", "yolo", "RoverMap"]
diff --git a/runAutonomous.sh b/run_autonomous.sh
old mode 100644
new mode 100755
similarity index 98%
rename from runAutonomous.sh
rename to run_autonomous.sh
index 28ae4f5..3787ad7
--- a/runAutonomous.sh
+++ b/run_autonomous.sh
@@ -1,4 +1,6 @@
#! /bin/bash
+cd src/autonomous
+
#Parses the config file below
main=$(cat config.ini | grep MAIN_CAMERA)
main=${main: -3}
diff --git a/src/RoverMap/.gitignore b/src/RoverMap/.gitignore
new file mode 100644
index 0000000..964a0c6
--- /dev/null
+++ b/src/RoverMap/.gitignore
@@ -0,0 +1,2 @@
+tiles/
+tiles.tar
\ No newline at end of file
diff --git a/src/RoverMap/LICENSE b/src/RoverMap/LICENSE
new file mode 100644
index 0000000..61d5f95
--- /dev/null
+++ b/src/RoverMap/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Benton Smith
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/RoverMap/README.md b/src/RoverMap/README.md
new file mode 100644
index 0000000..b938213
--- /dev/null
+++ b/src/RoverMap/README.md
@@ -0,0 +1,34 @@
+# Map Server
+
+This houses both the HTML/CSS/JS frontend to render the map with the rover's coordinates,
+and the backend server that powers it.
+
+## Structure
+
+The frontend provides a leafletjs map which draws the rover's position on it.
+The map renderer uses image files served by `server.py`, and generated by
+the generating program in `RoverMapTileGenerator`.
+
+## Running and Integration
+
+You can run the server in standalone mode by running `python3 server.py`.
+
+To run it from python, import and call `start_map_server`.
+The flask app isn't wrapped nicely in a class (this would be a good refactor),
+so we really need to test this integration and make sure it works.
+
+The server sends the rover's gps coords to the web client,
+so the server will need to be supplied with those coords.
+Call `update_rover_coords` with an array of lat, lng like `[38.4065, -110.79147]`,
+the center of the mars thingy.
+
+There's an example in `example/updater.py` to go off of.
+
+## Accessing
+
+The server should open to port 5000, so you can access by connecting to `10.0.0.2:5000`.
+That port could change somehow, so you may need to check stdout to find the exact address and port.
+
+## Documentation
+
+This SoRo component is a new candidate for documentation! If you know markdown, and have a good idea about what's going on here, please feel free to [make a new page about it in the docs](https://sooner-rover-team.github.io/soro-documentation/html/new-page-guide.html)! :)
diff --git a/src/RoverMap/RoverMapTileGenerator/.gitignore b/src/RoverMap/RoverMapTileGenerator/.gitignore
new file mode 100644
index 0000000..317f156
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+tiles/*
+*.tif
\ No newline at end of file
diff --git a/src/RoverMap/RoverMapTileGenerator/README.md b/src/RoverMap/RoverMapTileGenerator/README.md
new file mode 100644
index 0000000..96178a4
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/README.md
@@ -0,0 +1,82 @@
+# Rover Map Tile Generator
+
+Generates slippy map tiles, compatible with leaflet, from a really hi-res sattelite image and the image's lat/lon range.
+
+We needed this to generate an offline slippy map that we could plot
+rover paths on. Map services that serve compatible tiles tend to (I think all of them?) have
+clauses in their ToC about not being able to proxy or save tiles on the server-side.
+This is almost certainly not targeted at us, but it's still a ToC violation,
+so we needed another solution.
+
+We used the [USGS Earth Explorer](https://earthexplorer.usgs.gov/) to obtain a super high-res .tif file
+of a satellite image over our region of interest ([The Mars Desert Research Station](http://mdrs.marssociety.org/)).
+If you're on the rover team and happened to have lost the source image,
+the image is from the NAIP dataset, entity id `m_3811034_se_12_1_20160707`.
+(scroll down to search criteria, hit the 'decimal' button just below the polygon/circle/predefined area buttons, to switch to decimal coordinate input.
+Enter 38.3750 for lat, and -110.8125 for lon. Go to data sets and search for NAIP and enable it. Go to additional criteria,
+select the NAIP data set from the dropdown, expand the entity id field, paste in `m_3811034_se_12_1_20160707`, go to results.
+You should see only one entry. Click the download options button, select full resolution, and wait for a long time.
+It could take 10 minutes or more to load. It won't really look like it's doing anything - just be patient. Wait and wait and wait. Eventually, the file will start downloading.)
+
+It should be one large image named `m_3811034_se_12_1_20160707.tif`, the .txt file it comes with will have coordinate info to pass to the CLI.
+
+## Dependencies
+
+You'll need opencv and numpy installed - I used version 4.5.5 of opencv and version 1.22.3 of numpy.
+
+(To run the dev server to serve those tiles, you'll need to just run `npm install`.
+The dev server will display a map centered on the map region for the rover team, if you want to see a different region
+you can just change the coordinates in `index.html`.)
+
+**For the rover team - don't use this devserver.js, use server.py in the `RoverMap` directory**
+
+## Usage
+
+The CLI needs a source image to pull tiles from and it needs to know where that source image comes from on the globe.
+
+`python generate_tiles.py [imagename] [southLatitude] [northLatitude] [westLongitude] [eastLongitude]`.
+
+If you're on the rover team, you want to invoke it with
+`python generate_tiles.py m_3811034_se_12_1_20160707.tif 38.3750 38.4375 -110.8125 -110.7500`
+
+The tiles will be output to `./tiles/`, of the format "z{z}, x{x}, y{y}.jpg", so you can serve the tiles as static content.
+You'll point your leaflet tile layer url template to some route on your server like "/tiles/z/x/y", pick off those values of z, y, x, and look for a file named with those values. Check the example in devserver.js and index.html to see it in action.
+
+If you're on the rover team, the tiles directory needs to go one level above, in `Mission Control/RoverMap/tiles`, not here.
+
+For example, say you have some tile named `z11, x393, y786.jpg`, stored in some tiles folder somewhere.
+When the user pans leaflet over that tile, leaflet will make a request to "/tiles/11/393/786" on your server,
+you'll pick off those values, read in the file `z11, x393, y786.jpg`, and send that off.
+
+
+## How tiles are generated
+
+A slippy map tile is defined by a zoom level and an x, y position. All tiles are 256x256 pixels.
+So zoom level 0 encompasses the entire planet's map in one 256x256 image tile. Zoom level 1 doubles your zoom, so the world will now be comprised of 4 256x256 tiles.
+The [OSM Wiki Entry](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) on this is a good resource.
+
+The program assumes an arbitrary zoom value of 10 to begin, and goes all the way to 19. You can pass in an optional last argument to change this.
+The `-b` flag changes the base value, and `-m` changes the max zoom. `python generate_tiles.py filename bunch_of_latlonstuff -b 5 -m 22`
+will start at zoom level 5, and go all the way to 22.
+
+The program begins by generating a single tile at the base zoom level, and it will only generate
+subdivisions of this tile. You can keep increasing the value you set as the base value if it cuts off your map.
+
+Make sure that you set your client's leaflet map to
+be at the right zoom level by default - just set it to whatever base zoom level you pass in with the b flag.
+If you didn't change it, set it to 10.
+If there's no tile for some zoom level, the leaflet map will display nothing,
+so it'll look like there are no tiles until you zoom in to a zoom
+level which you generated tiles for. You could just generate
+starting at level 0, that way you'd always be able to find the area
+you generated tiles for, or set the leaflet map to have a max zoom
+equal to whatever base zoom level you used.
+
+The program will figure out what tiles need to be generated, and then will find where the map image should intersect these tiles,
+pick off that subimage, and correctly scale and place it. It won't generate tiles that do not intersect with the map region.
+
+Untangling the transformations between (lat, lon) representing (vert, horiz) where right and up are positive,
+(x, y) representing (horiz, vert) where right and down are positive, and np array indicies where (row, col) represent (vert, horiz), and tile (x, y)'s representing (horiz, vert) where (I think?) right and down are positive,
+was an absolute mess, but it works.
+I must have seen every possible combination of ways in which these coordinates could be confused, and all of the
+weird space-warping maps resulted from them.
diff --git a/src/RoverMap/RoverMapTileGenerator/devserver.js b/src/RoverMap/RoverMapTileGenerator/devserver.js
new file mode 100644
index 0000000..40a30c2
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/devserver.js
@@ -0,0 +1,27 @@
+const express = require('express');
+const { existsSync } = require('fs');
+const app = express()
+const port = 3000
+
+app.get('/', (req, res) => {
+ res.sendFile("index.html", {
+ root: "./"
+ });
+})
+
+
+app.get("/tiles/:z/:x/:y", (req, res) => {
+ if(existsSync(`./tiles/z${req.params.z}, x${req.params.x}, y${req.params.y}.jpg`)){
+ res.sendFile(`z${req.params.z}, x${req.params.x}, y${req.params.y}.jpg`, {
+ root: "./tiles/"
+ })
+ } else{
+ res.send(400)
+ }
+ console.log(req.params.z, req.params.x, req.params.y);
+})
+
+
+app.listen(port, () => {
+ console.log(`Example app listening on port ${port}`)
+})
\ No newline at end of file
diff --git a/src/RoverMap/RoverMapTileGenerator/generate_tiles.py b/src/RoverMap/RoverMapTileGenerator/generate_tiles.py
new file mode 100644
index 0000000..ba9783c
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/generate_tiles.py
@@ -0,0 +1,184 @@
+import numpy as np
+import cv2 as cv
+import argparse
+import math
+
+# from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
+def deg2num(lat_deg, lon_deg, zoom):
+ lat_rad = math.radians(lat_deg)
+ n = 2.0 ** zoom
+ xtile = int((lon_deg + 180.0) / 360.0 * n)
+ ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
+ return (xtile, ytile)
+
+# also from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
+def num2deg(xtile, ytile, zoom):
+ n = 2.0 ** zoom
+ lon_deg = xtile / n * 360.0 - 180.0
+ lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
+ lat_deg = math.degrees(lat_rad)
+ return (lat_deg, lon_deg)
+
+
+#
+# We represent lat/lon regions as intervals, where interval[0] <= interval[1].
+# This is useful to figure out where the lat/lon bounds of a tile
+# intersect the map.
+#
+
+def intersectIntervals(int1, int2):
+ adjusted1 = (min(int1), max(int1))
+ adjusted2 = (min(int2), max(int2))
+ return (max((adjusted1[0], adjusted2[0])), min(adjusted1[1], adjusted2[1]))
+
+def isValueWithinInterval(value, interval):
+ return value >= interval[0] and value <= interval[1]
+
+def isIntervalNonempty(interval):
+ return interval[0] <= interval[1]
+
+def isIntervalWithinThisOtherInterval(innerInterval, outerInterval):
+ return innerInterval[0] >= outerInterval[0] and outerInterval[1] >= innerInterval[1]
+
+
+# These latlons are the corner coordinates of the image map.
+# The base zoom value is some arbitrary zoom value to start at.
+# you could approximate this by following the table at https://wiki.openstreetmap.org/wiki/Zoom_levels
+# tiles will be generated up to the maxZoomValue
+def generateTiles(image_name, northWestLatLon, southEastLatLon, baseZoomValue, maxZoomValue):
+
+ mapLatBounds = [southEastLatLon[0], northWestLatLon[0]]
+ mapLonBounds = [northWestLatLon[1], southEastLatLon[1]]
+
+
+ # The goal here is to get the one tile of the baseZoomValue that fully encompasses the map image.
+ # We just find the tile that contains the top left corner and assume that the baseZoomValue encompasses the whole map.
+ # You should decrease your baseZoomValue in case this cuts off your map
+ baseTile = deg2num(*northWestLatLon, baseZoomValue)
+ # Why do we run deg2num and pipe it straight to num2deg?
+ # to get the lat/lon of the top left corner of the biggest tile that we'll generate.
+ # We'll use that lat/lon, but increase the zoom value,
+ # to compute what index we should start at for lower zoom levels.
+ tileBaseLatLon = num2deg(*baseTile, baseZoomValue)
+
+
+ mapImage = cv.imread(image_name)
+
+ mapImageHeight = mapImage.shape[0]
+ mapImageWidth = mapImage.shape[1]
+
+
+ # From a lat/lon in world space, convert it to a pixel location
+ # on the mapImage. This may be negative or outside the bounds of the image,
+ # representing that the map doesn't contain that point.
+ def latLonToPixels(lat, lon):
+ # latitude maps to y values
+ # longitude maps to x values
+
+ # the idea is that we find what percent we are along
+ # our latitude/longitude ranges on the map,
+ # and find that percentage along the horiz/vertical
+ # axes of the map image.
+
+ longitudeRange = southEastLatLon[1] - northWestLatLon[1]
+ latitudeRange = southEastLatLon[0] - northWestLatLon[0]
+
+ xValue = mapImageWidth * (lon - northWestLatLon[1]) / longitudeRange
+ yValue = mapImageHeight * (lat - northWestLatLon[0]) / latitudeRange
+
+ return (round(xValue), round(yValue))
+
+
+ for zoomInFactor in range(0, maxZoomValue - baseZoomValue + 1):
+ # the zoomInFactor is the number of divisons
+ # by 2 of the base tile size.
+ numTilesToCreate = 2**zoomInFactor
+
+
+ # the true zoom value of a tile
+ z = baseZoomValue + zoomInFactor
+ print(f"Generating tiles for zoom level {z}/{19}", flush=True)
+
+ # the index of the north western most (minimum x and y)
+ # tile that we generate in this step.
+ # we'll move to the right and down to fill out the grid for this step.
+ tileNumRoot = deg2num(*tileBaseLatLon, z)
+
+ for tileDx in range(0, numTilesToCreate):
+ for tileDy in range(0, numTilesToCreate):
+ tileX = tileNumRoot[0] + tileDx
+ tileY = tileNumRoot[1] + tileDy
+ # figure out what our lat/lon bounds are
+ tileTopLeftLatLon = num2deg(tileX, tileY, z)
+ tileBottomRightLatLon = num2deg(tileX+1, tileY+1, z)
+
+ tileLatBounds = [min(tileTopLeftLatLon[0], tileBottomRightLatLon[0]), max(tileTopLeftLatLon[0], tileBottomRightLatLon[0])]
+ tileLonBounds = [min(tileTopLeftLatLon[1], tileBottomRightLatLon[1]), max(tileTopLeftLatLon[1], tileBottomRightLatLon[1])]
+
+
+ # figure out what region of lat and lon from our tile intersects the map
+ latRangeToDraw = intersectIntervals(tileLatBounds, mapLatBounds)
+ lonRangeToDraw = intersectIntervals(tileLonBounds, mapLonBounds)
+
+
+
+ # if we have a meaningful intersection in both axes, we need to draw
+ # some section of the map!
+ if isIntervalNonempty(latRangeToDraw) and isIntervalNonempty(lonRangeToDraw):
+ # This is the base of the tile. We'll draw the intersected map region on it.
+ blankImage = np.zeros((256, 256, 3), np.uint8)
+
+
+ ## These pixel ranges are on the tile, not the mapImage
+ yPixelRange = (
+ round(256 * ((lonRangeToDraw[0] - tileLonBounds[0]) / (tileLonBounds[1] - tileLonBounds[0]))),
+ round(256 * ((lonRangeToDraw[1] - tileLonBounds[0]) / (tileLonBounds[1] - tileLonBounds[0])))
+ )
+
+ # Some insane coordinate axis garbling.
+ # transposed and inverted arguments, which I discovered
+ # by literally bruteforcing all 8 possible combinations
+ # of inversions and transpositions until it worked.
+ xPixelRange = (
+ 256 - round(256 * (latRangeToDraw[1] - tileLatBounds[0]) / (tileLatBounds[1] - tileLatBounds[0])),
+ 256 - round(256 * (latRangeToDraw[0] - tileLatBounds[0]) / (tileLatBounds[1] - tileLatBounds[0])),
+ )
+
+ pixelRangeWidth = xPixelRange[1] - xPixelRange[0]
+ pixelRangeHeight = yPixelRange[1] - yPixelRange[0]
+
+ # no need to draw a tile with no section of the map.
+ if pixelRangeWidth == 0 or pixelRangeHeight == 0:
+ continue
+
+ # Figure out what region of the mapImage to pull off,
+ # resize it, and stick it into the tile image.
+ imageTopLeft = latLonToPixels(latRangeToDraw[1], lonRangeToDraw[0])
+ imageBottomRight = latLonToPixels(latRangeToDraw[0], lonRangeToDraw[1])
+ mapSeg = mapImage[
+ imageTopLeft[1]:imageBottomRight[1],
+ imageTopLeft[0]:imageBottomRight[0],
+ ]
+ blankImage[xPixelRange[0]:xPixelRange[1], yPixelRange[0]:yPixelRange[1]] = cv.resize(mapSeg, (yPixelRange[1] - yPixelRange[0], xPixelRange[1] - xPixelRange[0]))
+
+
+ cv.imwrite("tiles/z{}, x{}, y{}.jpg".format(z, tileX, tileY), blankImage)
+ else:
+ continue
+
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='Generates slippy map tiles from a very hir-res map image')
+ parser.add_argument("image_name", help="The large map image to take tiles from")
+ parser.add_argument("southLatitude", help="The latitude of the south edge of the map region in the image", type=float)
+ parser.add_argument("northLatitude", help="The latitude of the north edge of the map region in the image", type=float)
+ parser.add_argument("westLongitude", help="The longitude of the west edge of the map region in the image", type=float)
+ parser.add_argument("eastLongitude", help="The longitude of the east edge of the map region in the image", type=float)
+ parser.add_argument("-b", "--baseZoomValue", help="What zoom level the entire map represents. As far as I can tell, you can set this to any arbitrary reasonable value. Default is 10.", type=int, required=False, default=10)
+ parser.add_argument("-m", "--maxZoomValue", help="The maximum zoom level of tile to be generated. Default is 19.", type=int, required=False, default=19)
+
+ args = parser.parse_args()
+
+
+ generateTiles(args.image_name, [args.northLatitude, args.westLongitude], [args.southLatitude, args.eastLongitude], args.baseZoomValue, args.maxZoomValue)
diff --git a/src/RoverMap/RoverMapTileGenerator/index.html b/src/RoverMap/RoverMapTileGenerator/index.html
new file mode 100644
index 0000000..02f3801
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/RoverMap/RoverMapTileGenerator/m_3811034_se_12_1_20160707.txt b/src/RoverMap/RoverMapTileGenerator/m_3811034_se_12_1_20160707.txt
new file mode 100644
index 0000000..8dd6fd8
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/m_3811034_se_12_1_20160707.txt
@@ -0,0 +1,352 @@
+Metadata:
+ Identification_Information:
+ Citation:
+ Citation_Information:
+ Originator: USDA-FSA-APFO Aerial Photography Field Office
+ Publication_Date: 20161019
+ Title: NAIP Digital Ortho Photo Image
+ Geospatial_Data_Presentation_Form: remote-sensing image
+ Publication_Information:
+ Publication_Place: Salt Lake City, Utah
+ Publisher: USDA-FSA-APFO Aerial Photography Field Office
+ Description:
+ Abstract:
+ This data set contains imagery from the National Agriculture
+ Imagery Program (NAIP). The NAIP program is administered by
+ USDA FSA and has been established to support two main FSA
+ strategic goals centered on agricultural production.
+ These are, increase stewardship of America's natural resources
+ while enhancing the environment, and to ensure commodities
+ are procured and distributed effectively and efficiently to
+ increase food security. The NAIP program supports these goals by
+ acquiring and providing ortho imagery that has been collected
+ during the agricultural growing season in the U.S. The NAIP
+ ortho imagery is tailored to meet FSA requirements and is a
+ fundamental tool used to support FSA farm and conservation
+ programs. Ortho imagery provides an effective, intuitive
+ means of communication about farm program administration
+ between FSA and stakeholders.
+ New technology and innovation is identified by fostering and
+ maintaining a relationship with vendors and government
+ partners, and by keeping pace with the broader geospatial
+ community. As a result of these efforts the NAIP program
+ provides three main products: DOQQ tiles,
+ Compressed County Mosaics (CCM), and Seamline shape files
+ The Contract specifications for NAIP imagery have changed
+ over time reflecting agency requirements and improving
+ technologies. These changes include image resolution,
+ horizontal accuracy, coverage area, and number of bands.
+ In general, flying seasons are established by FSA and are
+ targeted for peak crop growing conditions. The NAIP
+ acquisition cycle is based on a minimum 3 year refresh of base
+ ortho imagery. The tiling format of the NAIP imagery is based
+ on a 3.75' x 3.75' quarter quadrangle with a 300 pixel buffer
+ on all four sides. NAIP quarter quads are formatted to the UTM
+ coordinate system using the North American Datum of 1983.
+ NAIP imagery may contain as much as 10% cloud cover per tile.
+ Purpose:
+ NAIP imagery is available for distribution within 60 days
+ of the end of a flying season and is intended to provide current
+ information of agricultural conditions in support of USDA farm
+ programs. For USDA Farm Service Agency, the 1 meter and 1/2 meter
+ GSD product provides an ortho image base for Common Land Unit
+ boundaries and other data sets. The 1 meter and 1/2 meter NAIP
+ imagery is generally acquired in projects covering full states in
+ cooperation with state government and other federal agencies that
+ use the imagery for a variety of purposes including land use
+ planning and natural resource assessment. The NAIP is also used
+ for disaster response. While suitable for a variety of uses,
+ prior to 2007 the 2 meter GSD NAIP imagery was primarily intended
+ to assess "crop condition and compliance" to USDA farm program
+ conditions. The 2 meter imagery was generally acquired only
+ for agricultural areas within state projects.
+ Time_Period_of_Content:
+ Time_Period_Information:
+ Single_Date/Time:
+ Calendar_Date: 20160707
+ Currentness_Reference: Ground Condition
+ Status:
+ Progress: Complete
+ Maintenance_and_Update_Frequency: Irregular
+ Spatial_Domain:
+ Bounding_Coordinates:
+ West_Bounding_Coordinate: -110.8125
+ East_Bounding_Coordinate: -110.7500
+ North_Bounding_Coordinate: 38.4375
+ South_Bounding_Coordinate: 38.3750
+ Keywords:
+ Theme:
+ Theme_Keyword_Thesaurus: None
+ Theme_Keyword: farming
+ Theme_Keyword: Digital Ortho rectified Image
+ Theme_Keyword: Ortho Rectification
+ Theme_Keyword: Quarter Quadrangle
+ Theme_Keyword: NAIP
+ Theme_Keyword: Aerial Compliance
+ Theme_Keyword: Compliance
+ Place:
+ Place_Keyword_Thesaurus: Geographic Names Information System
+ Place_Keyword: UT
+ Place_Keyword: Wayne
+ Place_Keyword: 49055
+ Place_Keyword: UT055
+ Place_Keyword: WAYNE CO UT FSA
+ Place_Keyword: 3811034
+ Place_Keyword: SKYLINE RIM, SE
+ Place_Keyword: SKYLINE RIM
+ Access_Constraints: There are no limitations for access.
+ Use_Constraints:
+ None. The USDA-FSA Aerial Photography Field office
+ asks to be credited in derived products.
+ Point_of_Contact:
+ Contact_Information:
+ Contact_Organization_Primary:
+ Contact_Organization: Aerial Photography Field Office (APFO)
+ Contact_Address:
+ Address_Type: mailing and physical address
+ Address: 2222 West 2300 South
+ City: Salt Lake City
+ State_or_Province: Utah
+ Postal_Code: 84119-2020
+ Country: USA
+ Contact_Voice_Telephone: 801-844-2922
+ Contact_Facsimile_Telephone: 801-956-3653
+ Contact_Electronic_Mail_Address: apfo.sales@slc.usda.gov
+ Browse_Graphic:
+ Browse_Graphic_File_Name: None
+ Browse_Graphic_File_Description: None
+ Browse_Graphic_File_Type: None
+ Native_Data_Set_Environment:
+ Data_Quality_Information:
+ Logical_Consistency_Report:
+ NAIP 3.75 minute tile file names are based
+ on the USGS quadrangle naming convention.
+ Completeness_Report: None
+ Positional_Accuracy:
+ Horizontal_Positional_Accuracy:
+ Horizontal_Positional_Accuracy_Report:
+ NAIP horizontal accuracy specifications have evolved over
+ the life of the program. From 2003 to 2004 the
+ specifications were as follows: 1-meter GSD imagery was
+ to match within 3-meters, and 2-meter GSD to match within 10
+ meters of reference imagery. For 2005 the 1-meter GSD
+ specification was changed to 5 meters matching the reference
+ imagery. In 2006 a pilot project was performed using true
+ ground specifications rather than reference imagery. All
+ states used the same specifications as 2005 except Utah,
+ which required a match of +/- 6 meters to true ground.
+ In 2007 all specifications were the same as 2006 except
+ Arizona used true ground specifications and all other states
+ used reference imagery. In 2008 and subsequent years
+ no 2-meter GSD imagery was acquired and all specifications
+ were the same as 2007 except approximately half of the
+ states acquired used true ground specifications and the
+ other half used reference imagery. The 2008 states that
+ used absolute ground control where; Indiana, Minnesota,
+ New Hampshire, North Carolina, Texas, Vermont, and Virginia.
+ From 2009 to present all NAIP imagery acquisitions used
+ the +/- 6 meters to ground specification.
+ Lineage:
+ Source_Information:
+ Source_Citation:
+ Citation_Information:
+ Originator: USDA-FSA-APFO Aerial Photography Field Office
+ Publication_Date: 20161019
+ Title: SKYLINE RIM, SE
+ Geospatial_Data_Presentation_Form: remote-sensing image
+ Type_of_Source_Media: UnKnown
+ Source_Time_Period_of_Content:
+ Time_Period_Information:
+ Single_Date/Time:
+ Calendar_Date: 20160707
+ Source_Currentness_Reference:
+ Aerial Photography Date for aerial photo source.
+ Source_Citation_Abbreviation: Georectifed Image
+ Source_Contribution: Digital Georectifed Image.
+
+ Process_Step:
+ Process_Description:
+ DOQQ Production Process Description
+ USDA FSA APFO NAIP Program 2014
+ State: Oregon
+
+ Digital imagery was collected at a nominal GSD of 40cm
+ using seven Cessna 441 aircraft flying at an average
+ flight height of 5000m AGL. All aircraft flew with
+ Leica Geosystem's ADS100/SH100 digital sensors with
+ firmware 4.12. Each sensor collected 12
+ image bands. Red,Green,Blue and Near-infrared at
+ each of three look angles; Backward 19 degrees,
+ Forward 26 degrees and Nadir. The Nadir Green band
+ was collected in high resolution mode effectively
+ doubling the resolution for that band.
+ The ADS100 spectral ranges are; Red 619-651nm,Green
+ 525-585nm,Blue 435-495nm and Near-infrared at
+ 808-882nm. The CCD arrays have a pixel size of 5.0
+ microns in a 20000x1 format at nadir; a 18000x1
+ format at the backward look angle and a 16000x1 format
+ at the forward look angle.
+ The CCD's have a dynamic range of 72db and the A/D
+ converters have a resolution of 14bits.
+ The ADS is a push-broom sensor and the ground
+ footprint of the imagery at a nominal GSD of 40cm
+ is approximately 8km wide by the length flightline.
+ The maximum flightline length is limited to approximately
+ 240km. The factory calibrations and IMU alignments for
+ each sensor (Serial Numbers: 10512,10514,10519,
+ 10521,10526,10527,10528) were tested and verified by
+ in-situ test flights before the start of the project.
+ The Leica MissionPro Flight Planning Software is used
+ to develop the flight acquisition plans.
+ Flight acquisition sub blocks are designed first to
+ define the GNSS base station logistics, and to break
+ the project up into manageable acquisition units. The
+ flight acquisition sub blocks are designed based on
+ the specified acquisition season, native UTM zone of
+ the DOQQs, flight line length limitations (to ensure
+ sufficient performance of the IMU solution) as well
+ as air traffic restrictions in the area. Once the sub
+ blocks have been delineated they are brought into MissionPro
+ for flight line design. The design parameters used in
+ MissionPro will be 30% lateral overlap and 40cm resolution.
+ The flight lines have been designed with a north/south
+ orientation. The design takes into account the
+ latitude of the state, which affects line spacing due
+ to convergence as well as the terrain. SRTM elevation
+ data is used in the MissionPro design to ensure the 50cm
+ GSD is achieved over all types of terrain.
+ The raw data was downloaded from the sensors after
+ each flight using Leica XPro software. The imagery was
+ then georeferenced using the 200Hz GPS/INS data
+ creating an exterior orientation for each scan line
+ (x/y/z/o/p/k). Leica Xpro APM software was used to
+ automatically generate tiepoint measurements between
+ the foward 26 degree, nadir and backward 19 degree look
+ angles for each line and to tie all flight lines together.
+ The resulting point data and exterior orientation data
+ were used to perform a full bundle adjustment using ORIMA
+ software. Blunders were removed, and additional tie points
+ measured in weak areas to ensure a robust solution.
+ Once the point data was clean and point coverage was
+ acceptable, photo-identifiable GPS-surveyed ground
+ control points were introduced into the block
+ adjustment. The bundle adjustment process produces
+ revised exterior orientation data for the sensor with
+ GPS/INS, datum, and sensor calibration errors modeled
+ and removed. Using the revised exterior orientation
+ from the bundle adjustment, orthorectified image
+ strips were created with Xpro software and the June
+ 2014 USGS 10m NED DEM. The Xpro orthorectification
+ software applies an atmospheric-BRDF radiometric
+ correction to the imagery. This correction compensates
+ for atmospheric absorption, solar illumination angle
+ and bi-directional reflectance. The orthorectified
+ strips were then overlaid with each other and the
+ ground control to check accuracy. Once the accuracy of
+ the orthorectified image strips were validated the
+ strips were then imported into Inpho's OrthoVista 5.7
+ package which was used for the final radiometric
+ balance, mosaic, and DOQQ sheet creation. The final
+ DOQQ sheets, with a 300m buffer and a ground pixel
+ resolution of 1 meter were then combined and compressed
+ to create the county wide CCMs.
+ Process_Date: 20161019
+ Spatial_Data_Organization_Information:
+ Indirect_Spatial_Reference: Wayne County, UT
+ Direct_Spatial_Reference_Method: Raster
+ Raster_Object_Information:
+ Raster_Object_Type: Pixel
+ Row_Count: 1
+ Column_Count: 1
+ Spatial_Reference_Information:
+ Horizontal_Coordinate_System_Definition:
+ Planar:
+ Grid_Coordinate_System:
+ Grid_Coordinate_System_Name: Universal Transverse Mercator
+ Universal_Transverse_Mercator:
+ UTM_Zone_Number: 12
+ Transverse_Mercator:
+ Scale_Factor_at_Central_Meridian: 0.9996
+ Longitude_of_Central_Meridian: -111.0
+ Latitude_of_Projection_Origin: 0.0
+ False_Easting: 500000
+ False_Northing: 0.0
+ Planar_Coordinate_Information:
+ Planar_Coordinate_Encoding_Method: row and column
+ Coordinate_Representation:
+ Abscissa_Resolution: 1
+ Ordinate_Resolution: 1
+ Planar_Distance_Units: meters
+ Geodetic_Model:
+ Horizontal_Datum_Name: North American Datum of 1983
+ Ellipsoid_Name: Geodetic Reference System 80 (GRS 80)
+ Semi-major_Axis: 6378137
+ Denominator_of_Flattening_Ratio: 298.257
+ Entity_and_Attribute_Information:
+ Overview_Description:
+ Entity_and_Attribute_Overview:
+ 32-bit pixels, 4 band color(RGBIR) values 0 - 255
+ Entity_and_Attribute_Detail_Citation: None
+ Distribution_Information:
+ Distributor:
+ Contact_Information:
+ Contact_Person_Primary:
+ Contact_Person: Supervisor Customer Services Section
+ Contact_Organization:
+ USDA-FSA-APFO Aerial Photography Field Office
+ Contact_Address:
+ Address_Type: mailing and physical address
+ Address: 2222 West 2300 South
+ City: Salt Lake City
+ State_or_Province: Utah
+ Postal_Code: 84119-2020
+ Country: USA
+ Contact_Voice_Telephone: 801-844-2922
+ Contact_Facsimile_Telephone: 801-956-3653
+ Contact_Electronic_Mail_Address: apfo.sales@slc.usda.gov
+ Distribution_Liability:
+ In no event shall the creators, custodians, or distributors
+ of this information be liable for any damages arising out
+ of its use (or the inability to use it).
+ Standard_Order_Process:
+ Digital_Form:
+ Digital_Transfer_Information:
+ Format_Name:
+ Format_Information_Content: Multispectral 4-band
+ Digital_Transfer_Option:
+ Offline_Option:
+ Offline_Media: CD-ROM
+ Recording_Format: ISO 9660 Mode 1 Level 2 Extensions
+ Offline_Option:
+ Offline_Media: DVD-R
+ Recording_Format: ISO 9660
+ Offline_Option:
+ Offline_Media: USB Hard Disk
+ Recording_Format: NTFS
+ Offline_Option:
+ Offline_Media: FireWire Hard Disk
+ Recording_Format: NTFS
+ Fees:
+ Contact the Aerial Photography Field Office
+ for more information
+ Resource_Description:
+ m_3811034_se_12_1_20160707_20161017.tif
+ Metadata_Reference_Information:
+ Metadata_Date: 20161019
+ Metadata_Contact:
+ Contact_Information:
+ Contact_Organization_Primary:
+ Contact_Organization:
+ USDA-FSA-APFO Aerial Photography Field Office
+ Contact_Address:
+ Address_Type: mailing and physical address
+ Address: 2222 West 2300 South
+ City: Salt Lake City
+ State_or_Province: Utah
+ Postal_Code: 84119-2020
+ Country: USA
+ Contact_Voice_Telephone: 801-844-2922
+ Metadata_Standard_Name:
+ Content Standard for Digital Geospatial Metadata
+ Metadata_Standard_Version: FGDC-STD-001-1998
+
diff --git a/src/RoverMap/RoverMapTileGenerator/package-lock.json b/src/RoverMap/RoverMapTileGenerator/package-lock.json
new file mode 100644
index 0000000..386c577
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/package-lock.json
@@ -0,0 +1,1018 @@
+{
+ "name": "somemdrsimagery",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "somemdrsimagery",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "express": "^4.18.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
+ "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.10.3",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz",
+ "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.0",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.10.3",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
+ "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.10.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
+ "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ },
+ "dependencies": {
+ "accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "requires": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "body-parser": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
+ "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
+ "requires": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.10.3",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ }
+ },
+ "bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
+ },
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
+ "content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "requires": {
+ "safe-buffer": "5.2.1"
+ }
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ },
+ "destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz",
+ "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==",
+ "requires": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.0",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.10.3",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+ },
+ "http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "requires": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+ },
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
+ },
+ "object-inspect": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
+ "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "requires": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ }
+ },
+ "qs": {
+ "version": "6.10.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
+ "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+ },
+ "raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "requires": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
+ },
+ "toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ }
+ }
+}
diff --git a/src/RoverMap/RoverMapTileGenerator/package.json b/src/RoverMap/RoverMapTileGenerator/package.json
new file mode 100644
index 0000000..b172e09
--- /dev/null
+++ b/src/RoverMap/RoverMapTileGenerator/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "somemdrsimagery",
+ "version": "1.0.0",
+ "description": "",
+ "main": "devserver.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "express": "^4.18.0"
+ }
+}
diff --git a/libs/__init__.py b/src/RoverMap/__init__.py
similarity index 100%
rename from libs/__init__.py
rename to src/RoverMap/__init__.py
diff --git a/src/RoverMap/example/simpleExample.py b/src/RoverMap/example/simpleExample.py
new file mode 100644
index 0000000..6ceaf3e
--- /dev/null
+++ b/src/RoverMap/example/simpleExample.py
@@ -0,0 +1,36 @@
+from nis import maps
+import os
+from random import randint, random
+
+from scipy import rand
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+import threading
+
+import sys
+sys.path.append('../')
+
+from server import MapServer
+
+if __name__ == '__main__':
+
+
+ mapServer = MapServer()
+ mapServer.register_routes()
+ mapServer.start()
+
+ def set_interval(func, sec):
+ def func_wrapper():
+ set_interval(func, sec)
+ func()
+ t = threading.Timer(sec, func_wrapper)
+ t.start()
+ return t
+
+ def update():
+ print("sending update...")
+ mapServer.update_rover_coords([38.4375 + randint(0, 100) / 10000 , -110.8125])
+
+ print("setting interval")
+ set_interval(update, 0.500)
+
diff --git a/src/RoverMap/extremeTraversal.py b/src/RoverMap/extremeTraversal.py
new file mode 100644
index 0000000..dc5b227
--- /dev/null
+++ b/src/RoverMap/extremeTraversal.py
@@ -0,0 +1,38 @@
+from nis import maps
+import os
+from random import randint, random
+
+from scipy import rand
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+import threading
+
+from server import MapServer
+from libs import Location
+
+if __name__ == '__main__':
+
+ loc = Location.Location('10.0.0.222', '55556')
+ print('Starting GPS')
+ loc.start_GPS()
+ loc.start_GPS_thread()
+
+ mapServer = MapServer()
+ mapServer.register_routes()
+ mapServer.start()
+
+ def set_interval(func, sec):
+ def func_wrapper():
+ set_interval(func, sec)
+ func()
+ t = threading.Timer(sec, func_wrapper)
+ t.start()
+ return t
+
+ def update():
+ print("sending update...")
+ #mapServer.update_rover_coords([38.4375 + randint(0, 100) / 10000 , -110.8125])
+ mapServer.update_rover_coords([loc.latitude, loc.longitude])
+
+ set_interval(update, 0.500)
+
diff --git a/src/RoverMap/frontend/.gitignore b/src/RoverMap/frontend/.gitignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/src/RoverMap/frontend/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/src/RoverMap/frontend/index.css b/src/RoverMap/frontend/index.css
new file mode 100644
index 0000000..8b8548e
--- /dev/null
+++ b/src/RoverMap/frontend/index.css
@@ -0,0 +1,68 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+body{
+ padding: 1em;
+}
+
+#infoWrapper{
+ /* position: absolute; */
+ left: 1em;
+ top: 0;
+ max-width: 90vw;
+ word-wrap: break-word;
+ z-index: 2;
+}
+
+#infoWrapper > button{
+ padding: 0.5em;
+}
+
+#unitConverter{
+ background-color: black;
+ width: 50%;
+ height: 60%;
+ position: absolute;
+ display: none;
+ top: 20%;
+ left: 25%;
+ z-index: 999;
+ color: white;
+ padding: 1em;
+ overflow-y: scroll;
+}
+
+#closeUnitConverterButton{
+ padding: 0.25em;
+}
+
+
+@media only screen and (max-width: 600px) {
+ #unitConverter {
+ width: 80%;
+ height: 70%;
+ position: absolute;
+ top: 15%;
+ left: 5%;
+ }
+ }
+
+
+#saved{
+ padding-top: 1em;
+}
+
+#forceUpdate{
+ display: none;
+}
+
+@media only screen and (max-width: 600px) {
+ #forceUpdate{
+ display: block;
+ margin-top: 1em;
+ }
+
+ }
+
diff --git a/src/RoverMap/frontend/index.html b/src/RoverMap/frontend/index.html
new file mode 100644
index 0000000..7c31d63
--- /dev/null
+++ b/src/RoverMap/frontend/index.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+ Rover Map Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Path Coords:
+
+ Click to plot a path on the map.
+ Refresh the page to clear it
+
+ `
+ document.execCommand("copy")
+}
\ No newline at end of file
diff --git a/src/RoverMap/frontend/leaflet/images/layers-2x.png b/src/RoverMap/frontend/leaflet/images/layers-2x.png
new file mode 100644
index 0000000..200c333
Binary files /dev/null and b/src/RoverMap/frontend/leaflet/images/layers-2x.png differ
diff --git a/src/RoverMap/frontend/leaflet/images/layers.png b/src/RoverMap/frontend/leaflet/images/layers.png
new file mode 100644
index 0000000..1a72e57
Binary files /dev/null and b/src/RoverMap/frontend/leaflet/images/layers.png differ
diff --git a/src/RoverMap/frontend/leaflet/images/marker-icon-2x.png b/src/RoverMap/frontend/leaflet/images/marker-icon-2x.png
new file mode 100644
index 0000000..88f9e50
Binary files /dev/null and b/src/RoverMap/frontend/leaflet/images/marker-icon-2x.png differ
diff --git a/src/RoverMap/frontend/leaflet/images/marker-icon.png b/src/RoverMap/frontend/leaflet/images/marker-icon.png
new file mode 100644
index 0000000..950edf2
Binary files /dev/null and b/src/RoverMap/frontend/leaflet/images/marker-icon.png differ
diff --git a/src/RoverMap/frontend/leaflet/images/marker-shadow.png b/src/RoverMap/frontend/leaflet/images/marker-shadow.png
new file mode 100644
index 0000000..9fd2979
Binary files /dev/null and b/src/RoverMap/frontend/leaflet/images/marker-shadow.png differ
diff --git a/src/RoverMap/frontend/leaflet/leaflet-src.esm.js b/src/RoverMap/frontend/leaflet/leaflet-src.esm.js
new file mode 100644
index 0000000..415fbfd
--- /dev/null
+++ b/src/RoverMap/frontend/leaflet/leaflet-src.esm.js
@@ -0,0 +1,14033 @@
+/* @preserve
+ * Leaflet 1.8.0, a JS library for interactive maps. https://leafletjs.com
+ * (c) 2010-2022 Vladimir Agafonkin, (c) 2010-2011 CloudMade
+ */
+
+var version = "1.8.0";
+
+/*
+ * @namespace Util
+ *
+ * Various utility functions, used by Leaflet internally.
+ */
+
+// @function extend(dest: Object, src?: Object): Object
+// Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut.
+function extend(dest) {
+ var i, j, len, src;
+
+ for (j = 1, len = arguments.length; j < len; j++) {
+ src = arguments[j];
+ for (i in src) {
+ dest[i] = src[i];
+ }
+ }
+ return dest;
+}
+
+// @function create(proto: Object, properties?: Object): Object
+// Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create)
+var create$2 = Object.create || (function () {
+ function F() {}
+ return function (proto) {
+ F.prototype = proto;
+ return new F();
+ };
+})();
+
+// @function bind(fn: Function, …): Function
+// Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind).
+// Has a `L.bind()` shortcut.
+function bind(fn, obj) {
+ var slice = Array.prototype.slice;
+
+ if (fn.bind) {
+ return fn.bind.apply(fn, slice.call(arguments, 1));
+ }
+
+ var args = slice.call(arguments, 2);
+
+ return function () {
+ return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments);
+ };
+}
+
+// @property lastId: Number
+// Last unique ID used by [`stamp()`](#util-stamp)
+var lastId = 0;
+
+// @function stamp(obj: Object): Number
+// Returns the unique ID of an object, assigning it one if it doesn't have it.
+function stamp(obj) {
+ if (!('_leaflet_id' in obj)) {
+ obj['_leaflet_id'] = ++lastId;
+ }
+ return obj._leaflet_id;
+}
+
+// @function throttle(fn: Function, time: Number, context: Object): Function
+// Returns a function which executes function `fn` with the given scope `context`
+// (so that the `this` keyword refers to `context` inside `fn`'s code). The function
+// `fn` will be called no more than one time per given amount of `time`. The arguments
+// received by the bound function will be any arguments passed when binding the
+// function, followed by any arguments passed when invoking the bound function.
+// Has an `L.throttle` shortcut.
+function throttle(fn, time, context) {
+ var lock, args, wrapperFn, later;
+
+ later = function () {
+ // reset lock and call if queued
+ lock = false;
+ if (args) {
+ wrapperFn.apply(context, args);
+ args = false;
+ }
+ };
+
+ wrapperFn = function () {
+ if (lock) {
+ // called too soon, queue to call later
+ args = arguments;
+
+ } else {
+ // call and lock until later
+ fn.apply(context, arguments);
+ setTimeout(later, time);
+ lock = true;
+ }
+ };
+
+ return wrapperFn;
+}
+
+// @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number
+// Returns the number `num` modulo `range` in such a way so it lies within
+// `range[0]` and `range[1]`. The returned value will be always smaller than
+// `range[1]` unless `includeMax` is set to `true`.
+function wrapNum(x, range, includeMax) {
+ var max = range[1],
+ min = range[0],
+ d = max - min;
+ return x === max && includeMax ? x : ((x - min) % d + d) % d + min;
+}
+
+// @function falseFn(): Function
+// Returns a function which always returns `false`.
+function falseFn() { return false; }
+
+// @function formatNum(num: Number, precision?: Number|false): Number
+// Returns the number `num` rounded with specified `precision`.
+// The default `precision` value is 6 decimal places.
+// `false` can be passed to skip any processing (can be useful to avoid round-off errors).
+function formatNum(num, precision) {
+ if (precision === false) { return num; }
+ var pow = Math.pow(10, precision === undefined ? 6 : precision);
+ return Math.round(num * pow) / pow;
+}
+
+// @function trim(str: String): String
+// Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)
+function trim(str) {
+ return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
+}
+
+// @function splitWords(str: String): String[]
+// Trims and splits the string on whitespace and returns the array of parts.
+function splitWords(str) {
+ return trim(str).split(/\s+/);
+}
+
+// @function setOptions(obj: Object, options: Object): Object
+// Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut.
+function setOptions(obj, options) {
+ if (!Object.prototype.hasOwnProperty.call(obj, 'options')) {
+ obj.options = obj.options ? create$2(obj.options) : {};
+ }
+ for (var i in options) {
+ obj.options[i] = options[i];
+ }
+ return obj.options;
+}
+
+// @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String
+// Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}`
+// translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will
+// be appended at the end. If `uppercase` is `true`, the parameter names will
+// be uppercased (e.g. `'?A=foo&B=bar'`)
+function getParamString(obj, existingUrl, uppercase) {
+ var params = [];
+ for (var i in obj) {
+ params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));
+ }
+ return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');
+}
+
+var templateRe = /\{ *([\w_ -]+) *\}/g;
+
+// @function template(str: String, data: Object): String
+// Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'`
+// and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string
+// `('Hello foo, bar')`. You can also specify functions instead of strings for
+// data values — they will be evaluated passing `data` as an argument.
+function template(str, data) {
+ return str.replace(templateRe, function (str, key) {
+ var value = data[key];
+
+ if (value === undefined) {
+ throw new Error('No value provided for variable ' + str);
+
+ } else if (typeof value === 'function') {
+ value = value(data);
+ }
+ return value;
+ });
+}
+
+// @function isArray(obj): Boolean
+// Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)
+var isArray = Array.isArray || function (obj) {
+ return (Object.prototype.toString.call(obj) === '[object Array]');
+};
+
+// @function indexOf(array: Array, el: Object): Number
+// Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)
+function indexOf(array, el) {
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] === el) { return i; }
+ }
+ return -1;
+}
+
+// @property emptyImageUrl: String
+// Data URI string containing a base64-encoded empty GIF image.
+// Used as a hack to free memory from unused images on WebKit-powered
+// mobile devices (by setting image `src` to this string).
+var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
+
+// inspired by https://paulirish.com/2011/requestanimationframe-for-smart-animating/
+
+function getPrefixed(name) {
+ return window['webkit' + name] || window['moz' + name] || window['ms' + name];
+}
+
+var lastTime = 0;
+
+// fallback for IE 7-8
+function timeoutDefer(fn) {
+ var time = +new Date(),
+ timeToCall = Math.max(0, 16 - (time - lastTime));
+
+ lastTime = time + timeToCall;
+ return window.setTimeout(fn, timeToCall);
+}
+
+var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
+var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') ||
+ getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); };
+
+// @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number
+// Schedules `fn` to be executed when the browser repaints. `fn` is bound to
+// `context` if given. When `immediate` is set, `fn` is called immediately if
+// the browser doesn't have native support for
+// [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame),
+// otherwise it's delayed. Returns a request ID that can be used to cancel the request.
+function requestAnimFrame(fn, context, immediate) {
+ if (immediate && requestFn === timeoutDefer) {
+ fn.call(context);
+ } else {
+ return requestFn.call(window, bind(fn, context));
+ }
+}
+
+// @function cancelAnimFrame(id: Number): undefined
+// Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame).
+function cancelAnimFrame(id) {
+ if (id) {
+ cancelFn.call(window, id);
+ }
+}
+
+var Util = {
+ __proto__: null,
+ extend: extend,
+ create: create$2,
+ bind: bind,
+ get lastId () { return lastId; },
+ stamp: stamp,
+ throttle: throttle,
+ wrapNum: wrapNum,
+ falseFn: falseFn,
+ formatNum: formatNum,
+ trim: trim,
+ splitWords: splitWords,
+ setOptions: setOptions,
+ getParamString: getParamString,
+ template: template,
+ isArray: isArray,
+ indexOf: indexOf,
+ emptyImageUrl: emptyImageUrl,
+ requestFn: requestFn,
+ cancelFn: cancelFn,
+ requestAnimFrame: requestAnimFrame,
+ cancelAnimFrame: cancelAnimFrame
+};
+
+// @class Class
+// @aka L.Class
+
+// @section
+// @uninheritable
+
+// Thanks to John Resig and Dean Edwards for inspiration!
+
+function Class() {}
+
+Class.extend = function (props) {
+
+ // @function extend(props: Object): Function
+ // [Extends the current class](#class-inheritance) given the properties to be included.
+ // Returns a Javascript function that is a class constructor (to be called with `new`).
+ var NewClass = function () {
+
+ setOptions(this);
+
+ // call the constructor
+ if (this.initialize) {
+ this.initialize.apply(this, arguments);
+ }
+
+ // call all constructor hooks
+ this.callInitHooks();
+ };
+
+ var parentProto = NewClass.__super__ = this.prototype;
+
+ var proto = create$2(parentProto);
+ proto.constructor = NewClass;
+
+ NewClass.prototype = proto;
+
+ // inherit parent's statics
+ for (var i in this) {
+ if (Object.prototype.hasOwnProperty.call(this, i) && i !== 'prototype' && i !== '__super__') {
+ NewClass[i] = this[i];
+ }
+ }
+
+ // mix static properties into the class
+ if (props.statics) {
+ extend(NewClass, props.statics);
+ }
+
+ // mix includes into the prototype
+ if (props.includes) {
+ checkDeprecatedMixinEvents(props.includes);
+ extend.apply(null, [proto].concat(props.includes));
+ }
+
+ // mix given properties into the prototype
+ extend(proto, props);
+ delete proto.statics;
+ delete proto.includes;
+
+ // merge options
+ if (proto.options) {
+ proto.options = parentProto.options ? create$2(parentProto.options) : {};
+ extend(proto.options, props.options);
+ }
+
+ proto._initHooks = [];
+
+ // add method for calling all hooks
+ proto.callInitHooks = function () {
+
+ if (this._initHooksCalled) { return; }
+
+ if (parentProto.callInitHooks) {
+ parentProto.callInitHooks.call(this);
+ }
+
+ this._initHooksCalled = true;
+
+ for (var i = 0, len = proto._initHooks.length; i < len; i++) {
+ proto._initHooks[i].call(this);
+ }
+ };
+
+ return NewClass;
+};
+
+
+// @function include(properties: Object): this
+// [Includes a mixin](#class-includes) into the current class.
+Class.include = function (props) {
+ var parentOptions = this.prototype.options;
+ extend(this.prototype, props);
+ if (props.options) {
+ this.prototype.options = parentOptions;
+ this.mergeOptions(props.options);
+ }
+ return this;
+};
+
+// @function mergeOptions(options: Object): this
+// [Merges `options`](#class-options) into the defaults of the class.
+Class.mergeOptions = function (options) {
+ extend(this.prototype.options, options);
+ return this;
+};
+
+// @function addInitHook(fn: Function): this
+// Adds a [constructor hook](#class-constructor-hooks) to the class.
+Class.addInitHook = function (fn) { // (Function) || (String, args...)
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ var init = typeof fn === 'function' ? fn : function () {
+ this[fn].apply(this, args);
+ };
+
+ this.prototype._initHooks = this.prototype._initHooks || [];
+ this.prototype._initHooks.push(init);
+ return this;
+};
+
+function checkDeprecatedMixinEvents(includes) {
+ if (typeof L === 'undefined' || !L || !L.Mixin) { return; }
+
+ includes = isArray(includes) ? includes : [includes];
+
+ for (var i = 0; i < includes.length; i++) {
+ if (includes[i] === L.Mixin.Events) {
+ console.warn('Deprecated include of L.Mixin.Events: ' +
+ 'this property will be removed in future releases, ' +
+ 'please inherit from L.Evented instead.', new Error().stack);
+ }
+ }
+}
+
+/*
+ * @class Evented
+ * @aka L.Evented
+ * @inherits Class
+ *
+ * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event).
+ *
+ * @example
+ *
+ * ```js
+ * map.on('click', function(e) {
+ * alert(e.latlng);
+ * } );
+ * ```
+ *
+ * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:
+ *
+ * ```js
+ * function onClick(e) { ... }
+ *
+ * map.on('click', onClick);
+ * map.off('click', onClick);
+ * ```
+ */
+
+var Events = {
+ /* @method on(type: String, fn: Function, context?: Object): this
+ * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`).
+ *
+ * @alternative
+ * @method on(eventMap: Object): this
+ * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+ */
+ on: function (types, fn, context) {
+
+ // types can be a map of types/handlers
+ if (typeof types === 'object') {
+ for (var type in types) {
+ // we don't process space-separated events here for performance;
+ // it's a hot path since Layer uses the on(obj) syntax
+ this._on(type, types[type], fn);
+ }
+
+ } else {
+ // types can be a string of space-separated words
+ types = splitWords(types);
+
+ for (var i = 0, len = types.length; i < len; i++) {
+ this._on(types[i], fn, context);
+ }
+ }
+
+ return this;
+ },
+
+ /* @method off(type: String, fn?: Function, context?: Object): this
+ * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener.
+ *
+ * @alternative
+ * @method off(eventMap: Object): this
+ * Removes a set of type/listener pairs.
+ *
+ * @alternative
+ * @method off: this
+ * Removes all listeners to all events on the object. This includes implicitly attached events.
+ */
+ off: function (types, fn, context) {
+
+ if (!arguments.length) {
+ // clear all listeners if called without arguments
+ delete this._events;
+
+ } else if (typeof types === 'object') {
+ for (var type in types) {
+ this._off(type, types[type], fn);
+ }
+
+ } else {
+ types = splitWords(types);
+
+ var removeAll = arguments.length === 1;
+ for (var i = 0, len = types.length; i < len; i++) {
+ if (removeAll) {
+ this._off(types[i]);
+ } else {
+ this._off(types[i], fn, context);
+ }
+ }
+ }
+
+ return this;
+ },
+
+ // attach listener (without syntactic sugar now)
+ _on: function (type, fn, context) {
+ if (typeof fn !== 'function') {
+ console.warn('wrong listener type: ' + typeof fn);
+ return;
+ }
+ this._events = this._events || {};
+
+ /* get/init listeners for type */
+ var typeListeners = this._events[type];
+ if (!typeListeners) {
+ typeListeners = [];
+ this._events[type] = typeListeners;
+ }
+
+ if (context === this) {
+ // Less memory footprint.
+ context = undefined;
+ }
+ var newListener = {fn: fn, ctx: context},
+ listeners = typeListeners;
+
+ // check if fn already there
+ for (var i = 0, len = listeners.length; i < len; i++) {
+ if (listeners[i].fn === fn && listeners[i].ctx === context) {
+ return;
+ }
+ }
+
+ listeners.push(newListener);
+ },
+
+ _off: function (type, fn, context) {
+ var listeners,
+ i,
+ len;
+
+ if (!this._events) { return; }
+
+ listeners = this._events[type];
+
+ if (!listeners) {
+ return;
+ }
+
+ if (arguments.length === 1) { // remove all
+ if (this._firingCount) {
+ // Set all removed listeners to noop
+ // so they are not called if remove happens in fire
+ for (i = 0, len = listeners.length; i < len; i++) {
+ listeners[i].fn = falseFn;
+ }
+ }
+ // clear all listeners for a type if function isn't specified
+ delete this._events[type];
+ return;
+ }
+
+ if (context === this) {
+ context = undefined;
+ }
+
+ if (typeof fn !== 'function') {
+ console.warn('wrong listener type: ' + typeof fn);
+ return;
+ }
+ // find fn and remove it
+ for (i = 0, len = listeners.length; i < len; i++) {
+ var l = listeners[i];
+ if (l.ctx !== context) { continue; }
+ if (l.fn === fn) {
+ if (this._firingCount) {
+ // set the removed listener to noop so that's not called if remove happens in fire
+ l.fn = falseFn;
+
+ /* copy array in case events are being fired */
+ this._events[type] = listeners = listeners.slice();
+ }
+ listeners.splice(i, 1);
+
+ return;
+ }
+ }
+ console.warn('listener not found');
+ },
+
+ // @method fire(type: String, data?: Object, propagate?: Boolean): this
+ // Fires an event of the specified type. You can optionally provide a data
+ // object — the first argument of the listener function will contain its
+ // properties. The event can optionally be propagated to event parents.
+ fire: function (type, data, propagate) {
+ if (!this.listens(type, propagate)) { return this; }
+
+ var event = extend({}, data, {
+ type: type,
+ target: this,
+ sourceTarget: data && data.sourceTarget || this
+ });
+
+ if (this._events) {
+ var listeners = this._events[type];
+
+ if (listeners) {
+ this._firingCount = (this._firingCount + 1) || 1;
+ for (var i = 0, len = listeners.length; i < len; i++) {
+ var l = listeners[i];
+ l.fn.call(l.ctx || this, event);
+ }
+
+ this._firingCount--;
+ }
+ }
+
+ if (propagate) {
+ // propagate the event to parents (set with addEventParent)
+ this._propagateEvent(event);
+ }
+
+ return this;
+ },
+
+ // @method listens(type: String, propagate?: Boolean): Boolean
+ // Returns `true` if a particular event type has any listeners attached to it.
+ // The verification can optionally be propagated, it will return `true` if parents have the listener attached to it.
+ listens: function (type, propagate) {
+ if (typeof type !== 'string') {
+ console.warn('"string" type argument expected');
+ }
+ var listeners = this._events && this._events[type];
+ if (listeners && listeners.length) { return true; }
+
+ if (propagate) {
+ // also check parents for listeners if event propagates
+ for (var id in this._eventParents) {
+ if (this._eventParents[id].listens(type, propagate)) { return true; }
+ }
+ }
+ return false;
+ },
+
+ // @method once(…): this
+ // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed.
+ once: function (types, fn, context) {
+
+ if (typeof types === 'object') {
+ for (var type in types) {
+ this.once(type, types[type], fn);
+ }
+ return this;
+ }
+
+ var handler = bind(function () {
+ this
+ .off(types, fn, context)
+ .off(types, handler, context);
+ }, this);
+
+ // add a listener that's executed once and removed after that
+ return this
+ .on(types, fn, context)
+ .on(types, handler, context);
+ },
+
+ // @method addEventParent(obj: Evented): this
+ // Adds an event parent - an `Evented` that will receive propagated events
+ addEventParent: function (obj) {
+ this._eventParents = this._eventParents || {};
+ this._eventParents[stamp(obj)] = obj;
+ return this;
+ },
+
+ // @method removeEventParent(obj: Evented): this
+ // Removes an event parent, so it will stop receiving propagated events
+ removeEventParent: function (obj) {
+ if (this._eventParents) {
+ delete this._eventParents[stamp(obj)];
+ }
+ return this;
+ },
+
+ _propagateEvent: function (e) {
+ for (var id in this._eventParents) {
+ this._eventParents[id].fire(e.type, extend({
+ layer: e.target,
+ propagatedFrom: e.target
+ }, e), true);
+ }
+ }
+};
+
+// aliases; we should ditch those eventually
+
+// @method addEventListener(…): this
+// Alias to [`on(…)`](#evented-on)
+Events.addEventListener = Events.on;
+
+// @method removeEventListener(…): this
+// Alias to [`off(…)`](#evented-off)
+
+// @method clearAllEventListeners(…): this
+// Alias to [`off()`](#evented-off)
+Events.removeEventListener = Events.clearAllEventListeners = Events.off;
+
+// @method addOneTimeEventListener(…): this
+// Alias to [`once(…)`](#evented-once)
+Events.addOneTimeEventListener = Events.once;
+
+// @method fireEvent(…): this
+// Alias to [`fire(…)`](#evented-fire)
+Events.fireEvent = Events.fire;
+
+// @method hasEventListeners(…): Boolean
+// Alias to [`listens(…)`](#evented-listens)
+Events.hasEventListeners = Events.listens;
+
+var Evented = Class.extend(Events);
+
+/*
+ * @class Point
+ * @aka L.Point
+ *
+ * Represents a point with `x` and `y` coordinates in pixels.
+ *
+ * @example
+ *
+ * ```js
+ * var point = L.point(200, 300);
+ * ```
+ *
+ * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent:
+ *
+ * ```js
+ * map.panBy([200, 300]);
+ * map.panBy(L.point(200, 300));
+ * ```
+ *
+ * Note that `Point` does not inherit from Leaflet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function Point(x, y, round) {
+ // @property x: Number; The `x` coordinate of the point
+ this.x = (round ? Math.round(x) : x);
+ // @property y: Number; The `y` coordinate of the point
+ this.y = (round ? Math.round(y) : y);
+}
+
+var trunc = Math.trunc || function (v) {
+ return v > 0 ? Math.floor(v) : Math.ceil(v);
+};
+
+Point.prototype = {
+
+ // @method clone(): Point
+ // Returns a copy of the current point.
+ clone: function () {
+ return new Point(this.x, this.y);
+ },
+
+ // @method add(otherPoint: Point): Point
+ // Returns the result of addition of the current and the given points.
+ add: function (point) {
+ // non-destructive, returns a new point
+ return this.clone()._add(toPoint(point));
+ },
+
+ _add: function (point) {
+ // destructive, used directly for performance in situations where it's safe to modify existing point
+ this.x += point.x;
+ this.y += point.y;
+ return this;
+ },
+
+ // @method subtract(otherPoint: Point): Point
+ // Returns the result of subtraction of the given point from the current.
+ subtract: function (point) {
+ return this.clone()._subtract(toPoint(point));
+ },
+
+ _subtract: function (point) {
+ this.x -= point.x;
+ this.y -= point.y;
+ return this;
+ },
+
+ // @method divideBy(num: Number): Point
+ // Returns the result of division of the current point by the given number.
+ divideBy: function (num) {
+ return this.clone()._divideBy(num);
+ },
+
+ _divideBy: function (num) {
+ this.x /= num;
+ this.y /= num;
+ return this;
+ },
+
+ // @method multiplyBy(num: Number): Point
+ // Returns the result of multiplication of the current point by the given number.
+ multiplyBy: function (num) {
+ return this.clone()._multiplyBy(num);
+ },
+
+ _multiplyBy: function (num) {
+ this.x *= num;
+ this.y *= num;
+ return this;
+ },
+
+ // @method scaleBy(scale: Point): Point
+ // Multiply each coordinate of the current point by each coordinate of
+ // `scale`. In linear algebra terms, multiply the point by the
+ // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)
+ // defined by `scale`.
+ scaleBy: function (point) {
+ return new Point(this.x * point.x, this.y * point.y);
+ },
+
+ // @method unscaleBy(scale: Point): Point
+ // Inverse of `scaleBy`. Divide each coordinate of the current point by
+ // each coordinate of `scale`.
+ unscaleBy: function (point) {
+ return new Point(this.x / point.x, this.y / point.y);
+ },
+
+ // @method round(): Point
+ // Returns a copy of the current point with rounded coordinates.
+ round: function () {
+ return this.clone()._round();
+ },
+
+ _round: function () {
+ this.x = Math.round(this.x);
+ this.y = Math.round(this.y);
+ return this;
+ },
+
+ // @method floor(): Point
+ // Returns a copy of the current point with floored coordinates (rounded down).
+ floor: function () {
+ return this.clone()._floor();
+ },
+
+ _floor: function () {
+ this.x = Math.floor(this.x);
+ this.y = Math.floor(this.y);
+ return this;
+ },
+
+ // @method ceil(): Point
+ // Returns a copy of the current point with ceiled coordinates (rounded up).
+ ceil: function () {
+ return this.clone()._ceil();
+ },
+
+ _ceil: function () {
+ this.x = Math.ceil(this.x);
+ this.y = Math.ceil(this.y);
+ return this;
+ },
+
+ // @method trunc(): Point
+ // Returns a copy of the current point with truncated coordinates (rounded towards zero).
+ trunc: function () {
+ return this.clone()._trunc();
+ },
+
+ _trunc: function () {
+ this.x = trunc(this.x);
+ this.y = trunc(this.y);
+ return this;
+ },
+
+ // @method distanceTo(otherPoint: Point): Number
+ // Returns the cartesian distance between the current and the given points.
+ distanceTo: function (point) {
+ point = toPoint(point);
+
+ var x = point.x - this.x,
+ y = point.y - this.y;
+
+ return Math.sqrt(x * x + y * y);
+ },
+
+ // @method equals(otherPoint: Point): Boolean
+ // Returns `true` if the given point has the same coordinates.
+ equals: function (point) {
+ point = toPoint(point);
+
+ return point.x === this.x &&
+ point.y === this.y;
+ },
+
+ // @method contains(otherPoint: Point): Boolean
+ // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).
+ contains: function (point) {
+ point = toPoint(point);
+
+ return Math.abs(point.x) <= Math.abs(this.x) &&
+ Math.abs(point.y) <= Math.abs(this.y);
+ },
+
+ // @method toString(): String
+ // Returns a string representation of the point for debugging purposes.
+ toString: function () {
+ return 'Point(' +
+ formatNum(this.x) + ', ' +
+ formatNum(this.y) + ')';
+ }
+};
+
+// @factory L.point(x: Number, y: Number, round?: Boolean)
+// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.
+
+// @alternative
+// @factory L.point(coords: Number[])
+// Expects an array of the form `[x, y]` instead.
+
+// @alternative
+// @factory L.point(coords: Object)
+// Expects a plain object of the form `{x: Number, y: Number}` instead.
+function toPoint(x, y, round) {
+ if (x instanceof Point) {
+ return x;
+ }
+ if (isArray(x)) {
+ return new Point(x[0], x[1]);
+ }
+ if (x === undefined || x === null) {
+ return x;
+ }
+ if (typeof x === 'object' && 'x' in x && 'y' in x) {
+ return new Point(x.x, x.y);
+ }
+ return new Point(x, y, round);
+}
+
+/*
+ * @class Bounds
+ * @aka L.Bounds
+ *
+ * Represents a rectangular area in pixel coordinates.
+ *
+ * @example
+ *
+ * ```js
+ * var p1 = L.point(10, 10),
+ * p2 = L.point(40, 60),
+ * bounds = L.bounds(p1, p2);
+ * ```
+ *
+ * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
+ *
+ * ```js
+ * otherBounds.intersects([[10, 10], [40, 60]]);
+ * ```
+ *
+ * Note that `Bounds` does not inherit from Leaflet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function Bounds(a, b) {
+ if (!a) { return; }
+
+ var points = b ? [a, b] : a;
+
+ for (var i = 0, len = points.length; i < len; i++) {
+ this.extend(points[i]);
+ }
+}
+
+Bounds.prototype = {
+ // @method extend(point: Point): this
+ // Extends the bounds to contain the given point.
+ extend: function (point) { // (Point)
+ point = toPoint(point);
+
+ // @property min: Point
+ // The top left corner of the rectangle.
+ // @property max: Point
+ // The bottom right corner of the rectangle.
+ if (!this.min && !this.max) {
+ this.min = point.clone();
+ this.max = point.clone();
+ } else {
+ this.min.x = Math.min(point.x, this.min.x);
+ this.max.x = Math.max(point.x, this.max.x);
+ this.min.y = Math.min(point.y, this.min.y);
+ this.max.y = Math.max(point.y, this.max.y);
+ }
+ return this;
+ },
+
+ // @method getCenter(round?: Boolean): Point
+ // Returns the center point of the bounds.
+ getCenter: function (round) {
+ return new Point(
+ (this.min.x + this.max.x) / 2,
+ (this.min.y + this.max.y) / 2, round);
+ },
+
+ // @method getBottomLeft(): Point
+ // Returns the bottom-left point of the bounds.
+ getBottomLeft: function () {
+ return new Point(this.min.x, this.max.y);
+ },
+
+ // @method getTopRight(): Point
+ // Returns the top-right point of the bounds.
+ getTopRight: function () { // -> Point
+ return new Point(this.max.x, this.min.y);
+ },
+
+ // @method getTopLeft(): Point
+ // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)).
+ getTopLeft: function () {
+ return this.min; // left, top
+ },
+
+ // @method getBottomRight(): Point
+ // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)).
+ getBottomRight: function () {
+ return this.max; // right, bottom
+ },
+
+ // @method getSize(): Point
+ // Returns the size of the given bounds
+ getSize: function () {
+ return this.max.subtract(this.min);
+ },
+
+ // @method contains(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle contains the given one.
+ // @alternative
+ // @method contains(point: Point): Boolean
+ // Returns `true` if the rectangle contains the given point.
+ contains: function (obj) {
+ var min, max;
+
+ if (typeof obj[0] === 'number' || obj instanceof Point) {
+ obj = toPoint(obj);
+ } else {
+ obj = toBounds(obj);
+ }
+
+ if (obj instanceof Bounds) {
+ min = obj.min;
+ max = obj.max;
+ } else {
+ min = max = obj;
+ }
+
+ return (min.x >= this.min.x) &&
+ (max.x <= this.max.x) &&
+ (min.y >= this.min.y) &&
+ (max.y <= this.max.y);
+ },
+
+ // @method intersects(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle intersects the given bounds. Two bounds
+ // intersect if they have at least one point in common.
+ intersects: function (bounds) { // (Bounds) -> Boolean
+ bounds = toBounds(bounds);
+
+ var min = this.min,
+ max = this.max,
+ min2 = bounds.min,
+ max2 = bounds.max,
+ xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
+ yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
+
+ return xIntersects && yIntersects;
+ },
+
+ // @method overlaps(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle overlaps the given bounds. Two bounds
+ // overlap if their intersection is an area.
+ overlaps: function (bounds) { // (Bounds) -> Boolean
+ bounds = toBounds(bounds);
+
+ var min = this.min,
+ max = this.max,
+ min2 = bounds.min,
+ max2 = bounds.max,
+ xOverlaps = (max2.x > min.x) && (min2.x < max.x),
+ yOverlaps = (max2.y > min.y) && (min2.y < max.y);
+
+ return xOverlaps && yOverlaps;
+ },
+
+ isValid: function () {
+ return !!(this.min && this.max);
+ }
+};
+
+
+// @factory L.bounds(corner1: Point, corner2: Point)
+// Creates a Bounds object from two corners coordinate pairs.
+// @alternative
+// @factory L.bounds(points: Point[])
+// Creates a Bounds object from the given array of points.
+function toBounds(a, b) {
+ if (!a || a instanceof Bounds) {
+ return a;
+ }
+ return new Bounds(a, b);
+}
+
+/*
+ * @class LatLngBounds
+ * @aka L.LatLngBounds
+ *
+ * Represents a rectangular geographical area on a map.
+ *
+ * @example
+ *
+ * ```js
+ * var corner1 = L.latLng(40.712, -74.227),
+ * corner2 = L.latLng(40.774, -74.125),
+ * bounds = L.latLngBounds(corner1, corner2);
+ * ```
+ *
+ * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
+ *
+ * ```js
+ * map.fitBounds([
+ * [40.712, -74.227],
+ * [40.774, -74.125]
+ * ]);
+ * ```
+ *
+ * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range.
+ *
+ * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[])
+ if (!corner1) { return; }
+
+ var latlngs = corner2 ? [corner1, corner2] : corner1;
+
+ for (var i = 0, len = latlngs.length; i < len; i++) {
+ this.extend(latlngs[i]);
+ }
+}
+
+LatLngBounds.prototype = {
+
+ // @method extend(latlng: LatLng): this
+ // Extend the bounds to contain the given point
+
+ // @alternative
+ // @method extend(otherBounds: LatLngBounds): this
+ // Extend the bounds to contain the given bounds
+ extend: function (obj) {
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2, ne2;
+
+ if (obj instanceof LatLng) {
+ sw2 = obj;
+ ne2 = obj;
+
+ } else if (obj instanceof LatLngBounds) {
+ sw2 = obj._southWest;
+ ne2 = obj._northEast;
+
+ if (!sw2 || !ne2) { return this; }
+
+ } else {
+ return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this;
+ }
+
+ if (!sw && !ne) {
+ this._southWest = new LatLng(sw2.lat, sw2.lng);
+ this._northEast = new LatLng(ne2.lat, ne2.lng);
+ } else {
+ sw.lat = Math.min(sw2.lat, sw.lat);
+ sw.lng = Math.min(sw2.lng, sw.lng);
+ ne.lat = Math.max(ne2.lat, ne.lat);
+ ne.lng = Math.max(ne2.lng, ne.lng);
+ }
+
+ return this;
+ },
+
+ // @method pad(bufferRatio: Number): LatLngBounds
+ // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.
+ // For example, a ratio of 0.5 extends the bounds by 50% in each direction.
+ // Negative values will retract the bounds.
+ pad: function (bufferRatio) {
+ var sw = this._southWest,
+ ne = this._northEast,
+ heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
+ widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
+
+ return new LatLngBounds(
+ new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
+ new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
+ },
+
+ // @method getCenter(): LatLng
+ // Returns the center point of the bounds.
+ getCenter: function () {
+ return new LatLng(
+ (this._southWest.lat + this._northEast.lat) / 2,
+ (this._southWest.lng + this._northEast.lng) / 2);
+ },
+
+ // @method getSouthWest(): LatLng
+ // Returns the south-west point of the bounds.
+ getSouthWest: function () {
+ return this._southWest;
+ },
+
+ // @method getNorthEast(): LatLng
+ // Returns the north-east point of the bounds.
+ getNorthEast: function () {
+ return this._northEast;
+ },
+
+ // @method getNorthWest(): LatLng
+ // Returns the north-west point of the bounds.
+ getNorthWest: function () {
+ return new LatLng(this.getNorth(), this.getWest());
+ },
+
+ // @method getSouthEast(): LatLng
+ // Returns the south-east point of the bounds.
+ getSouthEast: function () {
+ return new LatLng(this.getSouth(), this.getEast());
+ },
+
+ // @method getWest(): Number
+ // Returns the west longitude of the bounds
+ getWest: function () {
+ return this._southWest.lng;
+ },
+
+ // @method getSouth(): Number
+ // Returns the south latitude of the bounds
+ getSouth: function () {
+ return this._southWest.lat;
+ },
+
+ // @method getEast(): Number
+ // Returns the east longitude of the bounds
+ getEast: function () {
+ return this._northEast.lng;
+ },
+
+ // @method getNorth(): Number
+ // Returns the north latitude of the bounds
+ getNorth: function () {
+ return this._northEast.lat;
+ },
+
+ // @method contains(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle contains the given one.
+
+ // @alternative
+ // @method contains (latlng: LatLng): Boolean
+ // Returns `true` if the rectangle contains the given point.
+ contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
+ if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) {
+ obj = toLatLng(obj);
+ } else {
+ obj = toLatLngBounds(obj);
+ }
+
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2, ne2;
+
+ if (obj instanceof LatLngBounds) {
+ sw2 = obj.getSouthWest();
+ ne2 = obj.getNorthEast();
+ } else {
+ sw2 = ne2 = obj;
+ }
+
+ return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
+ (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
+ },
+
+ // @method intersects(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common.
+ intersects: function (bounds) {
+ bounds = toLatLngBounds(bounds);
+
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2 = bounds.getSouthWest(),
+ ne2 = bounds.getNorthEast(),
+
+ latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
+ lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
+
+ return latIntersects && lngIntersects;
+ },
+
+ // @method overlaps(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area.
+ overlaps: function (bounds) {
+ bounds = toLatLngBounds(bounds);
+
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2 = bounds.getSouthWest(),
+ ne2 = bounds.getNorthEast(),
+
+ latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat),
+ lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng);
+
+ return latOverlaps && lngOverlaps;
+ },
+
+ // @method toBBoxString(): String
+ // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data.
+ toBBoxString: function () {
+ return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
+ },
+
+ // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean
+ // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number.
+ equals: function (bounds, maxMargin) {
+ if (!bounds) { return false; }
+
+ bounds = toLatLngBounds(bounds);
+
+ return this._southWest.equals(bounds.getSouthWest(), maxMargin) &&
+ this._northEast.equals(bounds.getNorthEast(), maxMargin);
+ },
+
+ // @method isValid(): Boolean
+ // Returns `true` if the bounds are properly initialized.
+ isValid: function () {
+ return !!(this._southWest && this._northEast);
+ }
+};
+
+// TODO International date line?
+
+// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng)
+// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle.
+
+// @alternative
+// @factory L.latLngBounds(latlngs: LatLng[])
+// Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds).
+function toLatLngBounds(a, b) {
+ if (a instanceof LatLngBounds) {
+ return a;
+ }
+ return new LatLngBounds(a, b);
+}
+
+/* @class LatLng
+ * @aka L.LatLng
+ *
+ * Represents a geographical point with a certain latitude and longitude.
+ *
+ * @example
+ *
+ * ```
+ * var latlng = L.latLng(50.5, 30.5);
+ * ```
+ *
+ * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent:
+ *
+ * ```
+ * map.panTo([50, 30]);
+ * map.panTo({lon: 30, lat: 50});
+ * map.panTo({lat: 50, lng: 30});
+ * map.panTo(L.latLng(50, 30));
+ * ```
+ *
+ * Note that `LatLng` does not inherit from Leaflet's `Class` object,
+ * which means new classes can't inherit from it, and new methods
+ * can't be added to it with the `include` function.
+ */
+
+function LatLng(lat, lng, alt) {
+ if (isNaN(lat) || isNaN(lng)) {
+ throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
+ }
+
+ // @property lat: Number
+ // Latitude in degrees
+ this.lat = +lat;
+
+ // @property lng: Number
+ // Longitude in degrees
+ this.lng = +lng;
+
+ // @property alt: Number
+ // Altitude in meters (optional)
+ if (alt !== undefined) {
+ this.alt = +alt;
+ }
+}
+
+LatLng.prototype = {
+ // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean
+ // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number.
+ equals: function (obj, maxMargin) {
+ if (!obj) { return false; }
+
+ obj = toLatLng(obj);
+
+ var margin = Math.max(
+ Math.abs(this.lat - obj.lat),
+ Math.abs(this.lng - obj.lng));
+
+ return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin);
+ },
+
+ // @method toString(): String
+ // Returns a string representation of the point (for debugging purposes).
+ toString: function (precision) {
+ return 'LatLng(' +
+ formatNum(this.lat, precision) + ', ' +
+ formatNum(this.lng, precision) + ')';
+ },
+
+ // @method distanceTo(otherLatLng: LatLng): Number
+ // Returns the distance (in meters) to the given `LatLng` calculated using the [Spherical Law of Cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines).
+ distanceTo: function (other) {
+ return Earth.distance(this, toLatLng(other));
+ },
+
+ // @method wrap(): LatLng
+ // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees.
+ wrap: function () {
+ return Earth.wrapLatLng(this);
+ },
+
+ // @method toBounds(sizeInMeters: Number): LatLngBounds
+ // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`.
+ toBounds: function (sizeInMeters) {
+ var latAccuracy = 180 * sizeInMeters / 40075017,
+ lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
+
+ return toLatLngBounds(
+ [this.lat - latAccuracy, this.lng - lngAccuracy],
+ [this.lat + latAccuracy, this.lng + lngAccuracy]);
+ },
+
+ clone: function () {
+ return new LatLng(this.lat, this.lng, this.alt);
+ }
+};
+
+
+
+// @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng
+// Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude).
+
+// @alternative
+// @factory L.latLng(coords: Array): LatLng
+// Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead.
+
+// @alternative
+// @factory L.latLng(coords: Object): LatLng
+// Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead.
+
+function toLatLng(a, b, c) {
+ if (a instanceof LatLng) {
+ return a;
+ }
+ if (isArray(a) && typeof a[0] !== 'object') {
+ if (a.length === 3) {
+ return new LatLng(a[0], a[1], a[2]);
+ }
+ if (a.length === 2) {
+ return new LatLng(a[0], a[1]);
+ }
+ return null;
+ }
+ if (a === undefined || a === null) {
+ return a;
+ }
+ if (typeof a === 'object' && 'lat' in a) {
+ return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt);
+ }
+ if (b === undefined) {
+ return null;
+ }
+ return new LatLng(a, b, c);
+}
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.Base
+ * Object that defines coordinate reference systems for projecting
+ * geographical points into pixel (screen) coordinates and back (and to
+ * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See
+ * [spatial reference system](https://en.wikipedia.org/wiki/Spatial_reference_system).
+ *
+ * Leaflet defines the most usual CRSs by default. If you want to use a
+ * CRS not defined by default, take a look at the
+ * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin.
+ *
+ * Note that the CRS instances do not inherit from Leaflet's `Class` object,
+ * and can't be instantiated. Also, new classes can't inherit from them,
+ * and methods can't be added to them with the `include` function.
+ */
+
+var CRS = {
+ // @method latLngToPoint(latlng: LatLng, zoom: Number): Point
+ // Projects geographical coordinates into pixel coordinates for a given zoom.
+ latLngToPoint: function (latlng, zoom) {
+ var projectedPoint = this.projection.project(latlng),
+ scale = this.scale(zoom);
+
+ return this.transformation._transform(projectedPoint, scale);
+ },
+
+ // @method pointToLatLng(point: Point, zoom: Number): LatLng
+ // The inverse of `latLngToPoint`. Projects pixel coordinates on a given
+ // zoom into geographical coordinates.
+ pointToLatLng: function (point, zoom) {
+ var scale = this.scale(zoom),
+ untransformedPoint = this.transformation.untransform(point, scale);
+
+ return this.projection.unproject(untransformedPoint);
+ },
+
+ // @method project(latlng: LatLng): Point
+ // Projects geographical coordinates into coordinates in units accepted for
+ // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services).
+ project: function (latlng) {
+ return this.projection.project(latlng);
+ },
+
+ // @method unproject(point: Point): LatLng
+ // Given a projected coordinate returns the corresponding LatLng.
+ // The inverse of `project`.
+ unproject: function (point) {
+ return this.projection.unproject(point);
+ },
+
+ // @method scale(zoom: Number): Number
+ // Returns the scale used when transforming projected coordinates into
+ // pixel coordinates for a particular zoom. For example, it returns
+ // `256 * 2^zoom` for Mercator-based CRS.
+ scale: function (zoom) {
+ return 256 * Math.pow(2, zoom);
+ },
+
+ // @method zoom(scale: Number): Number
+ // Inverse of `scale()`, returns the zoom level corresponding to a scale
+ // factor of `scale`.
+ zoom: function (scale) {
+ return Math.log(scale / 256) / Math.LN2;
+ },
+
+ // @method getProjectedBounds(zoom: Number): Bounds
+ // Returns the projection's bounds scaled and transformed for the provided `zoom`.
+ getProjectedBounds: function (zoom) {
+ if (this.infinite) { return null; }
+
+ var b = this.projection.bounds,
+ s = this.scale(zoom),
+ min = this.transformation.transform(b.min, s),
+ max = this.transformation.transform(b.max, s);
+
+ return new Bounds(min, max);
+ },
+
+ // @method distance(latlng1: LatLng, latlng2: LatLng): Number
+ // Returns the distance between two geographical coordinates.
+
+ // @property code: String
+ // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`)
+ //
+ // @property wrapLng: Number[]
+ // An array of two numbers defining whether the longitude (horizontal) coordinate
+ // axis wraps around a given range and how. Defaults to `[-180, 180]` in most
+ // geographical CRSs. If `undefined`, the longitude axis does not wrap around.
+ //
+ // @property wrapLat: Number[]
+ // Like `wrapLng`, but for the latitude (vertical) axis.
+
+ // wrapLng: [min, max],
+ // wrapLat: [min, max],
+
+ // @property infinite: Boolean
+ // If true, the coordinate space will be unbounded (infinite in both axes)
+ infinite: false,
+
+ // @method wrapLatLng(latlng: LatLng): LatLng
+ // Returns a `LatLng` where lat and lng has been wrapped according to the
+ // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds.
+ wrapLatLng: function (latlng) {
+ var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng,
+ lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat,
+ alt = latlng.alt;
+
+ return new LatLng(lat, lng, alt);
+ },
+
+ // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
+ // Returns a `LatLngBounds` with the same size as the given one, ensuring
+ // that its center is within the CRS's bounds.
+ // Only accepts actual `L.LatLngBounds` instances, not arrays.
+ wrapLatLngBounds: function (bounds) {
+ var center = bounds.getCenter(),
+ newCenter = this.wrapLatLng(center),
+ latShift = center.lat - newCenter.lat,
+ lngShift = center.lng - newCenter.lng;
+
+ if (latShift === 0 && lngShift === 0) {
+ return bounds;
+ }
+
+ var sw = bounds.getSouthWest(),
+ ne = bounds.getNorthEast(),
+ newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift),
+ newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift);
+
+ return new LatLngBounds(newSw, newNe);
+ }
+};
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.Earth
+ *
+ * Serves as the base for CRS that are global such that they cover the earth.
+ * Can only be used as the base for other CRS and cannot be used directly,
+ * since it does not have a `code`, `projection` or `transformation`. `distance()` returns
+ * meters.
+ */
+
+var Earth = extend({}, CRS, {
+ wrapLng: [-180, 180],
+
+ // Mean Earth Radius, as recommended for use by
+ // the International Union of Geodesy and Geophysics,
+ // see https://rosettacode.org/wiki/Haversine_formula
+ R: 6371000,
+
+ // distance between two geographical points using spherical law of cosines approximation
+ distance: function (latlng1, latlng2) {
+ var rad = Math.PI / 180,
+ lat1 = latlng1.lat * rad,
+ lat2 = latlng2.lat * rad,
+ sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2),
+ sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2),
+ a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon,
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return this.R * c;
+ }
+});
+
+/*
+ * @namespace Projection
+ * @projection L.Projection.SphericalMercator
+ *
+ * Spherical Mercator projection — the most common projection for online maps,
+ * used by almost all free and commercial tile providers. Assumes that Earth is
+ * a sphere. Used by the `EPSG:3857` CRS.
+ */
+
+var earthRadius = 6378137;
+
+var SphericalMercator = {
+
+ R: earthRadius,
+ MAX_LATITUDE: 85.0511287798,
+
+ project: function (latlng) {
+ var d = Math.PI / 180,
+ max = this.MAX_LATITUDE,
+ lat = Math.max(Math.min(max, latlng.lat), -max),
+ sin = Math.sin(lat * d);
+
+ return new Point(
+ this.R * latlng.lng * d,
+ this.R * Math.log((1 + sin) / (1 - sin)) / 2);
+ },
+
+ unproject: function (point) {
+ var d = 180 / Math.PI;
+
+ return new LatLng(
+ (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d,
+ point.x * d / this.R);
+ },
+
+ bounds: (function () {
+ var d = earthRadius * Math.PI;
+ return new Bounds([-d, -d], [d, d]);
+ })()
+};
+
+/*
+ * @class Transformation
+ * @aka L.Transformation
+ *
+ * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d`
+ * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing
+ * the reverse. Used by Leaflet in its projections code.
+ *
+ * @example
+ *
+ * ```js
+ * var transformation = L.transformation(2, 5, -1, 10),
+ * p = L.point(1, 2),
+ * p2 = transformation.transform(p), // L.point(7, 8)
+ * p3 = transformation.untransform(p2); // L.point(1, 2)
+ * ```
+ */
+
+
+// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)
+// Creates a `Transformation` object with the given coefficients.
+function Transformation(a, b, c, d) {
+ if (isArray(a)) {
+ // use array properties
+ this._a = a[0];
+ this._b = a[1];
+ this._c = a[2];
+ this._d = a[3];
+ return;
+ }
+ this._a = a;
+ this._b = b;
+ this._c = c;
+ this._d = d;
+}
+
+Transformation.prototype = {
+ // @method transform(point: Point, scale?: Number): Point
+ // Returns a transformed point, optionally multiplied by the given scale.
+ // Only accepts actual `L.Point` instances, not arrays.
+ transform: function (point, scale) { // (Point, Number) -> Point
+ return this._transform(point.clone(), scale);
+ },
+
+ // destructive transform (faster)
+ _transform: function (point, scale) {
+ scale = scale || 1;
+ point.x = scale * (this._a * point.x + this._b);
+ point.y = scale * (this._c * point.y + this._d);
+ return point;
+ },
+
+ // @method untransform(point: Point, scale?: Number): Point
+ // Returns the reverse transformation of the given point, optionally divided
+ // by the given scale. Only accepts actual `L.Point` instances, not arrays.
+ untransform: function (point, scale) {
+ scale = scale || 1;
+ return new Point(
+ (point.x / scale - this._b) / this._a,
+ (point.y / scale - this._d) / this._c);
+ }
+};
+
+// factory L.transformation(a: Number, b: Number, c: Number, d: Number)
+
+// @factory L.transformation(a: Number, b: Number, c: Number, d: Number)
+// Instantiates a Transformation object with the given coefficients.
+
+// @alternative
+// @factory L.transformation(coefficients: Array): Transformation
+// Expects an coefficients array of the form
+// `[a: Number, b: Number, c: Number, d: Number]`.
+
+function toTransformation(a, b, c, d) {
+ return new Transformation(a, b, c, d);
+}
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.EPSG3857
+ *
+ * The most common CRS for online maps, used by almost all free and commercial
+ * tile providers. Uses Spherical Mercator projection. Set in by default in
+ * Map's `crs` option.
+ */
+
+var EPSG3857 = extend({}, Earth, {
+ code: 'EPSG:3857',
+ projection: SphericalMercator,
+
+ transformation: (function () {
+ var scale = 0.5 / (Math.PI * SphericalMercator.R);
+ return toTransformation(scale, 0.5, -scale, 0.5);
+ }())
+});
+
+var EPSG900913 = extend({}, EPSG3857, {
+ code: 'EPSG:900913'
+});
+
+// @namespace SVG; @section
+// There are several static functions which can be called without instantiating L.SVG:
+
+// @function create(name: String): SVGElement
+// Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement),
+// corresponding to the class name passed. For example, using 'line' will return
+// an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement).
+function svgCreate(name) {
+ return document.createElementNS('http://www.w3.org/2000/svg', name);
+}
+
+// @function pointsToPath(rings: Point[], closed: Boolean): String
+// Generates a SVG path string for multiple rings, with each ring turning
+// into "M..L..L.." instructions
+function pointsToPath(rings, closed) {
+ var str = '',
+ i, j, len, len2, points, p;
+
+ for (i = 0, len = rings.length; i < len; i++) {
+ points = rings[i];
+
+ for (j = 0, len2 = points.length; j < len2; j++) {
+ p = points[j];
+ str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
+ }
+
+ // closes the ring for polygons; "x" is VML syntax
+ str += closed ? (Browser.svg ? 'z' : 'x') : '';
+ }
+
+ // SVG complains about empty path strings
+ return str || 'M0 0';
+}
+
+/*
+ * @namespace Browser
+ * @aka L.Browser
+ *
+ * A namespace with static properties for browser/feature detection used by Leaflet internally.
+ *
+ * @example
+ *
+ * ```js
+ * if (L.Browser.ielt9) {
+ * alert('Upgrade your browser, dude!');
+ * }
+ * ```
+ */
+
+var style = document.documentElement.style;
+
+// @property ie: Boolean; `true` for all Internet Explorer versions (not Edge).
+var ie = 'ActiveXObject' in window;
+
+// @property ielt9: Boolean; `true` for Internet Explorer versions less than 9.
+var ielt9 = ie && !document.addEventListener;
+
+// @property edge: Boolean; `true` for the Edge web browser.
+var edge = 'msLaunchUri' in navigator && !('documentMode' in document);
+
+// @property webkit: Boolean;
+// `true` for webkit-based browsers like Chrome and Safari (including mobile versions).
+var webkit = userAgentContains('webkit');
+
+// @property android: Boolean
+// **Deprecated.** `true` for any browser running on an Android platform.
+var android = userAgentContains('android');
+
+// @property android23: Boolean; **Deprecated.** `true` for browsers running on Android 2 or Android 3.
+var android23 = userAgentContains('android 2') || userAgentContains('android 3');
+
+/* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */
+var webkitVer = parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit
+// @property androidStock: Boolean; **Deprecated.** `true` for the Android stock browser (i.e. not Chrome)
+var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window);
+
+// @property opera: Boolean; `true` for the Opera browser
+var opera = !!window.opera;
+
+// @property chrome: Boolean; `true` for the Chrome browser.
+var chrome = !edge && userAgentContains('chrome');
+
+// @property gecko: Boolean; `true` for gecko-based browsers like Firefox.
+var gecko = userAgentContains('gecko') && !webkit && !opera && !ie;
+
+// @property safari: Boolean; `true` for the Safari browser.
+var safari = !chrome && userAgentContains('safari');
+
+var phantom = userAgentContains('phantom');
+
+// @property opera12: Boolean
+// `true` for the Opera browser supporting CSS transforms (version 12 or later).
+var opera12 = 'OTransition' in style;
+
+// @property win: Boolean; `true` when the browser is running in a Windows platform
+var win = navigator.platform.indexOf('Win') === 0;
+
+// @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms.
+var ie3d = ie && ('transition' in style);
+
+// @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms.
+var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23;
+
+// @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms.
+var gecko3d = 'MozPerspective' in style;
+
+// @property any3d: Boolean
+// `true` for all browsers supporting CSS transforms.
+var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom;
+
+// @property mobile: Boolean; `true` for all browsers running in a mobile device.
+var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile');
+
+// @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device.
+var mobileWebkit = mobile && webkit;
+
+// @property mobileWebkit3d: Boolean
+// `true` for all webkit-based browsers in a mobile device supporting CSS transforms.
+var mobileWebkit3d = mobile && webkit3d;
+
+// @property msPointer: Boolean
+// `true` for browsers implementing the Microsoft touch events model (notably IE10).
+var msPointer = !window.PointerEvent && window.MSPointerEvent;
+
+// @property pointer: Boolean
+// `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx).
+var pointer = !!(window.PointerEvent || msPointer);
+
+// @property touchNative: Boolean
+// `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events).
+// **This does not necessarily mean** that the browser is running in a computer with
+// a touchscreen, it only means that the browser is capable of understanding
+// touch events.
+var touchNative = 'ontouchstart' in window || !!window.TouchEvent;
+
+// @property touch: Boolean
+// `true` for all browsers supporting either [touch](#browser-touch) or [pointer](#browser-pointer) events.
+// Note: pointer events will be preferred (if available), and processed for all `touch*` listeners.
+var touch = !window.L_NO_TOUCH && (touchNative || pointer);
+
+// @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device.
+var mobileOpera = mobile && opera;
+
+// @property mobileGecko: Boolean
+// `true` for gecko-based browsers running in a mobile device.
+var mobileGecko = mobile && gecko;
+
+// @property retina: Boolean
+// `true` for browsers on a high-resolution "retina" screen or on any screen when browser's display zoom is more than 100%.
+var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1;
+
+// @property passiveEvents: Boolean
+// `true` for browsers that support passive events.
+var passiveEvents = (function () {
+ var supportsPassiveOption = false;
+ try {
+ var opts = Object.defineProperty({}, 'passive', {
+ get: function () { // eslint-disable-line getter-return
+ supportsPassiveOption = true;
+ }
+ });
+ window.addEventListener('testPassiveEventSupport', falseFn, opts);
+ window.removeEventListener('testPassiveEventSupport', falseFn, opts);
+ } catch (e) {
+ // Errors can safely be ignored since this is only a browser support test.
+ }
+ return supportsPassiveOption;
+}());
+
+// @property canvas: Boolean
+// `true` when the browser supports [`