// uses functions in utils.js var host = window.location.protocol + "//" + window.location.host; // "global" map variables var map; var previewmap; var visited_clues_m = L.layerGroup([]); // all clues var unvisited_clues_m = L.layerGroup([]); // subset of all clues - unvisited clues var destination_clues_m = L.layerGroup([]); // clues bikers are currently destined for var bikes_m = L.layerGroup([]); // bike markers var routes_m = L.layerGroup([]); // polyline routes var homemarker, homebase, clues, clue_rels, bikes, routes, previewmarker; var latest_timestamp = -1; // initially -1, otherwise set to value given by server in last successful info update var baseIcon = L.Icon.extend({ options: { iconSize: [25, 41], // size of the icon iconAnchor: [12, 41], // point of the icon which will correspond to marker's location popupAnchor: [0, -41] // point from which the popup should open relative to the iconAnchor } }) var homeIcon = new baseIcon({iconUrl: 'static/img/marker-home.png'}), activeBikeIcon = new baseIcon({iconUrl: 'static/img/marker-bike-active.png', className:"leaflet-bike-marker"}), inactiveBikeIcon = new baseIcon({iconUrl: 'static/img/marker-bike-inactive.png'}), visitedIcon = new baseIcon({iconUrl: 'static/img/marker-icon-grey.png'}), unvisitedIcon = new baseIcon({iconUrl: 'static/img/marker-icon-blue.png'}), // generic, becomes colored when assigned to route orangeIcon = new baseIcon({iconUrl: 'static/img/marker-icon-orange.png'}), redIcon = new baseIcon({iconUrl: 'static/img/marker-icon-red.png'}), greenIcon = new baseIcon({iconUrl: 'static/img/marker-icon-green.png'}), yellowIcon = new baseIcon({iconUrl: 'static/img/marker-icon-yellow.png'}); var bikeIcons = {'ACTIVE' : activeBikeIcon, 'INACTIVE' : inactiveBikeIcon} function zoomToBike(team_name){ map.panTo(bikes[team_name]['marker'].getLatLng()); } function zoomToClue(clue_name){ map.panTo(clue_rels[clue_name].getLatLng()); } function previewZoom(){ var long = parseFloat(document.getElementById("new_clue_longitude").value); var lat = parseFloat(document.getElementById("new_clue_latitude").value); console.log(long); console.log(document.getElementById("new_clue_longitude").value); if (!isNaN(long) && !isNaN(lat)){ previewmarker.setLatLng([lat, long]); previewmap.panTo(previewmarker.getLatLng()); } } function drawRoute(route_coords_osrm, team_color) { //osrm lat/long are swapped for (var i = 0; i < route_coords_osrm.length; i++){ var t = route_coords_osrm[i][1]; route_coords_osrm[i][1] = route_coords_osrm[i][0]; route_coords_osrm[i][0] = t; } var route = new L.polyline(route_coords_osrm, {color: team_color}); routes_m.addLayer(route); } function drawRoutes() { routes_m.clearLayers(); for (var i = 0; i < routes['clusters'].length; i++){ if(routes['clusters'][i].length > 0){ var color; var r = Math.floor(Math.random() * 155+100); var g = 0; // no greens -- avoid yellow var b = Math.floor(Math.random() * 155+100); color= "rgb("+r+","+g+","+ b+")"; console.log(color); drawRoute(routes['clusters'][i], color); } } for (var i = 0; i < routes['individual_routes'].length; i++){ if(routes['individual_routes'][i].length > 0){ drawRoute(routes['individual_routes'][i], 'yellow'); } } } function updateBikeStatus(){ var table = document.getElementById("bike-teams-table"); table.innerHTML = ''; for (const [key, value] of Object.entries(bikes)) { var name = key; var bike = value; var row = document.createElement("tr"); var namecell = document.createElement("td"); namecell.innerHTML = ""+name+""; var statuscell = document.createElement("td"); statuscell.innerHTML = "" + bike['team_status'] + " ("+parseInt(bike['time_since_last_contact']).toString()+"s)"; var targetcell = document.createElement("td"); targetcell.innerHTML = ""+bike['target_clue']+""; var etacell = document.createElement("td"); etacell.innerHTML = "ETA: "+routes['cluster_times'][key]; row.appendChild(namecell); row.appendChild(statuscell); row.appendChild(targetcell); row.appendChild(etacell); table.appendChild(row); } } function updateClueStats(){ document.getElementById("total_count").innerText = clues.length; var visited_count = 0; var avg_distance = 0; for (var i =0; i < clues.length; i++){ if(clues[i]['clue_status'] == "VISITED"){ visited_count++; avg_distance += getDistanceFromLatLon(homebase, clues[i]); } } avg_distance /= visited_count; document.getElementById("unvisited_count").innerText = clues.length-visited_count; document.getElementById("visited_count").innerText = visited_count; document.getElementById("percent_visited").innerText = (100*(visited_count/clues.length)).toFixed(2); document.getElementById("avg_visited_distance").innerText = avg_distance.toFixed(2); } function drawClues(){ unvisited_clues_m.clearLayers(); visited_clues_m.clearLayers(); for (var i = 0; i < clues.length; i++) { var tempIcon = visitedIcon; if (clues[i]['clue_status'] == "UNVISITED") tempIcon = unvisitedIcon; var popupdiv = document.createElement('p'); var toggleVisitText = clues[i]['clue_status'] == "UNVISITED" ? "Visit" : "Unvisit"; var toggleDisableText = clues[i]['clue_status'] != "DISABLED" ? "Disable" : "Enable"; popupdiv.innerHTML = "" + clues[i]['clue_name'] + ": " + clues[i]['clue_info'] + ""; popupdiv.innerHTML += ""; popupdiv.innerHTML += ""; var clueMarker = L.marker([clues[i]['latitude'], clueVisits[i]['longitude']], {icon: tempIcon}).bindPopup(popupdiv); clue_rels[clues[i]['clue_name']] = clueMarker; if (clues[i]['clue_status'] == "UNVISITED") unvisited_clues_m.addLayer(clueMarker); else visited_clues_m.addLayer(clueMarker); } } function toggleVisitClue(clue_name){ if(!confirm("Are you sure you want to visit/unvisit this clue?")) return; console.log("toggling visited status for "+clue_name); document.getElementById("visitbutton_"+clues[i]['clue_name']).disabled = true; // temporarily disable buttons until new server frame received document.getElementById("enablebutton_"+clues[i]['clue_name']).disabled = true; fetch(host+'/visitClueGeneric', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "clue_name" : clue_name })}) .then((response) => response.json()) .then((json) => console.log(json)); } function toggleEnableClue(clue_name){ console.log("toggling enabled status for "+clue_name); document.getElementById("visitbutton_"+clues[i]['clue_name']).disabled = true; // temporarily disable buttons until new server frame received document.getElementById("enablebutton_"+clues[i]['clue_name']).disabled = true; fetch(host+'/enableClue', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "clue_name" : clue_name })}) .then((response) => response.json()) .then((json) => console.log(json)); } function addClue(){ var long = parseFloat(document.getElementById("new_clue_longitude").value); var lat = parseFloat(document.getElementById("new_clue_latitude").value); if (isNaN(long) || isNaN(lat)){ alert("Invalid coordinates."); return; } if(confirm("Are you sure this is the right location?")){ console.log("yes"); } } // run every x seconds to get latest info from server function requestLatestInfo(){ function handleLatestInfo(json){ if(json['status'] != "OK") return; latest_timestamp = json['timestamp']; // process home base if (json['home_changed']){ homebase = json['homebase']; if(homemarker)homemarker.setLatLng([homebase['latitude'], homebase['longitude']]); else homemarker = L.marker([homebase['latitude'], homebase['longitude']], {icon: homeIcon}).addTo(map).bindPopup("Home is where the club is."); } // process routes if(json['routes_changed']){ routes = json['routes']; console.log(routes); drawRoutes(); } // process bikes if(true || json['bikes_changed']){ // always true since we need constant updates for bikes bikes_t = json['bikes']; for (var i = 0; i < bikes_t.length; i++){ var name = bikes_t[i]['team_name']; if(name in bikes) { bikes[name]['marker'].setLatLng([bikes_t[i]['latitude'],bikes_t[i]['longitude']]); if(bikes_t[i]['team_status'] != [name]['team_status'])bikes[name]['marker'].setIcon(bikeIcons[bikes_t[i]['team_status']]); for (const [key, value] of Object.entries(bikes_t[i])) { bikes[name][key] = value; } } else { // add bike var bikeMarker = new L.marker([bikes_t[i]['latitude'],bikes_t[i]['longitude']]).bindPopup(bikes_t[i]['team_name']); bikes[name] = bikes_t[i]; bikes[name]['marker'] = bikeMarker; bikeMarker.setIcon(bikeIcons[bikes[name]['team_status']]); bikes_m.addLayer(bikeMarker); } } // remove no longer existing bikes for (const [key, value] of Object.entries(bikes)) { var deleteBike = true; for(var i = 0; i < bikes_t.length; i++){ if(bikes_t[i]['team_name'] == key) {deleteBike = false; } } if(deleteBike) { console.log("deleting bike"); bikes_m.removeLayer(value['marker']); delete bikes[key]; } } updateBikeStatus(); } // process clues if(json['clues_changed']){ clues = json['clues']; clue_rels = {}; drawClues(); updateClueStats(); } document.getElementById("routeinfo").innerHTML = json['calculating_routes'] ? "ROUTE INFO | (Calculating...)" : "ROUTE INFO"; } fetch(host+'/getLatestInfo', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "info_age" : latest_timestamp })}) .then((response) => response.json()) .then((json) => handleLatestInfo(json)); } var intervalId = window.setInterval(function(){ requestLatestInfo(); }, 5000); var clockINterval = window.setInterval(function(){ var d = new Date(); document.getElementById("titletime").innerText = d.toLocaleString(); }, 1000); // RUN ON PAGE LOAD window.onload = function() { clues = {}; bikes = {}; routes = {}; map = L.map('map').setView([42.3626081,-71.0620591], 13); L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.{ext}?api_key=24746ff5-179d-482f-97a8-0267c5b276a1', { minZoom: 0, maxZoom: 20, attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', ext: 'png' }).addTo(map); previewmap = L.map('previewmap').setView([42.3626081,-71.0620591], 16); L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.{ext}?api_key=24746ff5-179d-482f-97a8-0267c5b276a1', { minZoom: 0, maxZoom: 20, ext: 'png' }).addTo(previewmap); //homemarker = L.marker([homebase['latitude'], homebase['longitude']], {icon: homeIcon}).addTo(map).bindPopup("Home is where the club is."); previewmarker = L.marker([0,0], {icon: greenIcon}).addTo(previewmap); map.addLayer(visited_clues_m); map.addLayer(unvisited_clues_m); map.addLayer(bikes_m); map.addLayer(routes_m); var layerControl = L.control.layers(null,null,{collapsed:false}); layerControl.addOverlay(unvisited_clues_m, "Unvisited Clues"); layerControl.addOverlay(visited_clues_m, "Visited Clues"); layerControl.addOverlay(bikes_m, "Bikes"); layerControl.addTo(map); requestLatestInfo(); }