diff options
| author | itsGarrin <garrin.shieh@gmail.com> | 2023-11-07 10:40:14 -0500 |
|---|---|---|
| committer | itsGarrin <garrin.shieh@gmail.com> | 2023-11-07 10:40:14 -0500 |
| commit | e2e38322580304b8c0168f3e3a8a3986d229b0fd (patch) | |
| tree | b4d6f99882ed5628b9447893e42e8421b4517d91 /dashboard_website | |
| parent | 8dad8eaf2d1a9992e2779053f306f6d0736886bf (diff) | |
| parent | 20f22f05a8a7b049c6946ac056773bef954642e9 (diff) | |
Merge remote-tracking branch 'origin/master'
Diffstat (limited to 'dashboard_website')
| -rw-r--r-- | dashboard_website/__pycache__/db.cpython-311.pyc | bin | 0 -> 9976 bytes | |||
| -rw-r--r-- | dashboard_website/all_clues.csv | 169 | ||||
| -rw-r--r-- | dashboard_website/dashboard.py | 39 | ||||
| -rw-r--r-- | dashboard_website/db.py | 168 | ||||
| -rw-r--r-- | dashboard_website/static/css/dashboard.css | 13 | ||||
| -rw-r--r-- | dashboard_website/static/css/leaflet_mods.css | 4 | ||||
| -rw-r--r-- | dashboard_website/static/fonts/HWYGOTH.ttf | bin | 0 -> 53012 bytes | |||
| -rw-r--r-- | dashboard_website/static/img/favicon.ico | bin | 0 -> 4286 bytes | |||
| -rw-r--r-- | dashboard_website/static/img/marker-bike-active.png | bin | 0 -> 8063 bytes | |||
| -rw-r--r-- | dashboard_website/static/img/marker-bike-inactive.png (renamed from dashboard_website/static/img/marker-bike.png) | bin | 7511 -> 7511 bytes | |||
| -rw-r--r-- | dashboard_website/static/img/marker-home.png | bin | 0 -> 7622 bytes | |||
| -rw-r--r-- | dashboard_website/static/js/dashboard.js | 210 | ||||
| -rw-r--r-- | dashboard_website/static/js/map.js | 6 | ||||
| -rw-r--r-- | dashboard_website/static/js/utils.js | 18 | ||||
| -rw-r--r-- | dashboard_website/templates/index.html | 169 |
15 files changed, 772 insertions, 24 deletions
diff --git a/dashboard_website/__pycache__/db.cpython-311.pyc b/dashboard_website/__pycache__/db.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..09dc8b3 --- /dev/null +++ b/dashboard_website/__pycache__/db.cpython-311.pyc diff --git a/dashboard_website/all_clues.csv b/dashboard_website/all_clues.csv new file mode 100644 index 0000000..f7a5ceb --- /dev/null +++ b/dashboard_website/all_clues.csv @@ -0,0 +1,169 @@ +521 Commercial Street #525,"42.3688272,-71.0553792" +Acorn St,"42.3576234,-71.0688746" +Arlington's Great Meadows,"42.4299758,-71.2038948" +Arthur Fiedler Statue,"42.3565057,-71.0754527" +BU Beach,"42.3511927,-71.1060828" +Blaze Pizza,"42.3446263,-71.0969274" +Bonchon Allston,"42.35304,-71.130887" +Boston Athenaeum,"42.3579151,-71.0620802" +Boston Green Academy,"42.3501823,-71.1459593" +Boston Irish Famine Memorial,"42.357357,-71.0586014" +Boston Massacre Site,"42.3587627,-71.0572023" +Bova's Bakery,"42.36521,-71.0556268" +Brighton High School,"42.3495825,-71.1460435" +Brookline Booksmith,"42.3426377,-71.1217152" +Citrus & Salt Boston,"42.3489004,-71.0720926" +Cocoanut Grove Memorial Plaque,"42.3500079,-71.067859" +Commodore John Barry Memorial,"42.3556154,-71.0632036" +Cypress Street Playground,"42.331864,-71.1258765" +Dana-Farber Cancer Institute,"42.3364675,-71.1095021" +Danehy Park,"42.3890049,-71.133103" +Dave's Hot Chicken,"42.3248471,-71.0620134" +Domingo F. Sarmiento Statue,"42.3500031,-71.0851891" +Dutch House,"42.3360385,-71.1123834" +Earl of Sandwich,"42.354296,-71.066414" +Echo Bridge,"42.3145041,-71.2273649" +Farmers Horse Coffee,"42.341987,-71.0834061" +Fenway High School,"42.3306454,-71.0992038" +Isabella Stewart Gardner Museum,"42.3381442,-71.0990577" +James Michael Curley Statues,"42.3604952,-71.0569649" +James P. Kelleher Rose Garden,"42.3419564,-71.0949218" +Japanese Bell,"42.3413301,-71.0942861" +Joe Moakley Park,"42.3256817,-71.0498714" +John Eliot Square,"42.329969,-71.0908104" +Lafayette City Center,"42.3537983,-71.0616035" +Leader Bank Pavilion,"42.3485465,-71.0359433" +Leif Erikson Statue,"42.3490205,-71.0913583" +Lilly's Gourmet Pasta Express,"42.3323776,-71.1000217" +LimeRed Teahouse (Boston),"42.3518397,-71.1241295" +Longyear Museum,"42.3245965,-71.1618052" +Massachusetts State House,"42.3587772,-71.0638101" +"McMullen Museum of Art, Boston College","42.340795,-71.1625829" +Menotomy Rocks Park,"42.4107892,-71.167854" +Metropolitan Waterworks Museum,"42.3317473,-71.155555" +Mr. Crêpe,"42.3965778,-71.1227278" +Muffin House Cafe,"42.3140229,-71.3598149" +Petsi Pies,"42.3836229,-71.1126695" +Polcari’s Coffee,"42.3640137,-71.0555003" +Porter,"42.3884,-71.119149" +Puerto Rican Veterans Memorial,"42.3407613,-71.0712561" +"Skinny House (Spite House) Boston, MA","42.3668968,-71.0561781" +Soldiers and Sailors Monument,"42.3554589,-71.0664019" +Starbucks,"42.359349,-71.059228" +Steinert Hall,"42.3524116,-71.0668408" +Sugar and Spice Ice Cream Cafe,"42.2961434,-71.0872846" +TD Garden,"42.366198,-71.062146" +Tasca Restaurant,"42.3433772,-71.1427371" +The Bagel Table,"42.3569102,-71.1438455" +The Great Elm,"42.3553972,-71.0651214" +Tiananmen Memorial,"42.3509517,-71.0596124" +Yankee Lobster,"42.3478381,-71.0359354" +Yas Chicken - Allston,"42.3525708,-71.1313443" +swissbakers,"42.3631904,-71.1284677" +"""The Charlestown Bells"" by Paul Matisse","42.3691906,-71.061757" +"42°23'18.8""N 71°07'09.5""W","42.388547,-71.119301" +Bow Market,"42.381008,-71.097883" +Cambridge Public Library,"42.3741209,-71.1107166" +Charlestown High School,"42.3803747,-71.0609962" +Christopher Columbus Waterfront Park,"42.3609921,-71.0516339" +Dawes Island,"42.3754427,-71.1194344" +Desfina Restaurant,"42.3675275,-71.0809932" +Dino Safari Boston: A Walk-Thru Adventure,"42.3597994,-71.0545357" +F. A. Kennedy Steam Bakery,"42.3627462,-71.1013044" +Forge Baking Co & Ice Cream Bar,"42.3838224,-71.1108423" +Greentown Labs,"42.3820702,-71.1026937" +Harvard Square,"42.373465,-71.1189467" +Igor Fokin memorial,"42.3732344,-71.1208817" +John F. Kennedy Presidential Library and Museum,"42.316274,-71.0342146" +Lechmere Canal Park,"42.3695046,-71.0756902" +Lynch Family Skatepark,"42.3701829,-71.0678704" +Machu Chicken,"42.3799095,-71.0968274" +Millers River Potato Monument,"42.3718401,-71.0656594" +"Museum of Fine Arts, Boston","42.339381,-71.094048" +P & E Microcomputer Systems Inc,"42.3621177,-71.1854722" +Peabody Museum of Archaeology and Ethnology,"42.3782386,-71.1146697" +Prospect Hill Monument,"42.3817274,-71.0935443" +Spirit Halloween,"42.3551807,-71.0611749" +Stata Center,"42.3616095,-71.0906355" +The Collection of Historical Scientific Instruments at the Putnam Gallery,"42.3766442,-71.1161887" +The Engine,"42.3627993,-71.0962734" +The Harvard Museum of Natural History,"42.3784629,-71.1155576" +The Mµseum (tiny museum),"42.3797674,-71.0949101" +The Nu Do' Society,"42.3640287,-71.1087411" +USS Constitution Museum,"42.3739796,-71.0554239" +Union Square,"42.37736,-71.09476" +University Park Commons,"42.3614115,-71.1014951" +Veggie Crust - Somerville,"42.3822934,-71.1024769" +Veggie Galaxy,"42.3636597,-71.1011111" +Warren Tavern,"42.3741694,-71.0631664" +"42°21'41.5""N 71°03'24.6""W","42.361531,-71.056823" +All Saints Church,"42.2857047,-71.0632852" +Ashmont,"42.2845163,-71.0637877" +Boston Children's Museum,"42.3519736,-71.0496839" +Boston College High School,"42.3162356,-71.0454645" +Boston Design Center,"42.3441918,-71.0336324" +Boston Fire Museum,"42.3508756,-71.0487437" +Boston Tea Party Ships & Museum,"42.3521821,-71.0512911" +Braintree,"42.2075316,-71.0013637" +Bunker Hill Monument,"42.3763541,-71.0607764" +Calf Pasture Pumping Station,"42.316031,-71.0374911" +Castle Island,"42.3378699,-71.0125206" +Edgar Allan Poe Statue,"42.3523158,-71.0672898" +Forbes Hill Standpipe,"42.2576602,-71.02832" +Glenn’s Kreme&Kone at the Hood Milk Bottle,"42.3516479,-71.0502126" +ICA Watershed,"42.3639107,-71.0331956" +Institute of Contemporary Art,"42.3528151,-71.0432778" +John Adams Birthplace - Adams National Historical Park,"42.2392354,-71.0035279" +John Joseph Moakley United States Courthouse,"42.3537343,-71.0470633" +L Street Bathhouse,"42.3291218,-71.0352443" +Massachusetts Historical Society,"42.3463992,-71.0898829" +O B's Cafe,"42.2743442,-71.0240951" +Pleasure Bay,"42.3358743,-71.0234949" +Revere Beach,"42.420226,-70.985881" +Schoolhouse Pizza,"42.2454086,-71.0005483" +"South Boston Korean War Memorial, Castle Island, South Boston, - Massachusetts, USA","42.3367603,-71.0096371" +Taiyaki NYC - Boston,"42.3509709,-71.0447796" +Tavern of Tales: Café & Bar,"42.3319001,-71.0983169" +The Clam Box,"42.2763168,-71.0092883" +The Partisans,"42.3478375,-71.0404428" +Union Oyster House,"42.361288,-71.056908" +Victoria's Diner,"42.3270498,-71.0667744" +Wollaston Beach,"42.2806539,-71.0119933" +Abbondanza,"42.4074484,-71.0618764" +Aeronaut Cannery & Taproom,"42.3986053,-71.0612182" +Andrew McArdle Bridge,"42.3855456,-71.0392667" +BearMoose Brewing Company,"42.4025721,-71.0515875" +Belle Isle Observation Tower,"42.3917606,-70.9903023" +Brasil On Ferry,"42.4206339,-71.055873" +Broadway Dairy Maid,"42.4222989,-71.0433886" +Colonel William Prescott Statue,"42.3761612,-71.06088" +Consulado de Honduras en Boston,"42.3936888,-71.0412802" +Costco Wholesale,"42.3968978,-71.0714924" +East Boston Branch of the Boston Public Library,"42.3778389,-71.0282154" +East Boston High School,"42.3809511,-71.0350852" +East Boston YMCA,"42.3734483,-71.0331398" +Fort Heath Park,"42.3895122,-70.9693867" +Fort Hill Tower,"42.3253252,-71.0945712" +Governor Bellingham-Cary House,"42.398422,-71.0280157" +Harry Della Russo Stadium,"42.4114215,-71.0155516" +Hook & Reel Cajun Seafood & Bar,"42.4110462,-70.993656" +Judie Dyer Park,"42.3976519,-71.0355621" +Madonna Queen of the Universe Shrine,"42.390191,-71.0056995" +Marao Burgers Everett,"42.403759,-71.0589219" +Maverick House Tavern,"42.3698284,-71.037937" +My Guatemala Bakery 2,"42.3903823,-71.0386285" +Newbridge Cafe,"42.4122481,-71.0316196" +PORT Park,"42.3861321,-71.0328839" +Paws & Play Dog Park,"42.4213082,-71.0270609" +Peach's & Cream,"42.391236,-71.0366491" +Piers Park,"42.3649623,-71.0361399" +Porrazzo Skating Rink,"42.3827415,-71.0116946" +Revere Karate Academy,"42.4183123,-70.9973058" +Sphere Luxury Apartments,"42.4008442,-71.1122037" +Suffolk Downs,"42.390501,-70.997123" +Super Burritos Mexican Grill,"42.41826,-71.0506461" +The Quiet Few,"42.3670906,-71.0359889" +The Tall Ship Boston,"42.3649544,-71.0414523" +Toasted Flats,"42.3711266,-71.0371343" +Vega Market,"42.3891835,-71.033703" +Winthrop High School,"42.3803348,-70.9799864" diff --git a/dashboard_website/dashboard.py b/dashboard_website/dashboard.py index 3e73c4e..6d4d1f7 100644 --- a/dashboard_website/dashboard.py +++ b/dashboard_website/dashboard.py @@ -3,7 +3,7 @@ # dashboard.py contains web interface to clue DB + router # -from flask import Flask, flash, request, redirect, render_template, send_from_directory +from flask import Flask, flash, request, redirect, render_template, send_from_directory, jsonify import db app = Flask(__name__) @@ -78,19 +78,40 @@ def updateTeamLocation(): pass # -# WEB PAGES +# WEB PAGES + DASHBOARD API # +# send updated bike/clue/home info +# POST = request above +@app.route("/getLatestInfo", methods=['POST']) +def getLatestInfo(): + db.moveBike2Test() + content = request.get_json() + last_timestamp = content['info_age'] + data = {'timestamp' : db.getTime(), + 'clues_changed' : False, + 'home_changed' : False} + cl = db.getCluesJSON(last_timestamp) + if cl != False: + data['clues_changed'] = True + data['clues'] = cl + h = db.getHomeBaseJSON(last_timestamp) + if h != False: + data['home_changed'] = True + data['homebase'] = h + data['bikes'] = db.getBikesJSON() + data['status'] = "OK" + + return jsonify(data) + + # main page # GET = get main page -# POST = request clue/bike updates -@app.route("/", methods=['GET', 'POST']) +@app.route("/", methods=['GET']) def siteIndex(): - if request.method == "GET": - clues = db.getClues(); bikes = db.getBikes() - return render_template("index.html", clues=clues, bikes=bikes) - else: - return + #clues = db.getClues(); bikes = db.getBikes() + return render_template("index.html")#, clues=clues, bikes=bikes) + if __name__ == "__main__": diff --git a/dashboard_website/db.py b/dashboard_website/db.py index fc9e992..450360c 100644 --- a/dashboard_website/db.py +++ b/dashboard_website/db.py @@ -1,3 +1,171 @@ # stores and manages clue DB # also manages currently available bike teams +import csv, time +# time since last ping before deactivating/deleting +BIKE_TIMEOUT = 180 +BIKE_DELETE = 1800 # time before bike deletes itself + +# data structures +class Point: + def __init__(self, lat, long): + self.longitude = long + self.latitude = lat + + def toJSON(self): + json_dict = {'longitude' : self.longitude, + 'latitude' : self.latitude} + return json_dict + + def setCoords(self, lat, long): + self.longitude = long + self.latitude = lat + + def move(self, d_lat, d_long): + self.longitude += d_long + self.latitude += d_lat + + def __str__(self): + return f"{self.longitude},{self.latitude}" + +class Clue(Point): + def __init__(self, lat, long, name, info, status): + self.longitude = long + self.latitude = lat + self.name = name + self.info = info + self.status = status # UNVISITED | VISITED + + def visit(self): + self.status = "VISITED" + + def toJSON(self): + json_dict = {'longitude' : self.longitude, + 'latitude' : self.latitude, + 'clue_name' : self.name.replace('"', "'"), + 'clue_info' : self.info.replace('"', "'"), + 'clue_status' : self.status} + return json_dict + +class Bike(Point): + def __init__(self, lat, long, name, status): + self.longitude = long + self.latitude = lat + self.name = name + self.last_contact = time.time() + self.target = "N/A" + self.status = status # ACTIVE | INACTIVE + + def setTarget(self, clue_name): + self.target = clue_name + + def ping(self): + self.status = "ACTIVE" + self.last_contact = time.time() + + def checkStatus(self): + if time.time() - self.last_contact > BIKE_TIMEOUT: + self.status = "INACTIVE" + if time.time() - self.last_contact > BIKE_DELETE: + return -1 + return 0 + + def toJSON(self): + json_dict = {'longitude' : self.longitude, + 'latitude' : self.latitude, + 'time_since_last_contact' : time.time()-self.last_contact, + 'team_name' : self.name, + 'team_status' : self.status, + 'target_clue' : self.target} + return json_dict + +# variables +homeBase = Point(42.340226, -71.088395) # krentzman, can be changed on dashboard +clues = [] +bikes = [] +clues_last_changed = time.time() +home_last_changed = time.time() + + + +# interface functions +def getTime(): + return time.time() + +def getHomeBaseJSON(timestamp): + if timestamp < 0 or home_last_changed - timestamp > 0: + return homeBase.toJSON() + return False + +def setHomeBase(latitude, longitude): + homeBase.setCoords(latitude, longitude) + home_last_changed = time.time() + + +def addBike(team_name, latitude, longitude): + for bike in bikes: + if bike.name == team_name: # already exists + bike.ping() + bike.setCoords(latitude, longitude) + return # return code indicating already exists, although status still OK + newBike = Bike(latitude, longitude, team_name, "ACTIVE") + bikes.append(newBike) + +def pingBike(team_name, latitude, longitude): + for bike in bikes: + if bike.name == team_name: + bike.ping() + bike.setCoords(latitude, longitude) + break + else: # bike team does not exist yet + newBike = Bike(latitude, longitude, team_name, "ACTIVE") + bikes.append(newBike) + + +def getBikesJSON(): + global bikes + bikes = [x for x in bikes if x.checkStatus() >= 0] + return [x.toJSON() for x in bikes] + + +def getCluesJSON(timestamp): + if timestamp < 0 or clues_last_changed - timestamp > 0: + return [x.toJSON() for x in clues] + return False + +def addClue(clue_name, clue_info, longitude, latitude, visited="UNVISITED"): + newClue = Clue(latitude, longitude, clue_name, clue_info, visited) + clues.append(newClue) + clues_last_changed = time.time() + +def deleteClue(clue_name): + for i, clue in enumereate(clues): + if clue.name == clue_name: + clues.pop(i) + clues_last_changed = time.time() + break + +def visitClue(clue_name): + for clue in clues: + if clue.name == clue_name: + clue.visit() + clues_last_changed = time.time() + +# junk for testing +with open("all_clues.csv", newline='') as f: + csvreader = csv.reader(f, delimiter=',', quotechar='"') + i = 1 + for row in csvreader: + coords = row[1].split(",") + coords[0] = float(coords[0]); coords[1] = float(coords[1]); + + newClue = Clue(coords[0], coords[1], f"Clue #{i}", row[0], "UNVISITED" if i < 50 else "VISITED") + clues.append(newClue) + i += 1 + +bike1 = Bike(42.340226, -71.088395, 'speedster', 'ACTIVE') +bike2 = Bike(42.320226, -71.100395, 'slowpoke', "ACTIVE") +bike1.setTarget("Clue #6") +def moveBike2Test(): + bike1.move(0, -0.001); bike1.ping(); +bikes.append(bike1); bikes.append(bike2)
\ No newline at end of file diff --git a/dashboard_website/static/css/dashboard.css b/dashboard_website/static/css/dashboard.css new file mode 100644 index 0000000..b4110a7 --- /dev/null +++ b/dashboard_website/static/css/dashboard.css @@ -0,0 +1,13 @@ + +.icon-class { + transition: all 5s; /* time between GPS pings */ +} + +@font-face { + font-family: "HWYGOTH"; + src: url("/static/fonts/HWYGOTH.ttf"); +} +* { + font-family:HWYGOTH; + color: lightgray; +} diff --git a/dashboard_website/static/css/leaflet_mods.css b/dashboard_website/static/css/leaflet_mods.css deleted file mode 100644 index 5fbb907..0000000 --- a/dashboard_website/static/css/leaflet_mods.css +++ /dev/null @@ -1,4 +0,0 @@ - -.icon-class { - transition: all 5s; /* time between GPS pings */ -} diff --git a/dashboard_website/static/fonts/HWYGOTH.ttf b/dashboard_website/static/fonts/HWYGOTH.ttf Binary files differnew file mode 100644 index 0000000..20ac3e2 --- /dev/null +++ b/dashboard_website/static/fonts/HWYGOTH.ttf diff --git a/dashboard_website/static/img/favicon.ico b/dashboard_website/static/img/favicon.ico Binary files differnew file mode 100644 index 0000000..7f391c2 --- /dev/null +++ b/dashboard_website/static/img/favicon.ico diff --git a/dashboard_website/static/img/marker-bike-active.png b/dashboard_website/static/img/marker-bike-active.png Binary files differnew file mode 100644 index 0000000..f4f0ae6 --- /dev/null +++ b/dashboard_website/static/img/marker-bike-active.png diff --git a/dashboard_website/static/img/marker-bike.png b/dashboard_website/static/img/marker-bike-inactive.png Binary files differindex 48dbf96..48dbf96 100644 --- a/dashboard_website/static/img/marker-bike.png +++ b/dashboard_website/static/img/marker-bike-inactive.png diff --git a/dashboard_website/static/img/marker-home.png b/dashboard_website/static/img/marker-home.png Binary files differnew file mode 100644 index 0000000..1c63656 --- /dev/null +++ b/dashboard_website/static/img/marker-home.png diff --git a/dashboard_website/static/js/dashboard.js b/dashboard_website/static/js/dashboard.js new file mode 100644 index 0000000..b54f65f --- /dev/null +++ b/dashboard_website/static/js/dashboard.js @@ -0,0 +1,210 @@ +// 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 homemarker, homebase, clues, clue_rels, bikes, 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}).addTo(map); +} + +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 = "<a href=\"#\" onclick=\"zoomToBike('"+name+"')\">"+name+"</a>"; + var statuscell = document.createElement("td"); statuscell.innerHTML = "<span "+((bike['team_status'] == "ACTIVE")? "style=\"color:lightgreen\"" : "") +">" + bike['team_status'] + " ("+parseInt(bike['time_since_last_contact']).toString()+"s)</span>"; + var targetcell = document.createElement("td"); targetcell.innerHTML = "<a href=\"#\" onclick=\"zoomToClue('"+bike['target_clue']+"')\">"+bike['target_clue']+"</a>"; + + row.appendChild(namecell); + row.appendChild(statuscell); + row.appendChild(targetcell); + 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 clueMarker = L.marker([clues[i]['latitude'], clues[i]['longitude']], {icon: tempIcon}).bindPopup(clues[i]['clue_name'] + ": " + clues[i]['clue_info']); + 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 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 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']]); + 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); + } + } + updateBikeStatus(); + } + // process clues + if(json['clues_changed']){ + clues = json['clues']; + clue_rels = {}; + drawClues(); + updateClueStats(); + } + } + 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 = {}; + + 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}', { + minZoom: 0, + maxZoom: 20, + attribution: '© <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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}', { + 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); + 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(); +}
\ No newline at end of file diff --git a/dashboard_website/static/js/map.js b/dashboard_website/static/js/map.js deleted file mode 100644 index 35eb70a..0000000 --- a/dashboard_website/static/js/map.js +++ /dev/null @@ -1,6 +0,0 @@ - -var all_clues = L.layerGroup([]); // all clues -var unvisited_clues = L.layerGroup([]); // subset of all clues - unvisited clues -var destination_clues = L.layerGroup([]); // clues bikers are currently destined for - -document.onload()
\ No newline at end of file diff --git a/dashboard_website/static/js/utils.js b/dashboard_website/static/js/utils.js new file mode 100644 index 0000000..83e6397 --- /dev/null +++ b/dashboard_website/static/js/utils.js @@ -0,0 +1,18 @@ +function deg2rad(deg) { + return deg * (Math.PI/180) +} +function getDistanceFromLatLon(item1, item2) { + lat1 = item1['latitude']; lon1 = item1['longitude']; + lat2 = item2['latitude']; lon2 = item2['longitude']; + var R = 3958.8; // Radius of the earth + var dLat = deg2rad(lat2-lat1); + var dLon = deg2rad(lon2-lon1); + var a = + Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * + Math.sin(dLon/2) * Math.sin(dLon/2) + ; + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + var d = R * c; // Distance in mi + return d; +}
\ No newline at end of file diff --git a/dashboard_website/templates/index.html b/dashboard_website/templates/index.html index ab90904..1317145 100644 --- a/dashboard_website/templates/index.html +++ b/dashboard_website/templates/index.html @@ -1,19 +1,178 @@ <!DOCTYPE html> -<html> +<html style="height:100%"> <head> + <title>MMHC HQ</title> + <link rel="icon" type="image/x-icon" href="/static/img/favicon.ico"> + <link rel="stylesheet" href="{{ url_for('static', filename='css/leaflet.css') }}" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> - <link rel="stylesheet" href="{{ url_for('static', filename='css/leaflet_mods.css') }}"/> + <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"/> <script src="{{ url_for('static', filename='js/leaflet.js') }}" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> - <script src="{{ url_for('static', filename='js/map.js') }}"></script> + <script src="https://unpkg.com/leaflet.vectorgrid@latest/dist/Leaflet.VectorGrid.js"></script> + <script src="{{ url_for('static', filename='js/utils.js') }}"></script> + <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script> </head> -<body> - <div id="map"></div> +<body style="height:100%"> + <style> + #map { height: 100%; } + * { + box-sizing: border-box; + } + html{ + padding:0; + margin: 0; + height:100%; + } + + body{ + padding:0; + margin: 0; + height:100%; + } + .page-container { + + display: flex; + + + background-color: black; + height: 100%; + gap: 5px; + height:100%; + padding:10px; + + } + .page-container > div{ + padding: 8px; + } + + + .left-column { + /* flex:0.75 1 auto; */ + flex-grow:0; + flex-basis:75em; + display: flex; + flex-direction: column; + gap: 5px; + } + .left-column > div{ + background: black; + border: 3px double lightgray; + padding: 0px; + } + .page-title { + /* flex:0.2 1 auto; */ + flex-grow:0.03; + display: flex; + align-items: center; + } + .map-frame { + /* flex:0.2 1 auto; */ + flex-grow:1.0; + } + .route-controls { + /* flex:0.6 1 auto; */ + flex-grow:0.15; + display: flex; + flex-direction: column; + } + + .right-column { + /* flex:0.25 1 auto; */ + flex-grow:0; + flex-basis:25em; + display: flex; + flex-direction: column; + gap: 5px; + } + .right-column > div{ + background: black; + border: 3px double lightgray; + padding: 0px; + } + .clue-stats { + /* flex:0.2 1 auto; */ + flex-grow:0.05; + display: flex; + flex-direction: column; + } + .bike-teams { + /* flex:0.2 1 auto; */ + flex-grow:0.35; + display: flex; + flex-direction: column; + align-content:stretch; + } + .add-clue { + /* flex:0.6 1 auto; */ + flex-grow:0.6; + display: flex; + flex-direction: column; + } + + .leaflet-popup-content { + color: black; + } + .leaflet-control-layers-overlays > label > span > span { + color: black; + } + .leaflet-bike-marker { + transition: all 5s; + } + input { + color: black; + } + button { + color: black; + } + + </style> + + <div class="page-container"> + + <div class="left-column"> + <div class="page-title"> + <h1 style="color:white;margin: 0px;margin-left:15px">CLUBHOUSE HQ | <span id="titletime" style="color:lightgray"></span></h1> + </div> + <div class="map-frame"> + <div id="map"></div> + </div> + <div class="route-controls"> + <span style="margin:-1px;margin-left:5px;margin-top:5px;">ROUTE PARAMETERS</span> + <hr style="width:100%;padding:0px;"/> + </div> + </div> + <div class="right-column"> + <div class="clue-stats"> + <span style="margin:-1px;margin-left:5px;margin-top:5px;">CLUE STATS</span> + <hr style="width:100%;padding:0px;"/> + <div style="margin:0px;margin-left:5px;display:flex;flex-direction:row"><span id="total_count" style="margin-right:5px;">XX</span><span>total clues (</span><span id="unvisited_count" style="margin-right:5px;">XX</span><span>unvisited)</span></div> + <div style="margin:0px;margin-left:5px;display:flex;flex-direction:row;"><span id="visited_count" style="margin-right:5px;">XX</span><span>visited clues (</span><span id="percent_visited">XX</span><span>%)</span></div> + <div style="margin:0px;margin-left:5px;display:flex;flex-direction:row"><span>Average visited distance: </span><span id="avg_visited_distance" style="margin-left:5px">XX</span><span>(mi)</span></div> + </div> + <div class="bike-teams"> + <span style="margin:-1px;margin-left:5px;margin-top:5px;">BIKE TEAMS</span> + <hr style="width:100%;padding:0px;"/> + <table id="bike-teams-table" style="width:100%"> + </table> + </div> + <div class="add-clue"> + <span style="margin:-1px;margin-left:5px;margin-top:5px;">ADD CLUE</span> + <hr style="width:100%;padding:0px;"/> + <div style="margin:5px;display:flex;flex-direction:row"> <span>Clue Name: </span> <input type="input" id="new_clue_name" style="flex:1;" placeholder="e.g., C34"/></div> + <div style="margin:5px;display:flex;flex-direction:row"> <span>Clue Information: </span> <input type="input" id="new_clue_info" style="flex:1;" placeholder="e.g., Savenor's Butcher"/></div> + <div style="margin:5px;display:flex;flex-direction:row"> <span>Longitude: </span> <input autocomplete=off type="input" onchange="previewZoom()" onkeyup="previewZoom()" id="new_clue_longitude" style="flex:1;" placeholder="e.g., -71.0688746"/></div> + <div style="margin:5px;display:flex;flex-direction:row"> <span>Latitude: </span> <input autocomplete=off type="input" onchange="previewZoom()" onkeyup="previewZoom()" id="new_clue_latitude" style="flex:1;" placeholder="e.g., 40.3587273"/></div> + <div id="previewmap" style="height:70%; margin:5px;"></div> + <div style="margin:5px;display:flex;flex-direction:row; justify-content: center;"> <button onclick="addClue()">Submit</button> <button>Clear</button></div> + </div> + </div> + + </div> </body> </html>
\ No newline at end of file |
