From 43d9812406393fa5ed95b9ab2232bbb41e1a0a05 Mon Sep 17 00:00:00 2001 From: Andrew Simonson Date: Tue, 24 Oct 2023 17:06:48 -0400 Subject: [PATCH] add hotspotsRIT page --- src/app.py | 14 + src/static/css/hotspots.css | 25 + src/static/js/hotspots.js | 430 ++++++++ src/static/js/lib/CUSTOM.leaflet.curve.js | 558 ++++++++++ src/static/js/lib/leaflet-providers.js | 1178 +++++++++++++++++++++ src/tasks.py | 57 + src/templates/hotspots.html | 21 + 7 files changed, 2283 insertions(+) create mode 100644 src/static/css/hotspots.css create mode 100644 src/static/js/hotspots.js create mode 100644 src/static/js/lib/CUSTOM.leaflet.curve.js create mode 100644 src/static/js/lib/leaflet-providers.js create mode 100644 src/tasks.py create mode 100644 src/templates/hotspots.html 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 @@ + + + + + + + + + +
+ +