summaryrefslogtreecommitdiff
path: root/dashboard_website
diff options
context:
space:
mode:
authoritsGarrin <garrin.shieh@gmail.com>2023-11-07 10:40:14 -0500
committeritsGarrin <garrin.shieh@gmail.com>2023-11-07 10:40:14 -0500
commite2e38322580304b8c0168f3e3a8a3986d229b0fd (patch)
treeb4d6f99882ed5628b9447893e42e8421b4517d91 /dashboard_website
parent8dad8eaf2d1a9992e2779053f306f6d0736886bf (diff)
parent20f22f05a8a7b049c6946ac056773bef954642e9 (diff)
Merge remote-tracking branch 'origin/master'
Diffstat (limited to 'dashboard_website')
-rw-r--r--dashboard_website/__pycache__/db.cpython-311.pycbin0 -> 9976 bytes
-rw-r--r--dashboard_website/all_clues.csv169
-rw-r--r--dashboard_website/dashboard.py39
-rw-r--r--dashboard_website/db.py168
-rw-r--r--dashboard_website/static/css/dashboard.css13
-rw-r--r--dashboard_website/static/css/leaflet_mods.css4
-rw-r--r--dashboard_website/static/fonts/HWYGOTH.ttfbin0 -> 53012 bytes
-rw-r--r--dashboard_website/static/img/favicon.icobin0 -> 4286 bytes
-rw-r--r--dashboard_website/static/img/marker-bike-active.pngbin0 -> 8063 bytes
-rw-r--r--dashboard_website/static/img/marker-bike-inactive.png (renamed from dashboard_website/static/img/marker-bike.png)bin7511 -> 7511 bytes
-rw-r--r--dashboard_website/static/img/marker-home.pngbin0 -> 7622 bytes
-rw-r--r--dashboard_website/static/js/dashboard.js210
-rw-r--r--dashboard_website/static/js/map.js6
-rw-r--r--dashboard_website/static/js/utils.js18
-rw-r--r--dashboard_website/templates/index.html169
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
new file mode 100644
index 0000000..09dc8b3
--- /dev/null
+++ b/dashboard_website/__pycache__/db.cpython-311.pyc
Binary files differ
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
new file mode 100644
index 0000000..20ac3e2
--- /dev/null
+++ b/dashboard_website/static/fonts/HWYGOTH.ttf
Binary files differ
diff --git a/dashboard_website/static/img/favicon.ico b/dashboard_website/static/img/favicon.ico
new file mode 100644
index 0000000..7f391c2
--- /dev/null
+++ b/dashboard_website/static/img/favicon.ico
Binary files differ
diff --git a/dashboard_website/static/img/marker-bike-active.png b/dashboard_website/static/img/marker-bike-active.png
new file mode 100644
index 0000000..f4f0ae6
--- /dev/null
+++ b/dashboard_website/static/img/marker-bike-active.png
Binary files differ
diff --git a/dashboard_website/static/img/marker-bike.png b/dashboard_website/static/img/marker-bike-inactive.png
index 48dbf96..48dbf96 100644
--- a/dashboard_website/static/img/marker-bike.png
+++ b/dashboard_website/static/img/marker-bike-inactive.png
Binary files differ
diff --git a/dashboard_website/static/img/marker-home.png b/dashboard_website/static/img/marker-home.png
new file mode 100644
index 0000000..1c63656
--- /dev/null
+++ b/dashboard_website/static/img/marker-home.png
Binary files differ
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: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <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