diff --git a/src/static/css/hotspots.css b/src/static/css/hotspots.css index 4ecae41..d27d814 100644 --- a/src/static/css/hotspots.css +++ b/src/static/css/hotspots.css @@ -8,8 +8,8 @@ body { height: 100vh; } -#map { - height: 100vh; +html, body, #map { + height: 100%; width: 100vw; } @@ -22,4 +22,18 @@ body { .leaflet-tile-pane { filter: brightness(50%); +} + +.legend { + border: solid #999999 3px; + color: #eee; + background-color: rgba(44, 44, 44, .8); + padding: 1em; +} + +#legendOccGrad { + background-image: linear-gradient(to right, transparent, red); + padding: 0 2em; + text-align: center; + text-shadow: black .2em .2em; } \ No newline at end of file diff --git a/src/static/js/hotspots.js b/src/static/js/hotspots.js index d0cf99a..af57e0f 100644 --- a/src/static/js/hotspots.js +++ b/src/static/js/hotspots.js @@ -1,430 +1,496 @@ 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, + 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; + let adjustedratio = ratio > 1 ? 1 : ratio; + let red = 255 * adjustedratio; + let style = { fillColor: `rgba(255, 0, 0, ${adjustedratio})` }; + ref.setStyle(style); + ref.bindPopup( + `${attrs.properties.name}
Current Occupation: ${attrs.properties.count}
${Math.round(ratio*100)}% capacity` + ); +} + +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 = { + pane: "nodePane", + 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); } - ).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; - } - - // 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], + } 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; +let nodePane = map.createPane("nodePane"); +nodePane.style.zIndex = "600"; +let nodeGroup; +const densityMapUrl = "/hotspotsrit"; // https://maps.rit.edu/proxySearch/densityMapDetail.php?mdo=1 + +async function init(legend = false) { + 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; + } + } + }); + + nodeGroup = L.geoJSON(Object.values(pts), { + pane: "nodePane", + 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, + }); + nodeGroup.addTo(map); + nodeGroup.bringToFront(); + + const features = nodeGroup.getLayers(); + for (let i = 0; i < features.length; i++) { + const key = features[i].feature.properties.mdo_id; + pts[key].properties.reference = features[i]; + } + + legend ? makeLegend() : null; +} + +function makeLegend() { + let legend = L.control({ position: "bottomright" }); + + legend.onAdd = function (mapref) { + let div = L.DomUtil.create("div", "info legend"); + div.innerHTML = "
Occupancy / Max Occupancy Gradient
" + div.innerHTML += + `

Markers represent locations where data is collected
+ Vectors represent the migration of aggregate occupation
+ vectors to/from nodeless points involve locations not tracked by RIT

`; + + return div; }; - 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 statControl = L.control({ position: "topright" }); + + statControl.onAdd = function (mapref) { + let div = L.DomUtil.create("div", "info legend"); + div.innerHTML = "

Occupancy is updated every 5 minutes


Next update in seconds

" + return div; + // + }; + + statControl.addTo(map); + legend.addTo(map); +} + +function updateLegend(shotcount=undefined){ + document.getElementById('shotCounter').textContent = `Previous update created ${shotcount} migration${shotcount == 1 ? "" : "s"}`; +} + +const space_coords = [43.09224, -77.674799]; +const UC_coords = [43.080361, -77.683296]; +const perkins_coords = [43.08616, -77.661796]; +function setSpace(features) { + features.forEach((x) => { + const centroid = getCoordArray(x); + if (centroid[1] > -77.673157) { + x.properties.space = perkins_coords; + } else if (centroid[1] < -77.677503 && centroid[0] < 43.08395) { + x.properties.space = UC_coords; + } else { + x.properties.space = space_coords; } - } - - // 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); + }); +} + +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)); - - 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 getCoordArray(ref) { + if (ref.properties == undefined) return ref; + let coords; + try { + coords = ref.properties.reference.getLatLng(); + } catch { + coords = ref.properties.reference.getBounds().getCenter(); } - - 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] + 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 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 +} + +let countdownTo; +function updateCountdown() { + const now = new Date().getTime(); + document.getElementById('countdownClock').textContent = Math.round((countdownTo - now) / 1000); +} + +async function getUpdate() { + console.log("Updating Occupancy Matrix"); + countdownTo = new Date().getTime() + 5 * 60 * 1000; + + 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` + ); + updateLegend(shots.length); + 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, x.properties.space]); + x.properties.diff--; + } + + while (x.properties.diff < 0) { + shots.push([x.properties.space, x]); + x.properties.diff++; + } + }); + + return shots; +} + +const useLegend = window.location.pathname.replaceAll("/", "") == "hotspots" +init(useLegend).then(() => { + // map.on("click", () => { + // shootVector(pts[2], pts[8]); + // }); + // shootVector(pts[0], pts[1], {speed: 500}); + let ptsarr = Object.values(pts); + calcDistances(ptsarr); + setSpace(ptsarr); + getUpdate(); + setInterval(updateCountdown, 1000); + setInterval(getUpdate, 60000 * 5); +}); diff --git a/src/static/json/projects.json b/src/static/json/projects.json index 2ac76b8..74a1471 100644 --- a/src/static/json/projects.json +++ b/src/static/json/projects.json @@ -1,5 +1,33 @@ { - "Lower 48 Alt. Energy Map": { + "RIT Hotspots": { + "status": "WIP", + "classes": "pinned geospacial programming", + "bgi": "hotspotsrit.png", + "content": "Live crowd migration map using RIT occupancy data", + "links": [ + [ + "github", "https://github.com/asimonson1125/hotspotsrit", "git repo" + ], + [ + "globe", "https://asimonson.com/hotspots", "demo" + ] + ] + }, + "LogicFlow": { + "status": "incomplete", + "classes": "programming", + "bgi": "logicflow.jpg", + "content": "Translate paragraphs to logical flowcharts, powered by ChatGPT Winner of CSHacks' Best Use of AI by Paychex", + "links": [ + [ + "github", "https://github.com/asimonson1125/LogicFlow", "git repo" + ], + [ + "globe", "https://devpost.com/software/logicflow", "Hackathon listing" + ] + ] + }, + "Alternative Energy Map": { "status": "complete", "classes": "pinned geospacial", "bgi": "geovisF.png", @@ -14,7 +42,7 @@ }, "OccupyRIT": { "status": "WIP", - "classes": "pinned programming", + "classes": "programming", "bgi": "occupyRIT.png", "content": "Collects RIT Gym Occupancy data, determining busiest workout times", "links": [ diff --git a/src/static/photos/hotspotsrit.png b/src/static/photos/hotspotsrit.png new file mode 100644 index 0000000..0b55591 Binary files /dev/null and b/src/static/photos/hotspotsrit.png differ diff --git a/src/static/photos/logicflow.jpg b/src/static/photos/logicflow.jpg new file mode 100644 index 0000000..7ebac96 Binary files /dev/null and b/src/static/photos/logicflow.jpg differ