diff --git a/src/app.py b/src/app.py
index f4410e0..456af98 100644
--- a/src/app.py
+++ b/src/app.py
@@ -1,6 +1,7 @@
import flask
from flask_minify import Minify
import json
+from tasks import TaskHandler
proj = json.load(open("./static/json/projects.json", "r"))
books = json.load(open("./static/json/books.json", "r"))
@@ -12,6 +13,7 @@ pages['home']['books'] = books
pages['books']['books'] = books
app = flask.Flask(__name__)
+tasks = TaskHandler()
@app.route('/api/goto/')
@@ -33,6 +35,18 @@ for i in pages:
def resume():
return flask.send_file("./static/Resume.pdf")
+@app.route("/hotspots")
+def hotspotsRIT():
+ return flask.render_template("hotspots.html")
+
+@app.route("/hotspotsrit/cached")
+def getCached():
+ return json.dumps(tasks.getCache())
+
+@app.route("/hotspotsrit/current")
+def getLive():
+ return json.dumps(tasks.getCurrent())
+
@app.errorhandler(Exception)
def page404(e):
diff --git a/src/static/css/hotspots.css b/src/static/css/hotspots.css
new file mode 100644
index 0000000..8dfd0a6
--- /dev/null
+++ b/src/static/css/hotspots.css
@@ -0,0 +1,25 @@
+body {
+ margin: 0;
+ padding: 0;
+ overflow: visible;
+}
+
+#map {
+ height: 100vh;
+}
+
+html, body, #map {
+ height: 100%;
+ width: 100vw;
+}
+
+.leaflet-layer,
+.leaflet-control-zoom-in,
+.leaflet-control-zoom-out,
+.leaflet-control-attribution {
+ filter: brightness(500%) contrast(130%);
+}
+
+.leaflet-tile-pane {
+ filter: brightness(50%);
+}
\ No newline at end of file
diff --git a/src/static/js/hotspots.js b/src/static/js/hotspots.js
new file mode 100644
index 0000000..d0cf99a
--- /dev/null
+++ b/src/static/js/hotspots.js
@@ -0,0 +1,430 @@
+let map = L.map("map", {
+ zoomControl: false,
+ attributionControl: false,
+ }).setView([43.084679, -77.674702], 17);
+ // L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ // maxZoom: 19,
+ // attribution: '© OpenStreetMap'
+ // }).addTo(map);
+
+ var CartoDB_DarkMatterNoLabels = L.tileLayer(
+ "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png",
+ {
+ attribution:
+ '© OpenStreetMap contributors © CARTO',
+ subdomains: "abcd",
+ maxZoom: 20,
+ }
+ ).addTo(map);
+
+ // var CartoDB_PositronNoLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
+ // attribution: '© OpenStreetMap contributors © CARTO',
+ // subdomains: 'abcd',
+ // maxZoom: 20
+ // }).addTo(map); // good hacky filter: invert(100%) hue-rotate(180deg) brightness(100%) contrast(100%);
+
+ // var CartoDB_Positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
+ // attribution: '© OpenStreetMap contributors © CARTO',
+ // subdomains: 'abcd',
+ // maxZoom: 20
+ // }).addTo(map);
+
+ function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ function shuffle(array) {
+ let currentIndex = array.length,
+ randomIndex;
+
+ // While there remain elements to shuffle.
+ while (currentIndex > 0) {
+ // Pick a remaining element.
+ randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex--;
+
+ // And swap it with the current element.
+ [array[currentIndex], array[randomIndex]] = [
+ array[randomIndex],
+ array[currentIndex],
+ ];
+ }
+
+ return array;
+ }
+
+ function setMarker(attrs, ref) {
+ // let red = parseInt("ff", 16);
+ // let green = parseInt("78", 16);
+ // let style = {"fillColor": `#${red.toString(16)}${green.toString(16)}00`};
+ let ratio = attrs.properties.count / attrs.properties.capacity;
+ ratio = ratio > 1 ? 1 : ratio;
+ let red = 255 * ratio;
+ let style = { fillColor: `rgba(${red}, 0, 0, ${ratio})` };
+ ref.setStyle(style);
+ ref.bindPopup(
+ `${attrs.properties.name}
Current Occupation: ${attrs.properties.count}`
+ );
+ }
+
+ function onEachFeature(feature, layer) {
+ // does this feature have a property named popupContent?
+ if (!feature.properties || !feature.properties.name) {
+ return;
+ }
+ setMarker(feature, layer);
+ }
+
+ const polyStyle = {
+ color: "#ff7800",
+ weight: 5,
+ opacity: 0.65,
+ };
+
+ const geojsonMarkerOptions = {
+ radius: 8,
+ fillColor: "#ff7800",
+ color: "#ff7800",
+ weight: 3,
+ opacity: 1,
+ fillOpacity: 1,
+ };
+
+ const pointStyle = {};
+
+ function ritCustomize(input) {
+ badOnes = [166]; // Nathan's (166) is a duplicate of Ben and Jerry's
+ for (let i = input.length - 1; i >= 0; i--) {
+ if (badOnes.indexOf(input[i].mdo_id) >= 0) {
+ input.splice(i, 1);
+ }
+ }
+ return input;
+ }
+
+ // Unused: "Campus", "Gleason_Engineering_Student_Area"
+ const no_mdo_ids = {
+ Library_A_Level: { type: "Point", coordinates: [-77.676355, 43.083974] },
+ Library_1st_Floor: { type: "Point", coordinates: [-77.676355, 43.083874] },
+ Library_2nd_Floor: { type: "Point", coordinates: [-77.676355, 43.083774] },
+ Library_3rd_Floor: { type: "Point", coordinates: [-77.676355, 43.083674] },
+ Library_4th_Floor: { type: "Point", coordinates: [-77.676355, 43.083574] },
+ Ross_Hall: { type: "Point", coordinates: [-77.677937, 43.082379] },
+ Gordon_Field_House: { type: "Point", coordinates: [-77.671725, 43.085149] },
+ Golisano_Institute_for_Sustainability_Lobby: {
+ type: "Point",
+ coordinates: [-77.681365, 43.085376],
+ },
+ };
+
+ function ritCustomizeCoords(input) {
+ try {
+ if (input.properties.name == "Beanz") {
+ input.geometry.coordinates = [-77.66904, 43.083876];
+ }
+ return input;
+ } catch {}
+ try {
+ if (no_mdo_ids[input.location] == undefined) return;
+ let geojsonObj = {
+ geometry: no_mdo_ids[input.location],
+ properties: {
+ mdo_id: input.location,
+ name: input.location.replaceAll("_", " "),
+ },
+ type: "Feature",
+ };
+ return geojsonObj;
+ } catch {}
+ }
+
+ let pts;
+ const densityMapUrl = "/hotspotsrit" // https://maps.rit.edu/proxySearch/densityMapDetail.php?mdo=1
+ async function init() {
+ let counts = fetch(densityMapUrl + "/cached");
+
+ let locations = fetch(
+ "https://maps.rit.edu/proxySearch/locations.search.php"
+ );
+
+ counts = Object.values(await (await counts).json());
+ counts = ritCustomize(counts);
+ locations = await (await locations).json();
+
+ pts = {};
+ locations.forEach((x) => {
+ for (let i = 0; i < counts.length; i++) {
+ if (counts[i].mdo_id == x.properties.mdo_id) {
+ x.properties.count = counts[i].count;
+ x.properties.capacity = counts[i].max_occ == null ? 100 : counts[i].max_occ;
+ x = ritCustomizeCoords(x);
+ pts[x.properties.mdo_id] = x;
+ break;
+ }
+ }
+ });
+
+ counts.forEach((x) => {
+ if (pts[x.mdo_id] == undefined) {
+ let geojson = ritCustomizeCoords(x);
+ if (geojson !== undefined) {
+ geojson.properties.count = x.count;
+ geojson.properties.capacity = x.max_occ == null ? 100 : x.max_occ;
+ pts[x.location] = geojson;
+ }
+ }
+ });
+
+ let ptsLayer = L.geoJSON(Object.values(pts), {
+ pointToLayer: function (feature, latlng) {
+ return L.circleMarker(latlng, geojsonMarkerOptions);
+ },
+ style: function (feature) {
+ switch (feature.geometry.type) {
+ case "Polygon":
+ return polyStyle;
+ case "Point":
+ return pointStyle;
+ }
+ },
+ onEachFeature: onEachFeature,
+ }).addTo(map);
+
+ const features = ptsLayer.getLayers();
+ for (let i = 0; i < features.length; i++) {
+ const key = features[i].feature.properties.mdo_id;
+ pts[key].properties.reference = features[i];
+ }
+ }
+
+ // let laid = L.geoJson(pts).addTo(map)
+ // laid.remove()
+
+ let bullets = L.layerGroup([]);
+ async function shootVector(
+ from,
+ to,
+ { speed = 500, color = null, onlyAnimate = true, trail = true } = {}
+ ) {
+ options = {
+ onlyAnimate: onlyAnimate,
+ animate: {
+ duration: speed,
+ },
+ };
+ if (color) options.color = color;
+ const fromC = getCoordArray(from);
+ const toC = getCoordArray(to);
+ arcGen(fromC, toC, (options = options));
+
+ if (trail) {
+ options["color"] = "rgba(190, 95, 0, 0.2)";
+ options.fade = true;
+ options.fadeSpeed = 60000 * 15;
+ arcGen(fromC, toC, (options = options));
+ }
+ }
+
+ function getCoordArray(ref) {
+ if (ref.properties == undefined) return ref;
+ let coords;
+ try {
+ coords = ref.properties.reference.getLatLng();
+ } catch {
+ coords = ref.properties.reference.getBounds().getCenter();
+ }
+ return [coords.lat, coords.lng];
+ }
+
+ function arcGen(latlng1, latlng2, options = {}) {
+ var latlngs = [];
+
+ var offsetX = latlng2[1] - latlng1[1],
+ offsetY = latlng2[0] - latlng1[0];
+
+ var r = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2)),
+ theta = Math.atan2(offsetY, offsetX);
+
+ var thetaOffset = 3.14 / 10;
+
+ var r2 = r / 2 / Math.cos(thetaOffset),
+ theta2 = theta + thetaOffset;
+
+ var midpointX = r2 * Math.cos(theta2) + latlng1[1],
+ midpointY = r2 * Math.sin(theta2) + latlng1[0];
+
+ var midpointLatLng = [midpointY, midpointX];
+
+ latlngs.push(latlng1, midpointLatLng, latlng2);
+
+ var pathDefaults = {
+ color: "#b35900",
+ weight: 3,
+ animate: 500,
+ hasBalls: true,
+ };
+
+ let pathOptions = Object.assign(pathDefaults, options);
+
+ var curvedPath = L.curve(
+ ["M", latlng1, "Q", midpointLatLng, latlng2],
+ pathOptions
+ ).addTo(map);
+
+ return curvedPath;
+ }
+
+ function calcDistances(nodes) {
+ nodes.forEach((x) => {
+ x.properties.distances = {};
+ const coords1 = getCoordArray(x);
+ nodes.forEach((y) => {
+ const coords2 = getCoordArray(y);
+ x.properties.distances[y.properties.mdo_id] = Math.sqrt(
+ Math.pow(coords1[0] - coords2[0], 2) +
+ Math.pow(coords1[1] - coords2[1], 2)
+ );
+ });
+ });
+ }
+
+ const space = [43.09224, -77.674799];
+ async function getUpdate() {
+ console.log("Updating Occupancy Matrix");
+ let counts = await fetch(densityMapUrl + "/current");
+ counts = await counts.json();
+
+ for (let i = 0; i < counts.length; i++) {
+ const pt =
+ counts[i].mdo_id == null
+ ? pts[counts[i].location]
+ : pts[counts[i].mdo_id];
+ if (pt == undefined) continue;
+ pt.properties.diff = counts[i].count - pt.properties.count;
+ pt.properties.count = counts[i].count;
+ setMarker(pt, pt.properties.reference);
+ }
+
+ let shots = getShots(Object.values(pts));
+ shots = shuffle(shuffle(shots));
+ const timeBetween = (60000 * 5 + 1) / shots.length;
+
+ // randomize time delay
+ let timeDelay = [];
+ shots.forEach(() => {
+ timeDelay.push(Math.random());
+ });
+ const interval = 60000 * 5; // 5 minute delay
+ for (let i = 0; i < timeDelay.length; i++) {
+ timeDelay[i] = timeDelay[i] * interval;
+ }
+
+ console.log(
+ `Shot total for next 5 minutes: ${shots.length} - ${
+ timeBetween / 1000
+ } second intervals`
+ );
+ for (let i = 0; i < shots.length; i++) {
+ loadShot(shots[i], timeDelay[i], { trail: true });
+ }
+ }
+
+ async function loadShot(shot, delay, { trail = false } = {}) {
+ await sleep(delay);
+ shootVector(shot[0], shot[1], { trail: trail });
+ }
+
+ function findOneShot(nodes, target) {
+ let sortedByDistance = nodes.sort((a, b) => {
+ return (
+ target.properties.distances[a.properties.mdo_id] -
+ target.properties.distances[b.properties.mdo_id]
+ );
+ });
+ const sign = target.properties.diff > 0;
+ for (let x = 1; x < sortedByDistance.length; x++) {
+ if (sortedByDistance[x].properties.diff > 0 !== sign) {
+ return sortedByDistance[x];
+ }
+ }
+ }
+
+ function getShots(nodes) {
+ let noChange = false;
+ let shots = [];
+ let sourcesAndSinks = nodes.filter((x) => {
+ return x.properties.diff !== 0;
+ });
+
+ let i;
+ while (!noChange && sourcesAndSinks.length > 0) {
+ noChange = true;
+ // sourcesAndSinks.forEach((x) => {
+ // x.properties.changed = false;
+ // });
+ // for (let i = sourcesAndSinks.length - 1; i >= 0; i--) {
+ // try {
+ // if (sourcesAndSinks[i].properties.changed) continue; // this node is a prior recipient this iteration
+ // } catch {
+ // continue;
+ // }
+ let sorted = sourcesAndSinks.sort((a, b) => {
+ Math.abs(a.properties.diff) - Math.abs(b.properties.diff);
+ });
+ i = sourcesAndSinks.indexOf(sorted[0]);
+ let recipient = findOneShot(sourcesAndSinks, sourcesAndSinks[i]);
+ if (recipient) {
+ let shotArr;
+ if (sourcesAndSinks[i].properties.diff > 0) {
+ shotArr = [sourcesAndSinks[i], recipient];
+ sourcesAndSinks[i].properties.diff--;
+ recipient.properties.diff++;
+ } else {
+ shotArr = [recipient, sourcesAndSinks[i]];
+ sourcesAndSinks[i].properties.diff++;
+ recipient.properties.diff--;
+ }
+ shots.push(shotArr);
+ noChange = false;
+ sourcesAndSinks[i].properties.changed = true;
+ recipient.properties.changed = true;
+
+ let tmpRef = recipient;
+ if (sourcesAndSinks[i].properties.diff == 0) {
+ sourcesAndSinks.splice(i, 1);
+ }
+ if (
+ sourcesAndSinks[sourcesAndSinks.indexOf(tmpRef)].properties.diff == 0
+ ) {
+ sourcesAndSinks.splice(recipient, 1);
+ }
+ }
+ // }
+ }
+
+ // if no more people on campus, get them from space
+ sourcesAndSinks.forEach((x) => {
+ while (x.properties.diff > 0) {
+ shots.push([x, space]);
+ x.properties.diff--;
+ }
+
+ while (x.properties.diff < 0) {
+ shots.push([space, x]);
+ x.properties.diff++;
+ }
+ });
+
+ return shots;
+ }
+
+ init().then(() => {
+ // map.on("click", () => {
+ // shootVector(pts[2], pts[8]);
+ // });
+ // shootVector(pts[0], pts[1], {speed: 500});
+ calcDistances(Object.values(pts));
+ getUpdate()
+ setInterval(getUpdate, 60000 * 5);
+ });
+
\ No newline at end of file
diff --git a/src/static/js/lib/CUSTOM.leaflet.curve.js b/src/static/js/lib/CUSTOM.leaflet.curve.js
new file mode 100644
index 0000000..fb99f63
--- /dev/null
+++ b/src/static/js/lib/CUSTOM.leaflet.curve.js
@@ -0,0 +1,558 @@
+/*
+ * Leaflet.curve v0.9.2 - a plugin for Leaflet mapping library. https://github.com/elfalem/Leaflet.curve
+ * (c) elfalem 2015-2023
+ */
+/*
+ * note that SVG (x, y) corresponds to (long, lat)
+ */
+
+L.Curve = L.Path.extend({
+ options: {},
+
+ initialize: function (path, options) {
+ L.setOptions(this, options);
+ this._setPath(path);
+ },
+
+ // Added to follow the naming convention of L.Polyline and other Leaflet component classes:
+ // (https://leafletjs.com/reference-1.6.0.html#polyline-setlatlngs)
+ setLatLngs: function (path) {
+ return this.setPath(path);
+ },
+
+ getLatLngs: function () {
+ return this.getPath();
+ },
+
+ _updateBounds: function () {
+ var tolerance = this._clickTolerance();
+ var tolerancePoint = new L.Point(tolerance, tolerance);
+
+ //_pxBounds is critical for canvas renderer, used to determine area that needs redrawing
+ this._pxBounds = new L.Bounds([
+ this._rawPxBounds.min.subtract(tolerancePoint),
+ this._rawPxBounds.max.add(tolerancePoint),
+ ]);
+ },
+
+ getPath: function () {
+ return this._coords;
+ },
+
+ setPath: function (path) {
+ this._setPath(path);
+ return this.redraw();
+ },
+
+ getBounds: function () {
+ return this._bounds;
+ },
+
+ _setPath: function (path) {
+ this._coords = path;
+ this._bounds = this._computeBounds();
+ },
+
+ _computeBounds: function () {
+ var bound = new L.LatLngBounds();
+ var lastPoint;
+ var lastCommand;
+ var coord;
+ for (var i = 0; i < this._coords.length; i++) {
+ coord = this._coords[i];
+ if (typeof coord == "string" || coord instanceof String) {
+ lastCommand = coord;
+ } else if (lastCommand == "H") {
+ bound.extend([lastPoint.lat, coord[0]]);
+ lastPoint = new L.latLng(lastPoint.lat, coord[0]);
+ } else if (lastCommand == "V") {
+ bound.extend([coord[0], lastPoint.lng]);
+ lastPoint = new L.latLng(coord[0], lastPoint.lng);
+ } else if (lastCommand == "C") {
+ var controlPoint1 = new L.latLng(coord[0], coord[1]);
+ coord = this._coords[++i];
+ var controlPoint2 = new L.latLng(coord[0], coord[1]);
+ coord = this._coords[++i];
+ var endPoint = new L.latLng(coord[0], coord[1]);
+
+ bound.extend(controlPoint1);
+ bound.extend(controlPoint2);
+ bound.extend(endPoint);
+
+ endPoint.controlPoint1 = controlPoint1;
+ endPoint.controlPoint2 = controlPoint2;
+ lastPoint = endPoint;
+ } else if (lastCommand == "S") {
+ var controlPoint2 = new L.latLng(coord[0], coord[1]);
+ coord = this._coords[++i];
+ var endPoint = new L.latLng(coord[0], coord[1]);
+
+ var controlPoint1 = lastPoint;
+ if (lastPoint.controlPoint2) {
+ var diffLat = lastPoint.lat - lastPoint.controlPoint2.lat;
+ var diffLng = lastPoint.lng - lastPoint.controlPoint2.lng;
+ controlPoint1 = new L.latLng(
+ lastPoint.lat + diffLat,
+ lastPoint.lng + diffLng
+ );
+ }
+
+ bound.extend(controlPoint1);
+ bound.extend(controlPoint2);
+ bound.extend(endPoint);
+
+ endPoint.controlPoint1 = controlPoint1;
+ endPoint.controlPoint2 = controlPoint2;
+ lastPoint = endPoint;
+ } else if (lastCommand == "Q") {
+ var controlPoint = new L.latLng(coord[0], coord[1]);
+ coord = this._coords[++i];
+ var endPoint = new L.latLng(coord[0], coord[1]);
+
+ bound.extend(controlPoint);
+ bound.extend(endPoint);
+
+ endPoint.controlPoint = controlPoint;
+ lastPoint = endPoint;
+ } else if (lastCommand == "T") {
+ var endPoint = new L.latLng(coord[0], coord[1]);
+
+ var controlPoint = lastPoint;
+ if (lastPoint.controlPoint) {
+ var diffLat = lastPoint.lat - lastPoint.controlPoint.lat;
+ var diffLng = lastPoint.lng - lastPoint.controlPoint.lng;
+ controlPoint = new L.latLng(
+ lastPoint.lat + diffLat,
+ lastPoint.lng + diffLng
+ );
+ }
+
+ bound.extend(controlPoint);
+ bound.extend(endPoint);
+
+ endPoint.controlPoint = controlPoint;
+ lastPoint = endPoint;
+ } else {
+ bound.extend(coord);
+ lastPoint = new L.latLng(coord[0], coord[1]);
+ }
+ }
+ return bound;
+ },
+
+ getCenter: function () {
+ return this._bounds.getCenter();
+ },
+
+ // _update() is invoked by Path._reset()
+ _update: function () {
+ if (!this._map) {
+ return;
+ }
+
+ // TODO: consider implementing this._clipPoints(); and this._simplifyPoints(); to improve performance
+ this._updatePath();
+ },
+
+ _updatePath: function () {
+ // the following can be thought of as this._renderer.updateCurve() in both SVG/Canvas renderers
+ // similar to Canvas._updatePoly(), Canvas._updateCircle(), etc...
+ if (this._usingCanvas) {
+ this._updateCurveCanvas();
+ } else {
+ this._updateCurveSvg();
+ }
+ },
+
+ //_project() is invoked by Path._reset()
+ _project: function () {
+ var coord, lastCoord, curCommand, curPoint;
+
+ this._points = [];
+
+ for (var i = 0; i < this._coords.length; i++) {
+ coord = this._coords[i];
+ if (typeof coord == "string" || coord instanceof String) {
+ this._points.push(coord);
+ curCommand = coord;
+ } else {
+ switch (coord.length) {
+ case 2:
+ curPoint = this._map.latLngToLayerPoint(coord);
+ lastCoord = coord;
+ break;
+ case 1:
+ if (curCommand == "H") {
+ curPoint = this._map.latLngToLayerPoint([lastCoord[0], coord[0]]);
+ lastCoord = [lastCoord[0], coord[0]];
+ } else {
+ curPoint = this._map.latLngToLayerPoint([coord[0], lastCoord[1]]);
+ lastCoord = [coord[0], lastCoord[1]];
+ }
+ break;
+ }
+ this._points.push(curPoint);
+ }
+ }
+
+ if (this._bounds.isValid()) {
+ var northWestLayerPoint = this._map.latLngToLayerPoint(
+ this._bounds.getNorthWest()
+ );
+ var southEastLayerPoint = this._map.latLngToLayerPoint(
+ this._bounds.getSouthEast()
+ );
+ this._rawPxBounds = new L.Bounds(
+ northWestLayerPoint,
+ southEastLayerPoint
+ );
+ this._updateBounds();
+ }
+ },
+
+ _curvePointsToPath: function (points) {
+ var point,
+ curCommand,
+ str = "";
+ for (var i = 0; i < points.length; i++) {
+ point = points[i];
+ if (typeof point == "string" || point instanceof String) {
+ curCommand = point;
+ str += curCommand;
+ } else {
+ switch (curCommand) {
+ case "H":
+ str += point.x + " ";
+ break;
+ case "V":
+ str += point.y + " ";
+ break;
+ default:
+ str += point.x + "," + point.y + " ";
+ break;
+ }
+ }
+ }
+ return str || "M0 0";
+ },
+
+ beforeAdd: function (map) {
+ L.Path.prototype.beforeAdd.call(this, map);
+
+ this._usingCanvas = this._renderer instanceof L.Canvas;
+
+ if (this._usingCanvas) {
+ this._pathSvgElement = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ }
+ },
+
+ onAdd: function (map) {
+ if (this._usingCanvas) {
+ // determine if dash array is set by user
+ this._canvasSetDashArray = !this.options.dashArray;
+ }
+
+ L.Path.prototype.onAdd.call(this, map); // calls _update()
+
+ if (this._usingCanvas) {
+ if (this.options.animate && typeof TWEEN === "object") {
+ this._normalizeCanvasAnimationOptions();
+
+ this._tweenedObject = { offset: this._pathSvgElement.getTotalLength() };
+ this._tween = new TWEEN.Tween(this._tweenedObject)
+ .to({ offset: 0 }, this.options.animate.duration)
+ // difference of behavior with SVG, delay occurs on every iteration
+ .delay(this.options.animate.delay)
+ .repeat(this.options.animate.iterations - 1)
+ .onComplete(
+ (function (scope) {
+ return function () {
+ scope._canvasAnimating = false;
+ };
+ })(this)
+ )
+ .start();
+
+ this._canvasAnimating = true;
+ this._animateCanvas();
+ } else {
+ this._canvasAnimating = false;
+ }
+ } else {
+ if (this.options.animate && this._path.animate) {
+ var length = Math.min(this._svgSetDashArray(), 1000);
+ this._path.pathLength.baseVal = length;
+ keyframes = [
+ { strokeDashoffset: length },
+ { strokeDashoffset: 0 }
+ ]
+ if (!this.options.fade) keyframes.push({ strokeDashoffset: -length });
+ let animation = this._path.animate(
+ keyframes,
+ this.options.animate
+ );
+ if (this.options.fade) {
+ animation.onfinish = () => {
+ this._path.animate(
+ [{ opacity: 1 }, { opacity: 0 }],
+ this.options.fadeSpeed
+ ).onfinish = () => { this.remove()};
+ };
+ } else if (this.options.onlyAnimate) {
+ animation.onfinish = () => {
+ this.remove();
+ };
+ }
+ }
+ }
+ },
+
+ // SVG specific logic
+ _updateCurveSvg: function () {
+ this._renderer._setPath(this, this._curvePointsToPath(this._points));
+
+ if (this.options.animate) {
+ this._svgSetDashArray();
+ }
+ },
+
+ _svgSetDashArray: function () {
+ var path = this._path;
+ var length = path.getTotalLength();
+
+ if (!this.options.dashArray) {
+ path.style.strokeDasharray = length + " " + length;
+ }
+ return length;
+ },
+
+ // Needed by the `Canvas` renderer for interactivity
+ _containsPoint: function (layerPoint) {
+ if (!this._bounds.isValid()) {
+ return false;
+ }
+ return this._bounds.contains(this._map.layerPointToLatLng(layerPoint));
+ },
+
+ // Canvas specific logic below here
+ _normalizeCanvasAnimationOptions: function () {
+ var opts = {
+ delay: 0,
+ duration: 0,
+ iterations: 1,
+ };
+ if (typeof this.options.animate == "number") {
+ opts.duration = this.options.animate;
+ } else {
+ if (this.options.animate.duration) {
+ opts.duration = this.options.animate.duration;
+ }
+ if (this.options.animate.delay) {
+ opts.delay = this.options.animate.delay;
+ }
+ if (this.options.animate.iterations) {
+ opts.iterations = this.options.animate.iterations;
+ }
+ }
+
+ this.options.animate = opts;
+ },
+
+ _updateCurveCanvas: function () {
+ var pathString = this._curvePointsToPath(this._points);
+ this._pathSvgElement.setAttribute("d", pathString);
+
+ if (
+ this.options.animate &&
+ typeof TWEEN === "object" &&
+ this._canvasSetDashArray
+ ) {
+ this.options.dashArray = this._pathSvgElement.getTotalLength() + "";
+ this._renderer._updateDashArray(this);
+ }
+
+ this._curveFillStroke(new Path2D(pathString), this._renderer._ctx);
+ },
+
+ _animateCanvas: function () {
+ TWEEN.update();
+
+ // clear out area and re-render all layers
+ this._renderer._updatePaths();
+
+ if (this._canvasAnimating) {
+ this._animationFrameId = L.Util.requestAnimFrame(
+ this._animateCanvas,
+ this
+ );
+ }
+ },
+
+ // similar to Canvas._fillStroke(ctx, layer)
+ _curveFillStroke: function (path2d, ctx) {
+ ctx.lineDashOffset = this._canvasAnimating
+ ? this._tweenedObject.offset
+ : 0.0;
+
+ var options = this.options;
+
+ if (options.fill) {
+ ctx.globalAlpha = options.fillOpacity;
+ ctx.fillStyle = options.fillColor || options.color;
+ ctx.fill(path2d, options.fillRule || "evenodd");
+ }
+
+ if (options.stroke && options.weight !== 0) {
+ if (ctx.setLineDash) {
+ ctx.setLineDash((this.options && this.options._dashArray) || []);
+ }
+ ctx.globalAlpha = options.opacity;
+ ctx.lineWidth = options.weight;
+ ctx.strokeStyle = options.color;
+ ctx.lineCap = options.lineCap;
+ ctx.lineJoin = options.lineJoin;
+ ctx.stroke(path2d);
+ }
+ },
+
+ // path tracing logic below here
+ trace: function (t) {
+ // initially map is undefined, but then null if curve was added and removed
+ if (this._map === undefined || this._map === null) {
+ return [];
+ }
+
+ t = t.filter(function (element) {
+ return element >= 0 && element <= 1;
+ });
+
+ var point, curCommand, curStartPoint, curEndPoint;
+ var p1, p2, p3;
+ var samples = [];
+ for (var i = 0; i < this._points.length; i++) {
+ point = this._points[i];
+ if (typeof point == "string" || point instanceof String) {
+ curCommand = point;
+
+ if (curCommand == "Z") {
+ samples = samples.concat(
+ this._linearTrace(t, curEndPoint, curStartPoint)
+ );
+ }
+ } else {
+ switch (curCommand) {
+ case "M":
+ curStartPoint = point;
+ curEndPoint = point;
+ break;
+ case "L":
+ case "H":
+ case "V":
+ samples = samples.concat(this._linearTrace(t, curEndPoint, point));
+
+ curEndPoint = point;
+ break;
+ case "C":
+ p1 = point;
+ p2 = this._points[++i];
+ p3 = this._points[++i];
+ samples = samples.concat(
+ this._cubicTrace(t, curEndPoint, p1, p2, p3)
+ );
+
+ curEndPoint = p3;
+ break;
+ case "S":
+ p1 = this._reflectPoint(p2, curEndPoint);
+ p2 = point;
+ p3 = this._points[++i];
+ samples = samples.concat(
+ this._cubicTrace(t, curEndPoint, p1, p2, p3)
+ );
+
+ curEndPoint = p3;
+ break;
+ case "Q":
+ p1 = point;
+ p2 = this._points[++i];
+ samples = samples.concat(
+ this._quadraticTrace(t, curEndPoint, p1, p2)
+ );
+
+ curEndPoint = p2;
+ break;
+ case "T":
+ p1 = this._reflectPoint(p1, curEndPoint);
+ p2 = point;
+ samples = samples.concat(
+ this._quadraticTrace(t, curEndPoint, p1, p2)
+ );
+
+ curEndPoint = p2;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return samples;
+ },
+ _linearTrace: function (t, p0, p1) {
+ return t.map((interval) => {
+ var x = this._singleLinearTrace(interval, p0.x, p1.x);
+ var y = this._singleLinearTrace(interval, p0.y, p1.y);
+ return this._map.layerPointToLatLng([x, y]);
+ });
+ },
+ _quadraticTrace: function (t, p0, p1, p2) {
+ return t.map((interval) => {
+ var x = this._singleQuadraticTrace(interval, p0.x, p1.x, p2.x);
+ var y = this._singleQuadraticTrace(interval, p0.y, p1.y, p2.y);
+ return this._map.layerPointToLatLng([x, y]);
+ });
+ },
+ _cubicTrace: function (t, p0, p1, p2, p3) {
+ return t.map((interval) => {
+ var x = this._singleCubicTrace(interval, p0.x, p1.x, p2.x, p3.x);
+ var y = this._singleCubicTrace(interval, p0.y, p1.y, p2.y, p3.y);
+ return this._map.layerPointToLatLng([x, y]);
+ });
+ },
+ _singleLinearTrace: function (t, p0, p1) {
+ return p0 + t * (p1 - p0);
+ },
+ _singleQuadraticTrace: function (t, p0, p1, p2) {
+ var oneMinusT = 1 - t;
+ return (
+ Math.pow(oneMinusT, 2) * p0 + 2 * oneMinusT * t * p1 + Math.pow(t, 2) * p2
+ );
+ },
+ _singleCubicTrace: function (t, p0, p1, p2, p3) {
+ var oneMinusT = 1 - t;
+ return (
+ Math.pow(oneMinusT, 3) * p0 +
+ 3 * Math.pow(oneMinusT, 2) * t * p1 +
+ 3 * oneMinusT * Math.pow(t, 2) * p2 +
+ Math.pow(t, 3) * p3
+ );
+ },
+ _reflectPoint: function (point, over) {
+ x = over.x + (over.x - point.x);
+ y = over.y + (over.y - point.y);
+ return L.point(x, y);
+ },
+});
+
+L.curve = function (path, options) {
+ return new L.Curve(path, options);
+};
+
+// async function killMe(promise, obj){
+// await promise;
+// // obj.remove();
+// console.log("killed");
+// console.log(promise);
+// }
diff --git a/src/static/js/lib/leaflet-providers.js b/src/static/js/lib/leaflet-providers.js
new file mode 100644
index 0000000..bcde1ed
--- /dev/null
+++ b/src/static/js/lib/leaflet-providers.js
@@ -0,0 +1,1178 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['leaflet'], factory);
+ } else if (typeof modules === 'object' && module.exports) {
+ // define a Common JS module that relies on 'leaflet'
+ module.exports = factory(require('leaflet'));
+ } else {
+ // Assume Leaflet is loaded into global object L already
+ factory(L);
+ }
+}(this, function (L) {
+ 'use strict';
+
+ L.TileLayer.Provider = L.TileLayer.extend({
+ initialize: function (arg, options) {
+ var providers = L.TileLayer.Provider.providers;
+
+ var parts = arg.split('.');
+
+ var providerName = parts[0];
+ var variantName = parts[1];
+
+ if (!providers[providerName]) {
+ throw 'No such provider (' + providerName + ')';
+ }
+
+ var provider = {
+ url: providers[providerName].url,
+ options: providers[providerName].options
+ };
+
+ // overwrite values in provider from variant.
+ if (variantName && 'variants' in providers[providerName]) {
+ if (!(variantName in providers[providerName].variants)) {
+ throw 'No such variant of ' + providerName + ' (' + variantName + ')';
+ }
+ var variant = providers[providerName].variants[variantName];
+ var variantOptions;
+ if (typeof variant === 'string') {
+ variantOptions = {
+ variant: variant
+ };
+ } else {
+ variantOptions = variant.options;
+ }
+ provider = {
+ url: variant.url || provider.url,
+ options: L.Util.extend({}, provider.options, variantOptions)
+ };
+ }
+
+ // replace attribution placeholders with their values from toplevel provider attribution,
+ // recursively
+ var attributionReplacer = function (attr) {
+ if (attr.indexOf('{attribution.') === -1) {
+ return attr;
+ }
+ return attr.replace(/\{attribution.(\w*)\}/g,
+ function (match, attributionName) {
+ return attributionReplacer(providers[attributionName].options.attribution);
+ }
+ );
+ };
+ provider.options.attribution = attributionReplacer(provider.options.attribution);
+
+ // Compute final options combining provider options with any user overrides
+ var layerOpts = L.Util.extend({}, provider.options, options);
+ L.TileLayer.prototype.initialize.call(this, provider.url, layerOpts);
+ }
+ });
+
+ /**
+ * Definition of providers.
+ * see http://leafletjs.com/reference.html#tilelayer for options in the options map.
+ */
+
+ L.TileLayer.Provider.providers = {
+ OpenStreetMap: {
+ url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution:
+ '© OpenStreetMap contributors'
+ },
+ variants: {
+ Mapnik: {},
+ DE: {
+ url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 18
+ }
+ },
+ CH: {
+ url: 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 18,
+ bounds: [[45, 5], [48, 11]]
+ }
+ },
+ France: {
+ url: 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 20,
+ attribution: '© OpenStreetMap France | {attribution.OpenStreetMap}'
+ }
+ },
+ HOT: {
+ url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
+ options: {
+ attribution:
+ '{attribution.OpenStreetMap}, ' +
+ 'Tiles style by Humanitarian OpenStreetMap Team ' +
+ 'hosted by OpenStreetMap France'
+ }
+ },
+ BZH: {
+ url: 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png',
+ options: {
+ attribution: '{attribution.OpenStreetMap}, Tiles courtesy of Breton OpenStreetMap Team',
+ bounds: [[46.2, -5.5], [50, 0.7]]
+ }
+ }
+ }
+ },
+ MapTilesAPI: {
+ url: 'https://maptiles.p.rapidapi.com/{variant}/{z}/{x}/{y}.png?rapidapi-key={apikey}',
+ options: {
+ attribution:
+ '© MapTiles API, {attribution.OpenStreetMap}',
+ variant: 'en/map/v1',
+ // Get your own MapTiles API access token here : https://www.maptilesapi.com/
+ // NB : this is a demonstration key that comes with no guarantee and not to be used in production
+ apikey: '',
+ maxZoom: 19
+ },
+ variants: {
+ OSMEnglish: {
+ options: {
+ variant: 'en/map/v1'
+ }
+ },
+ OSMFrancais: {
+ options: {
+ variant: 'fr/map/v1'
+ }
+ },
+ OSMEspagnol: {
+ options: {
+ variant: 'es/map/v1'
+ }
+ }
+ }
+ },
+ OpenSeaMap: {
+ url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
+ options: {
+ attribution: 'Map data: © OpenSeaMap contributors'
+ }
+ },
+ OPNVKarte: {
+ url: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 18,
+ attribution: 'Map memomaps.de CC-BY-SA, map data {attribution.OpenStreetMap}'
+ }
+ },
+ OpenTopoMap: {
+ url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 17,
+ attribution: 'Map data: {attribution.OpenStreetMap}, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
+ }
+ },
+ OpenRailwayMap: {
+ url: 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenRailwayMap (CC-BY-SA)'
+ }
+ },
+ OpenFireMap: {
+ url: 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenFireMap (CC-BY-SA)'
+ }
+ },
+ SafeCast: {
+ url: 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 16,
+ attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © SafeCast (CC-BY-SA)'
+ }
+ },
+ Stadia: {
+ url: 'https://tiles.stadiamaps.com/tiles/{variant}/{z}/{x}/{y}{r}.{ext}',
+ options: {
+ minZoom: 0,
+ maxZoom: 20,
+ attribution:
+ '© Stadia Maps ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'alidade_smooth',
+ ext: 'png'
+ },
+ variants: {
+ AlidadeSmooth: 'alidade_smooth',
+ AlidadeSmoothDark: 'alidade_smooth_dark',
+ OSMBright: 'osm_bright',
+ Outdoors: 'outdoors',
+ StamenToner: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_toner'
+ }
+ },
+ StamenTonerBackground: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_toner_background'
+ }
+ },
+ StamenTonerLines: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_toner_lines'
+ }
+ },
+ StamenTonerLabels: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_toner_labels'
+ }
+ },
+ StamenTonerLite: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_toner_lite'
+ }
+ },
+ StamenWatercolor: {
+ url: 'https://tiles.stadiamaps.com/tiles/{variant}/{z}/{x}/{y}.{ext}',
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_watercolor',
+ ext: 'jpg',
+ minZoom: 1,
+ maxZoom: 16
+ }
+ },
+ StamenTerrain: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_terrain',
+ minZoom: 0,
+ maxZoom: 18
+ }
+ },
+ StamenTerrainBackground: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_terrain_background',
+ minZoom: 0,
+ maxZoom: 18
+ }
+ },
+ StamenTerrainLabels: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_terrain_labels',
+ minZoom: 0,
+ maxZoom: 18
+ }
+ },
+ StamenTerrainLines: {
+ options: {
+ attribution:
+ '© Stadia Maps ' +
+ '© Stamen Design ' +
+ '© OpenMapTiles ' +
+ '{attribution.OpenStreetMap}',
+ variant: 'stamen_terrain_lines',
+ minZoom: 0,
+ maxZoom: 18
+ }
+ }
+ }
+ },
+ Thunderforest: {
+ url: 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
+ options: {
+ attribution:
+ '© Thunderforest, {attribution.OpenStreetMap}',
+ variant: 'cycle',
+ apikey: '',
+ maxZoom: 22
+ },
+ variants: {
+ OpenCycleMap: 'cycle',
+ Transport: {
+ options: {
+ variant: 'transport'
+ }
+ },
+ TransportDark: {
+ options: {
+ variant: 'transport-dark'
+ }
+ },
+ SpinalMap: {
+ options: {
+ variant: 'spinal-map'
+ }
+ },
+ Landscape: 'landscape',
+ Outdoors: 'outdoors',
+ Pioneer: 'pioneer',
+ MobileAtlas: 'mobile-atlas',
+ Neighbourhood: 'neighbourhood'
+ }
+ },
+ CyclOSM: {
+ url: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 20,
+ attribution: 'CyclOSM | Map data: {attribution.OpenStreetMap}'
+ }
+ },
+ Jawg: {
+ url: 'https://{s}.tile.jawg.io/{variant}/{z}/{x}/{y}{r}.png?access-token={accessToken}',
+ options: {
+ attribution:
+ '© JawgMaps ' +
+ '{attribution.OpenStreetMap}',
+ minZoom: 0,
+ maxZoom: 22,
+ subdomains: 'abcd',
+ variant: 'jawg-terrain',
+ // Get your own Jawg access token here : https://www.jawg.io/lab/
+ // NB : this is a demonstration key that comes with no guarantee
+ accessToken: '',
+ },
+ variants: {
+ Streets: 'jawg-streets',
+ Terrain: 'jawg-terrain',
+ Sunny: 'jawg-sunny',
+ Dark: 'jawg-dark',
+ Light: 'jawg-light',
+ Matrix: 'jawg-matrix'
+ }
+ },
+ MapBox: {
+ url: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}',
+ options: {
+ attribution:
+ '© Mapbox ' +
+ '{attribution.OpenStreetMap} ' +
+ 'Improve this map',
+ tileSize: 512,
+ maxZoom: 18,
+ zoomOffset: -1,
+ id: 'mapbox/streets-v11',
+ accessToken: '',
+ }
+ },
+ MapTiler: {
+ url: 'https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}',
+ options: {
+ attribution:
+ '© MapTiler © OpenStreetMap contributors',
+ variant: 'streets',
+ ext: 'png',
+ key: '',
+ tileSize: 512,
+ zoomOffset: -1,
+ minZoom: 0,
+ maxZoom: 21
+ },
+ variants: {
+ Streets: 'streets',
+ Basic: 'basic',
+ Bright: 'bright',
+ Pastel: 'pastel',
+ Positron: 'positron',
+ Hybrid: {
+ options: {
+ variant: 'hybrid',
+ ext: 'jpg'
+ }
+ },
+ Toner: 'toner',
+ Topo: 'topo',
+ Voyager: 'voyager'
+ }
+ },
+ TomTom: {
+ url: 'https://{s}.api.tomtom.com/map/1/tile/{variant}/{style}/{z}/{x}/{y}.{ext}?key={apikey}',
+ options: {
+ variant: 'basic',
+ maxZoom: 22,
+ attribution:
+ '© 1992 - ' + new Date().getFullYear() + ' TomTom. ',
+ subdomains: 'abcd',
+ style: 'main',
+ ext: 'png',
+ apikey: '',
+ },
+ variants: {
+ Basic: 'basic',
+ Hybrid: 'hybrid',
+ Labels: 'labels'
+ }
+ },
+ Esri: {
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
+ options: {
+ variant: 'World_Street_Map',
+ attribution: 'Tiles © Esri'
+ },
+ variants: {
+ WorldStreetMap: {
+ options: {
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+ }
+ },
+ DeLorme: {
+ options: {
+ variant: 'Specialty/DeLorme_World_Base_Map',
+ minZoom: 1,
+ maxZoom: 11,
+ attribution: '{attribution.Esri} — Copyright: ©2012 DeLorme'
+ }
+ },
+ WorldTopoMap: {
+ options: {
+ variant: 'World_Topo_Map',
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community'
+ }
+ },
+ WorldImagery: {
+ options: {
+ variant: 'World_Imagery',
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
+ }
+ },
+ WorldTerrain: {
+ options: {
+ variant: 'World_Terrain_Base',
+ maxZoom: 13,
+ attribution:
+ '{attribution.Esri} — ' +
+ 'Source: USGS, Esri, TANA, DeLorme, and NPS'
+ }
+ },
+ WorldShadedRelief: {
+ options: {
+ variant: 'World_Shaded_Relief',
+ maxZoom: 13,
+ attribution: '{attribution.Esri} — Source: Esri'
+ }
+ },
+ WorldPhysical: {
+ options: {
+ variant: 'World_Physical_Map',
+ maxZoom: 8,
+ attribution: '{attribution.Esri} — Source: US National Park Service'
+ }
+ },
+ OceanBasemap: {
+ options: {
+ variant: 'Ocean/World_Ocean_Base',
+ maxZoom: 13,
+ attribution: '{attribution.Esri} — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri'
+ }
+ },
+ NatGeoWorldMap: {
+ options: {
+ variant: 'NatGeo_World_Map',
+ maxZoom: 16,
+ attribution: '{attribution.Esri} — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC'
+ }
+ },
+ WorldGrayCanvas: {
+ options: {
+ variant: 'Canvas/World_Light_Gray_Base',
+ maxZoom: 16,
+ attribution: '{attribution.Esri} — Esri, DeLorme, NAVTEQ'
+ }
+ }
+ }
+ },
+ OpenWeatherMap: {
+ url: 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
+ options: {
+ maxZoom: 19,
+ attribution: 'Map data © OpenWeatherMap',
+ apiKey: '',
+ opacity: 0.5
+ },
+ variants: {
+ Clouds: 'clouds',
+ CloudsClassic: 'clouds_cls',
+ Precipitation: 'precipitation',
+ PrecipitationClassic: 'precipitation_cls',
+ Rain: 'rain',
+ RainClassic: 'rain_cls',
+ Pressure: 'pressure',
+ PressureContour: 'pressure_cntr',
+ Wind: 'wind',
+ Temperature: 'temp',
+ Snow: 'snow'
+ }
+ },
+ HERE: {
+ /*
+ * HERE maps, formerly Nokia maps.
+ * These basemaps are free, but you need an api id and app key. Please sign up at
+ * https://developer.here.com/plans
+ */
+ url:
+ 'https://{s}.{base}.maps.api.here.com/maptile/2.1/' +
+ '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' +
+ 'app_id={app_id}&app_code={app_code}&lg={language}',
+ options: {
+ attribution:
+ 'Map © 1987-' + new Date().getFullYear() + ' HERE',
+ subdomains: '1234',
+ mapID: 'newest',
+ 'app_id': '',
+ 'app_code': '',
+ base: 'base',
+ variant: 'normal.day',
+ maxZoom: 20,
+ type: 'maptile',
+ language: 'eng',
+ format: 'png8',
+ size: '256'
+ },
+ variants: {
+ normalDay: 'normal.day',
+ normalDayCustom: 'normal.day.custom',
+ normalDayGrey: 'normal.day.grey',
+ normalDayMobile: 'normal.day.mobile',
+ normalDayGreyMobile: 'normal.day.grey.mobile',
+ normalDayTransit: 'normal.day.transit',
+ normalDayTransitMobile: 'normal.day.transit.mobile',
+ normalDayTraffic: {
+ options: {
+ variant: 'normal.traffic.day',
+ base: 'traffic',
+ type: 'traffictile'
+ }
+ },
+ normalNight: 'normal.night',
+ normalNightMobile: 'normal.night.mobile',
+ normalNightGrey: 'normal.night.grey',
+ normalNightGreyMobile: 'normal.night.grey.mobile',
+ normalNightTransit: 'normal.night.transit',
+ normalNightTransitMobile: 'normal.night.transit.mobile',
+ reducedDay: 'reduced.day',
+ reducedNight: 'reduced.night',
+ basicMap: {
+ options: {
+ type: 'basetile'
+ }
+ },
+ mapLabels: {
+ options: {
+ type: 'labeltile',
+ format: 'png'
+ }
+ },
+ trafficFlow: {
+ options: {
+ base: 'traffic',
+ type: 'flowtile'
+ }
+ },
+ carnavDayGrey: 'carnav.day.grey',
+ hybridDay: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day'
+ }
+ },
+ hybridDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.mobile'
+ }
+ },
+ hybridDayTransit: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.transit'
+ }
+ },
+ hybridDayGrey: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.grey.day'
+ }
+ },
+ hybridDayTraffic: {
+ options: {
+ variant: 'hybrid.traffic.day',
+ base: 'traffic',
+ type: 'traffictile'
+ }
+ },
+ pedestrianDay: 'pedestrian.day',
+ pedestrianNight: 'pedestrian.night',
+ satelliteDay: {
+ options: {
+ base: 'aerial',
+ variant: 'satellite.day'
+ }
+ },
+ terrainDay: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day'
+ }
+ },
+ terrainDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day.mobile'
+ }
+ }
+ }
+ },
+ HEREv3: {
+ /*
+ * HERE maps API Version 3.
+ * These basemaps are free, but you need an API key. Please sign up at
+ * https://developer.here.com/plans
+ * Version 3 deprecates the app_id and app_code access in favor of apiKey
+ *
+ * Supported access methods as of 2019/12/21:
+ * @see https://developer.here.com/faqs#access-control-1--how-do-you-control-access-to-here-location-services
+ */
+ url:
+ 'https://{s}.{base}.maps.ls.hereapi.com/maptile/2.1/' +
+ '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' +
+ 'apiKey={apiKey}&lg={language}',
+ options: {
+ attribution:
+ 'Map © 1987-' + new Date().getFullYear() + ' HERE',
+ subdomains: '1234',
+ mapID: 'newest',
+ apiKey: '',
+ base: 'base',
+ variant: 'normal.day',
+ maxZoom: 20,
+ type: 'maptile',
+ language: 'eng',
+ format: 'png8',
+ size: '256'
+ },
+ variants: {
+ normalDay: 'normal.day',
+ normalDayCustom: 'normal.day.custom',
+ normalDayGrey: 'normal.day.grey',
+ normalDayMobile: 'normal.day.mobile',
+ normalDayGreyMobile: 'normal.day.grey.mobile',
+ normalDayTransit: 'normal.day.transit',
+ normalDayTransitMobile: 'normal.day.transit.mobile',
+ normalNight: 'normal.night',
+ normalNightMobile: 'normal.night.mobile',
+ normalNightGrey: 'normal.night.grey',
+ normalNightGreyMobile: 'normal.night.grey.mobile',
+ normalNightTransit: 'normal.night.transit',
+ normalNightTransitMobile: 'normal.night.transit.mobile',
+ reducedDay: 'reduced.day',
+ reducedNight: 'reduced.night',
+ basicMap: {
+ options: {
+ type: 'basetile'
+ }
+ },
+ mapLabels: {
+ options: {
+ type: 'labeltile',
+ format: 'png'
+ }
+ },
+ trafficFlow: {
+ options: {
+ base: 'traffic',
+ type: 'flowtile'
+ }
+ },
+ carnavDayGrey: 'carnav.day.grey',
+ hybridDay: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day'
+ }
+ },
+ hybridDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.mobile'
+ }
+ },
+ hybridDayTransit: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.day.transit'
+ }
+ },
+ hybridDayGrey: {
+ options: {
+ base: 'aerial',
+ variant: 'hybrid.grey.day'
+ }
+ },
+ pedestrianDay: 'pedestrian.day',
+ pedestrianNight: 'pedestrian.night',
+ satelliteDay: {
+ options: {
+ base: 'aerial',
+ variant: 'satellite.day'
+ }
+ },
+ terrainDay: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day'
+ }
+ },
+ terrainDayMobile: {
+ options: {
+ base: 'aerial',
+ variant: 'terrain.day.mobile'
+ }
+ }
+ }
+ },
+ FreeMapSK: {
+ url: 'https://{s}.freemap.sk/T/{z}/{x}/{y}.jpeg',
+ options: {
+ minZoom: 8,
+ maxZoom: 16,
+ subdomains: 'abcd',
+ bounds: [[47.204642, 15.996093], [49.830896, 22.576904]],
+ attribution:
+ '{attribution.OpenStreetMap}, visualization CC-By-SA 2.0 Freemap.sk'
+ }
+ },
+ MtbMap: {
+ url: 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png',
+ options: {
+ attribution:
+ '{attribution.OpenStreetMap} & USGS'
+ }
+ },
+ CartoDB: {
+ url: 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
+ options: {
+ attribution: '{attribution.OpenStreetMap} © CARTO',
+ subdomains: 'abcd',
+ maxZoom: 20,
+ variant: 'light_all'
+ },
+ variants: {
+ Positron: 'light_all',
+ PositronNoLabels: 'light_nolabels',
+ PositronOnlyLabels: 'light_only_labels',
+ DarkMatter: 'dark_all',
+ DarkMatterNoLabels: 'dark_nolabels',
+ DarkMatterOnlyLabels: 'dark_only_labels',
+ Voyager: 'rastertiles/voyager',
+ VoyagerNoLabels: 'rastertiles/voyager_nolabels',
+ VoyagerOnlyLabels: 'rastertiles/voyager_only_labels',
+ VoyagerLabelsUnder: 'rastertiles/voyager_labels_under'
+ }
+ },
+ HikeBike: {
+ url: 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 19,
+ attribution: '{attribution.OpenStreetMap}',
+ variant: 'hikebike'
+ },
+ variants: {
+ HikeBike: {},
+ HillShading: {
+ options: {
+ maxZoom: 15,
+ variant: 'hillshading'
+ }
+ }
+ }
+ },
+ BasemapAT: {
+ url: 'https://mapsneu.wien.gv.at/basemap/{variant}/{type}/google3857/{z}/{y}/{x}.{format}',
+ options: {
+ maxZoom: 19,
+ attribution: 'Datenquelle: basemap.at',
+ type: 'normal',
+ format: 'png',
+ bounds: [[46.358770, 8.782379], [49.037872, 17.189532]],
+ variant: 'geolandbasemap'
+ },
+ variants: {
+ basemap: {
+ options: {
+ maxZoom: 20, // currently only in Vienna
+ variant: 'geolandbasemap'
+ }
+ },
+ grau: 'bmapgrau',
+ overlay: 'bmapoverlay',
+ terrain: {
+ options: {
+ variant: 'bmapgelaende',
+ type: 'grau',
+ format: 'jpeg'
+ }
+ },
+ surface: {
+ options: {
+ variant: 'bmapoberflaeche',
+ type: 'grau',
+ format: 'jpeg'
+ }
+ },
+ highdpi: {
+ options: {
+ variant: 'bmaphidpi',
+ format: 'jpeg'
+ }
+ },
+ orthofoto: {
+ options: {
+ maxZoom: 20, // currently only in Vienna
+ variant: 'bmaporthofoto30cm',
+ format: 'jpeg'
+ }
+ }
+ }
+ },
+ nlmaps: {
+ url: 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/{variant}/EPSG:3857/{z}/{x}/{y}.png',
+ options: {
+ minZoom: 6,
+ maxZoom: 19,
+ bounds: [[50.5, 3.25], [54, 7.6]],
+ attribution: 'Kaartgegevens © Kadaster'
+ },
+ variants: {
+ 'standaard': 'standaard',
+ 'pastel': 'pastel',
+ 'grijs': 'grijs',
+ 'water': 'water',
+ 'luchtfoto': {
+ 'url': 'https://service.pdok.nl/hwh/luchtfotorgb/wmts/v1_0/Actueel_ortho25/EPSG:3857/{z}/{x}/{y}.jpeg',
+ }
+ }
+ },
+ NASAGIBS: {
+ url: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}',
+ options: {
+ attribution:
+ 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ' +
+ '(ESDIS) with funding provided by NASA/HQ.',
+ bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
+ minZoom: 1,
+ maxZoom: 9,
+ format: 'jpg',
+ time: '',
+ tilematrixset: 'GoogleMapsCompatible_Level'
+ },
+ variants: {
+ ModisTerraTrueColorCR: 'MODIS_Terra_CorrectedReflectance_TrueColor',
+ ModisTerraBands367CR: 'MODIS_Terra_CorrectedReflectance_Bands367',
+ ViirsEarthAtNight2012: {
+ options: {
+ variant: 'VIIRS_CityLights_2012',
+ maxZoom: 8
+ }
+ },
+ ModisTerraLSTDay: {
+ options: {
+ variant: 'MODIS_Terra_Land_Surface_Temp_Day',
+ format: 'png',
+ maxZoom: 7,
+ opacity: 0.75
+ }
+ },
+ ModisTerraSnowCover: {
+ options: {
+ variant: 'MODIS_Terra_NDSI_Snow_Cover',
+ format: 'png',
+ maxZoom: 8,
+ opacity: 0.75
+ }
+ },
+ ModisTerraAOD: {
+ options: {
+ variant: 'MODIS_Terra_Aerosol',
+ format: 'png',
+ maxZoom: 6,
+ opacity: 0.75
+ }
+ },
+ ModisTerraChlorophyll: {
+ options: {
+ variant: 'MODIS_Terra_Chlorophyll_A',
+ format: 'png',
+ maxZoom: 7,
+ opacity: 0.75
+ }
+ }
+ }
+ },
+ NLS: {
+ // NLS maps are copyright National library of Scotland.
+ // http://maps.nls.uk/projects/api/index.html
+ // Please contact NLS for anything other than non-commercial low volume usage
+ //
+ // Map sources: Ordnance Survey 1:1m to 1:63K, 1920s-1940s
+ // z0-9 - 1:1m
+ // z10-11 - quarter inch (1:253440)
+ // z12-18 - one inch (1:63360)
+ url: 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg',
+ options: {
+ attribution: 'National Library of Scotland Historic Maps',
+ bounds: [[49.6, -12], [61.7, 3]],
+ minZoom: 1,
+ maxZoom: 18,
+ subdomains: '0123',
+ }
+ },
+ JusticeMap: {
+ // Justice Map (http://www.justicemap.org/)
+ // Visualize race and income data for your community, county and country.
+ // Includes tools for data journalists, bloggers and community activists.
+ url: 'https://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
+ options: {
+ attribution: 'Justice Map',
+ // one of 'county', 'tract', 'block'
+ size: 'county',
+ // Bounds for USA, including Alaska and Hawaii
+ bounds: [[14, -180], [72, -56]]
+ },
+ variants: {
+ income: 'income',
+ americanIndian: 'indian',
+ asian: 'asian',
+ black: 'black',
+ hispanic: 'hispanic',
+ multi: 'multi',
+ nonWhite: 'nonwhite',
+ white: 'white',
+ plurality: 'plural'
+ }
+ },
+ GeoportailFrance: {
+ url: 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
+ options: {
+ attribution: 'Geoportail France',
+ bounds: [[-75, -180], [81, 180]],
+ minZoom: 2,
+ maxZoom: 18,
+ // Get your own geoportail apikey here : http://professionnels.ign.fr/ign/contrats/
+ // NB : 'choisirgeoportail' is a demonstration key that comes with no guarantee
+ apikey: 'choisirgeoportail',
+ format: 'image/png',
+ style: 'normal',
+ variant: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2'
+ },
+ variants: {
+ plan: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2',
+ parcels: {
+ options: {
+ variant: 'CADASTRALPARCELS.PARCELLAIRE_EXPRESS',
+ style: 'PCI vecteur',
+ maxZoom: 20
+ }
+ },
+ orthos: {
+ options: {
+ maxZoom: 19,
+ format: 'image/jpeg',
+ variant: 'ORTHOIMAGERY.ORTHOPHOTOS'
+ }
+ }
+ }
+ },
+ OneMapSG: {
+ url: 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
+ options: {
+ variant: 'Default',
+ minZoom: 11,
+ maxZoom: 18,
+ bounds: [[1.56073, 104.11475], [1.16, 103.502]],
+ attribution: '
New OneMap | Map data © contributors, Singapore Land Authority'
+ },
+ variants: {
+ Default: 'Default',
+ Night: 'Night',
+ Original: 'Original',
+ Grey: 'Grey',
+ LandLot: 'LandLot'
+ }
+ },
+ USGS: {
+ url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
+ options: {
+ maxZoom: 20,
+ attribution: 'Tiles courtesy of the U.S. Geological Survey'
+ },
+ variants: {
+ USTopo: {},
+ USImagery: {
+ url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}'
+ },
+ USImageryTopo: {
+ url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}'
+ }
+ }
+ },
+ WaymarkedTrails: {
+ url: 'https://tile.waymarkedtrails.org/{variant}/{z}/{x}/{y}.png',
+ options: {
+ maxZoom: 18,
+ attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © waymarkedtrails.org (CC-BY-SA)'
+ },
+ variants: {
+ hiking: 'hiking',
+ cycling: 'cycling',
+ mtb: 'mtb',
+ slopes: 'slopes',
+ riding: 'riding',
+ skating: 'skating'
+ }
+ },
+ OpenAIP: {
+ url: 'https://{s}.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{y}.{ext}',
+ options: {
+ attribution: 'openAIP Data (CC-BY-NC-SA)',
+ ext: 'png',
+ minZoom: 4,
+ maxZoom: 14,
+ tms: true,
+ detectRetina: true,
+ subdomains: '12'
+ }
+ },
+ OpenSnowMap: {
+ url: 'https://tiles.opensnowmap.org/{variant}/{z}/{x}/{y}.png',
+ options: {
+ minZoom: 9,
+ maxZoom: 18,
+ attribution: 'Map data: {attribution.OpenStreetMap} & ODbL, © www.opensnowmap.org CC-BY-SA'
+ },
+ variants: {
+ pistes: 'pistes',
+ }
+ },
+ AzureMaps: {
+ url:
+ 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+
+ '&tilesetId={variant}&x={x}&y={y}&zoom={z}&language={language}'+
+ '&subscription-key={subscriptionKey}',
+ options: {
+ attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile for details.',
+ apiVersion: '2.0',
+ variant: 'microsoft.imagery',
+ subscriptionKey: '',
+ language: 'en-US',
+ },
+ variants: {
+ MicrosoftImagery: 'microsoft.imagery',
+ MicrosoftBaseDarkGrey: 'microsoft.base.darkgrey',
+ MicrosoftBaseRoad: 'microsoft.base.road',
+ MicrosoftBaseHybridRoad: 'microsoft.base.hybrid.road',
+ MicrosoftTerraMain: 'microsoft.terra.main',
+ MicrosoftWeatherInfraredMain: {
+ url:
+ 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+
+ '&tilesetId={variant}&x={x}&y={y}&zoom={z}'+
+ '&timeStamp={timeStamp}&language={language}' +
+ '&subscription-key={subscriptionKey}',
+ options: {
+ timeStamp: '2021-05-08T09:03:00Z',
+ attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile#uri-parameters for details.',
+ variant: 'microsoft.weather.infrared.main',
+ },
+ },
+ MicrosoftWeatherRadarMain: {
+ url:
+ 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+
+ '&tilesetId={variant}&x={x}&y={y}&zoom={z}'+
+ '&timeStamp={timeStamp}&language={language}' +
+ '&subscription-key={subscriptionKey}',
+ options: {
+ timeStamp: '2021-05-08T09:03:00Z',
+ attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile#uri-parameters for details.',
+ variant: 'microsoft.weather.radar.main',
+ },
+ }
+ },
+ },
+ SwissFederalGeoportal: {
+ url: 'https://wmts.geo.admin.ch/1.0.0/{variant}/default/current/3857/{z}/{x}/{y}.jpeg',
+ options: {
+ attribution: '© swisstopo',
+ minZoom: 2,
+ maxZoom: 18,
+ bounds: [[45.398181, 5.140242], [48.230651, 11.47757]]
+ },
+ variants: {
+ NationalMapColor: 'ch.swisstopo.pixelkarte-farbe',
+ NationalMapGrey: 'ch.swisstopo.pixelkarte-grau',
+ SWISSIMAGE: {
+ options: {
+ variant: 'ch.swisstopo.swissimage',
+ maxZoom: 19
+ }
+ }
+ }
+ }
+ };
+
+ L.tileLayer.provider = function (provider, options) {
+ return new L.TileLayer.Provider(provider, options);
+ };
+
+ return L;
+}));
diff --git a/src/tasks.py b/src/tasks.py
new file mode 100644
index 0000000..fb8daeb
--- /dev/null
+++ b/src/tasks.py
@@ -0,0 +1,57 @@
+import requests
+import threading
+import json
+
+
+class TaskHandler:
+ def __init__(self):
+ self.hRIT_delayed = {}
+ self.hRIT_current = {}
+ self.updateCache()
+ TaskHandler.set_interval(self.updateCache, 60*5)
+
+# Start HotspotsRIT webworker
+ # Source for corrections: I made it up
+ corrections = {
+ "Library_3rd_Floor": {"max_occ": 550},
+ "Library_2nd_Floor": {"max_occ": 250},
+ "Library_1st_Floor": {"max_occ": 350},
+ "Library_4th_Floor": {"max_occ": 400},
+ "Library_A_Level": {"max_occ": 300},
+ "Ross_Hall": {"max_occ": 200},
+ "Gordon_Field_House": {"max_occ": 750}
+ }
+
+ def getCache(self):
+ return self.hRIT_delayed
+
+ def getCurrent(self):
+ return self.hRIT_current
+
+ def updateCache(self):
+ r = requests.get("https://maps.rit.edu/proxySearch/densityMapDetail.php?mdo=1")
+ if r.status_code == 200:
+ if self.hRIT_current == {}:
+ self.hRIT_delayed = TaskHandler.dataAdjustments(r.json())
+ else:
+ self.hRIT_delayed = json.loads(json.dumps(self.hRIT_current)) # deepcopy was returning a function for some reason
+ self.hRIT_current = TaskHandler.dataAdjustments(r.json())
+ return self.hRIT_delayed, self.hRIT_current
+ else:
+ print("FUCK!", r.status_code)
+
+ def dataAdjustments(data):
+ for dp in data:
+ if dp['location'] in TaskHandler.corrections:
+ for correction in TaskHandler.corrections[dp['location']]:
+ dp[correction] = TaskHandler.corrections[dp['location']][correction]
+ return data
+
+ def set_interval(func, sec):
+ def func_wrapper():
+ TaskHandler.set_interval(func, sec)
+ func()
+ t = threading.Timer(sec, func_wrapper)
+ t.start()
+ return t
+
diff --git a/src/templates/hotspots.html b/src/templates/hotspots.html
new file mode 100644
index 0000000..022b220
--- /dev/null
+++ b/src/templates/hotspots.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+