// 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 p_routes_m = L.layerGroup([]); // var homemarker, homebase, clues, clue_rels, bikes, routes, p_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-green.png'}), disabledIcon = new baseIcon({iconUrl: 'static/img/marker-icon-black.png'}), unvisitedIcon = new baseIcon({iconUrl: 'static/img/marker-icon-grey.png'}), // generic, becomes colored when assigned to route visitedIconReq = new baseIcon({iconUrl: 'static/img/marker-icon-green-req.png'}), disabledIconReq = new baseIcon({iconUrl: 'static/img/marker-icon-black-req.png'}), unvisitedIconReq = new baseIcon({iconUrl: 'static/img/marker-icon-grey-req.png'}); // generic, becomes colored when assigned to route var teamIcons = [ unvisitedIcon, new baseIcon({iconUrl: 'static/img/marker-icon-orange.png'}), new baseIcon({iconUrl: 'static/img/marker-icon-red.png'}), new baseIcon({iconUrl: 'static/img/marker-icon-blue.png'}), new baseIcon({iconUrl: 'static/img/marker-icon-yellow.png'}) ]; var teamIconsReq = [ unvisitedIconReq, new baseIcon({iconUrl: 'static/img/marker-icon-orange-req.png'}), new baseIcon({iconUrl: 'static/img/marker-icon-red-req.png'}), new baseIcon({iconUrl: 'static/img/marker-icon-blue-req.png'}), new baseIcon({iconUrl: 'static/img/marker-icon-yellow-req.png'}) ]; team1IconReq = new baseIcon({iconUrl: 'static/img/marker-icon-orange.png'}), team2IconReq = new baseIcon({iconUrl: 'static/img/marker-icon-red.png'}), team3IconReq = new baseIcon({iconUrl: 'static/img/marker-icon-green.png'}), team4Icon = new baseIcon({iconUrl: 'static/img/marker-icon-yellow.png'}); var bikeIcons = {'ACTIVE' : activeBikeIcon, 'INACTIVE' : inactiveBikeIcon, 'DISABLED' : inactiveBikeIcon} var bikeStatusOpacities = {"ACTIVE" : 1.0, "INACTIVE" : 0.75, "DISABLED" : 0.0}; 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); if (!isNaN(long) && !isNaN(lat)){ previewmarker.setLatLng([lat, long]); previewmap.panTo(previewmarker.getLatLng()); } } function filterClues(text){ for(var i = 0; i < clues.length; i++){ var clue = clues[i]['clue_name']; if((text == "") || (clue.includes(text))){ clue_rels[clue].setOpacity(1.0); } else { clue_rels[clue].setOpacity(0.25); } } } function drawRoute(route_coords_osrm, team_color, layer) { //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}); layer.addLayer(route); } function drawRoutes() { routes_m.clearLayers(); p_routes_m.clearLayers(); for (var i = 0; i < p_routes['clusters'].length; i++){ if(p_routes['clusters'][i].length > 0){ var color; var shade = i*35+100; var r = shade; var g = shade; // grey previews var b = shade; color= "rgb("+r+","+g+","+ b+")"; console.log(color); drawRoute(p_routes['clusters'][i], color, p_routes_m); } } for (var i = 0; i < p_routes['individual_routes'].length; i++){ if(p_routes['individual_routes'][i].length > 0){ drawRoute(p_routes['individual_routes'][i], 'white', p_routes_m); } } for (var i = 0; i < routes['clusters'].length; i++){ if(routes['clusters'][i].length > 0){ var color; var shade = i*50+100; var r = shade; var g = 0; // no greens -- avoid yellow var b = shade-50; color= "rgb("+r+","+g+","+ b+")"; console.log(color); drawRoute(routes['clusters'][i], color, routes_m); } } for (var i = 0; i < routes['individual_routes'].length; i++){ if(routes['individual_routes'][i].length > 0){ drawRoute(routes['individual_routes'][i], 'yellow', routes_m); } } } function updateBikeStatus(){ var table = document.getElementById("bike-teams-table"); table.innerHTML = ''; var index = 1; for (const [key, value] of Object.entries(bikes)) { var name = key; var bike = value; var row = document.createElement("tr"); if(bike['team_status'] == "DISABLED")row.classList.add("disabled-class"); 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 = " "; row.appendChild(namecell); row.appendChild(statuscell); row.appendChild(targetcell); row.appendChild(etacell); table.appendChild(row); index += 1; } } 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 = teamIcons[clues[i]['assigned_team']]; if(clues[i]['clue_required']){ tempIcon = teamIconsReq[clues[i]['assigned_team']]; if (clues[i]['clue_status'] == "UNVISITED") tempIcon = unvisitedIconReq; if (clues[i]['clue_status'] == "VISITED") tempIcon = visitedIconReq; } else{ if (clues[i]['clue_status'] == "UNVISITED") tempIcon = unvisitedIcon; if (clues[i]['clue_status'] == "VISITED") tempIcon = visitedIcon; } 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 += ""; popupdiv.innerHTML += "
"; popupdiv.innerHTML += "
Assigned team: "; var clueMarker = L.marker([clues[i]['latitude'], clues[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); popupdiv.querySelector("#assignedteam_"+clues[i]['clue_name']).selectedIndex = clues[i]['assigned_team']; } } function assignedTeamChanged(clue_name, team){ fetch(host+'/setClueTeam', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "clue_name" : clue_name, "bike_team": parseInt(team)})}) // unvisit (if appropriate) .then((response) => response.json()) .then((json) => console.log(json)); } function toggle_visit_clue(clue_name){ if(!confirm("Are you sure you want to visit/unvisit this clue?")) return; console.log("toggling visited status for "+clue_name); var clue = get_clue_by_name(clue_name); document.getElementById("visitbutton_"+clue['clue_name']).disabled = true; // temporarily disable buttons until new server frame received document.getElementById("enablebutton_"+clue['clue_name']).disabled = true; document.getElementById("requirebutton_"+clue['clue_name']).disabled = true; fetch(host+'/visitClueGeneric', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "clue_name" : clue_name, "unvisit": true})}) // unvisit (if appropriate) .then((response) => response.json()) .then((json) => console.log(json)); } function toggle_required_clue(clue_name){ if(!confirm("Are you sure you want to mark/unmark this clue?")) return; console.log("toggling visited status for "+clue_name); var clue = get_clue_by_name(clue_name); document.getElementById("visitbutton_"+clue['clue_name']).disabled = true; // temporarily disable buttons until new server frame received document.getElementById("enablebutton_"+clue['clue_name']).disabled = true; document.getElementById("requirebutton_"+clue['clue_name']).disabled = true; fetch(host+'/requireClue', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "clue_name" : clue_name, "unvisit": true})}) // unvisit (if appropriate) .then((response) => response.json()) .then((json) => console.log(json)); } function get_clue_by_name(clue_name){ for (var i = 0; i < clues.length; i++) if(clues[i]['clue_name'] == clue_name) return clues[i]; return NaN; } function toggle_enable_clue(clue_name){ if(!confirm("Are you sure you want to enable/disable this clue?")) return; console.log("toggling enabled status for "+clue_name); var clue = get_clue_by_name(clue_name); document.getElementById("visitbutton_"+clue['clue_name']).disabled = true; // temporarily disable buttons until new server frame received document.getElementById("enablebutton_"+clue['clue_name']).disabled = true; document.getElementById("requirebutton_"+clue['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 toggleBikeEnable(team_index){ var bike = bikes["Team "+team_index]; team_index = parseInt(team_index); var page = "/disableTeam"; if(bike['team_status'] == "DISABLED"){ page = "/enableTeam"; } if(page == "/disableTeam") { if(!confirm("Are you sure you want to disable this team?"))return; } fetch(host+page, { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "team_index" : team_index })}).then((response) => requestLatestInfo()); } function addClue(){ var clue_name = document.getElementById("new_clue_name").value; var clue_info = document.getElementById("new_clue_info").value; 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("adding clue..."); fetch(host+'/addClue', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "clue_name" : clue_name, "clue_info" : clue_info, "latitude" : lat, "longitude" : long})}) .then((response) => response.json()) .then((json) => { console.log(json); if(json['status'] != "OK") alert(json['status']); else alert("Clue added successfully."); }); } } // run every x seconds to get latest info from server function requestLatestInfo(){ console.log("requesting update"); 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']; p_routes = json['preview_routes']; drawRoutes(); } // process bikes if(true || json['bikes_changed']){ // always true since we need constant updates for bikes var 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']]); bikes[name]['marker'].setIcon(bikeIcons[bikes_t[i]['team_status']]); bikes[name]['marker'].setOpacity(bikeStatusOpacities[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']]); bikeMarker.setOpacity(bikeStatusOpacities[bikes_t[i]['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... (Force Stop))" : "ROUTE INFO"; if(json['routes_to_commit'])document.getElementById("routeinfo").innerHTML += "| Commit preview routes" } 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)); } function stopRouting(){ var formData = new FormData(); formData.append("command", "stopGenerating"); fetch(host+'/controls', { method: "POST", body: formData }).then( res => requestLatestInfo()); } function commitRoutes(){ var formData = new FormData(); formData.append("command", "commitRoutes"); fetch(host+'/controls', { method: "POST", body: formData }).then( res => requestLatestInfo()); } function onDeadlineChange(element){ var team_index = parseInt(element.id.split("_")[1]); var new_deadline = element.value; fetch(host+'/updateTeamDeadline', { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ "team_index" : team_index, "new_deadline" : new_deadline})}) .then((response) => response.json()) .then((json) => { console.log(json); if(json['status'] != "OK") alert(json['status']); }); } 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: visitedIcon}).addTo(previewmap); map.addLayer(visited_clues_m); map.addLayer(unvisited_clues_m); map.addLayer(bikes_m); map.addLayer(routes_m); map.addLayer(p_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.addOverlay(routes_m, "Routes"); layerControl.addOverlay(p_routes_m, "Route Previews"); layerControl.addTo(map); requestLatestInfo(); }