summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dashboard_website/__pycache__/datastructs.cpython-312.pycbin7899 -> 9061 bytes
-rw-r--r--dashboard_website/__pycache__/db.cpython-312.pycbin11486 -> 12755 bytes
-rw-r--r--dashboard_website/__pycache__/router.cpython-312.pycbin15791 -> 18499 bytes
-rw-r--r--dashboard_website/dashboard.py89
-rw-r--r--dashboard_website/datastructs.py52
-rw-r--r--dashboard_website/db.py174
-rw-r--r--dashboard_website/router.py161
-rw-r--r--dashboard_website/savefile.csv55
-rw-r--r--dashboard_website/static/css/dashboard.css7
-rw-r--r--dashboard_website/static/js/controls.js7
-rw-r--r--dashboard_website/static/js/dashboard.js94
-rw-r--r--dashboard_website/templates/controls.html14
-rw-r--r--dashboard_website/templates/index.html9
13 files changed, 470 insertions, 192 deletions
diff --git a/dashboard_website/__pycache__/datastructs.cpython-312.pyc b/dashboard_website/__pycache__/datastructs.cpython-312.pyc
index ff4cf5d..4d2a713 100644
--- a/dashboard_website/__pycache__/datastructs.cpython-312.pyc
+++ b/dashboard_website/__pycache__/datastructs.cpython-312.pyc
Binary files differ
diff --git a/dashboard_website/__pycache__/db.cpython-312.pyc b/dashboard_website/__pycache__/db.cpython-312.pyc
index 2172d4d..1d7188c 100644
--- a/dashboard_website/__pycache__/db.cpython-312.pyc
+++ b/dashboard_website/__pycache__/db.cpython-312.pyc
Binary files differ
diff --git a/dashboard_website/__pycache__/router.cpython-312.pyc b/dashboard_website/__pycache__/router.cpython-312.pyc
index 2611148..c55cd00 100644
--- a/dashboard_website/__pycache__/router.cpython-312.pyc
+++ b/dashboard_website/__pycache__/router.cpython-312.pyc
Binary files differ
diff --git a/dashboard_website/dashboard.py b/dashboard_website/dashboard.py
index 8ec53e0..77832aa 100644
--- a/dashboard_website/dashboard.py
+++ b/dashboard_website/dashboard.py
@@ -19,25 +19,17 @@ app = Flask(__name__)
# responds with your next route
@app.route("/enableTeam", methods=['POST'])
# Expected JSON
-# {"team_name" : "XXXX", str
-# "longitude" : xx.xxxxxx, float //current team location
-# "latitude" : xx.xxxxxx, float }
+# {"team_index" : 1,2,3,4; int }
# Returns JSON
# {"status" : "OK"/"ERROR XX", str } expect something like: http://acetyl.net:5000/route/v1/bike/-71.0553792,42.3688272;-71.0688746,42.3576234 without the waypoints section
# ERROR CODES: 1 = missing fields, 2 = invalid coordinates, 4 = team ALREADY exists,
def enableTeam():
content = request.get_json()
- if not ('team_name' in content and 'longitude' in content and 'latitude' in content):
+ if not ('team_index' in content):
status = "ERROR 1"
return jsonify({'status' : status})
-
- if not ( (type(content['longitude']) is float ) and (type(content['latitude']) is float )):
- status = "ERROR 2"
- return jsonify({'status' : status})
- if db.addBike(content['team_name'], content['latitude'], content['longitude']) == 4:
- status = "ERROR 4"
- return jsonify({'status' : status})
+ db.setBikeEnabled(content['team_index'], True)
return jsonify({'status' : "OK"})
@@ -45,26 +37,24 @@ def enableTeam():
# responds with your next route
@app.route("/disableTeam", methods=['POST'])
# Expected JSON
-# {"team_name" : "XXXX", str }
+# {"team_index" : 1,2,3,4; int }
# Returns JSON
# {"status" : "OK"/"ERROR XX", str }
# ERROR CODES: 1 = missing fields, 4 = team does not exist,
def disableTeam():
content = request.get_json()
- if not ('team_name' in content):
+ if not ('team_index' in content):
status = "ERROR 1"
return jsonify({'status' : status})
- if db.deleteBike(content['team_name']) == 4:
- status = "ERROR 4"
- return jsonify({'status' : status})
+ db.setBikeEnabled(content['team_index'], False)
return jsonify({'status' : "OK"})
# requests a route to the best clue given the team's current coordinates
@app.route("/requestRoute", methods=['POST'])
# Expected JSON
-# {"team_name" : "XXXX", str
+# {"team_index" : 1,2,3,4; int
# "longitude" : xx.xxxxxx, float //current team location
# "latitude" : xx.xxxxxx, float }
# Returns JSON
@@ -78,19 +68,19 @@ def disableTeam():
def requestRoute():
content = request.get_json()
# verify request
- if not ('team_name' in content and 'longitude' in content and 'latitude' in content):
+ if not ('team_index' in content and 'longitude' in content and 'latitude' in content):
return jsonify({'status' : "ERROR 1"})
if not ( (type(content['longitude']) is float ) and (type(content['latitude']) is float)):
return jsonify({'status' : "ERROR 2"})
- if db.pingBike(content['team_name'], content['latitude'], content['longitude']) == 4:
+ if db.pingBike(content['team_index'], content['latitude'], content['longitude']) == 4:
return jsonify({'status' : "ERROR 4"})
if db.currently_updating:
return jsonify({'status' : "ERROR 3"})
- bike, clue = db.getBikeCluePair(content['team_name'])
+ bike, clue = db.getBikeCluePair(content['team_index'])
if clue == None:
return jsonify({'status' : "ERROR 5"})
@@ -111,7 +101,7 @@ def requestRoute():
# periodically called to update team location in the management dashboard
@app.route("/updateTeamLocation", methods=['POST'])
# Expected JSON
-# {"team_name" : "XXXX", str
+# {"team_index" : 1,2,3,4; int
# "longitude" : xx.xxxxxx, float
# "latitude" : xx.xxxxxx, float }
# Returns JSON
@@ -120,7 +110,7 @@ def requestRoute():
def updateTeamLocation():
status = "OK"
content = request.get_json()
- if not ('team_name' in content and 'longitude' in content and 'latitude' in content):
+ if not ('team_index' in content and 'longitude' in content and 'latitude' in content):
status = "ERROR 1"
return jsonify({'status' : status})
@@ -128,16 +118,30 @@ def updateTeamLocation():
status = "ERROR 2"
return jsonify({'status' : status})
- if db.pingBike(content['team_name'], content['latitude'], content['longitude']) == 4:
+ if db.pingBike(content['team_index'], content['latitude'], content['longitude']) == 4:
status = "ERROR 4"
return jsonify({'status' : status})
return jsonify({'status' : "OK"})
+# from website
+# {"team_name"}
+@app.route("/updateTeamDeadline", methods=['POST'])
+def updateTeamDeadline():
+ content = request.get_json()
+ if not ('team_index' in content and 'new_deadline' in content):
+ status = "ERROR 1"
+ return jsonify({'status' : status})
+ db.setBikeDeadline(content['team_index'], content['new_deadline'])
+ return jsonify({'status' : "OK"})
+
+
+
+
# mark clue as visited from app
@app.route("/visitClueTeam", methods=['POST'])
# Expected JSON
-# {"team_name" : xxxx, str
+# {"team_index" : 1,2,3,4; int
# "clue_name" : xxxx, str
# "longitude" : xx.xxxxxx, float
# "latitude" : xx.xxxxxx, float }
@@ -146,17 +150,17 @@ def updateTeamLocation():
# ERROR CODES: 1 = missing fields, 2 = invalid coordinates, 3 = too far from clue location, 4 = no such team, 5 = no such clue, 6 = already visited
def visitTeam():
content = request.get_json()
- if not ('team_name' in content and 'longitude' in content and 'latitude' in content and 'clue_name' in content):
+ if not ('team_index' in content and 'longitude' in content and 'latitude' in content and 'clue_name' in content):
return jsonify({'status' : "ERROR 1"})
if not ( (type(content['longitude']) is float ) and (type(content['latitude']) is float)):
status = "ERROR 2"
return jsonify({'status' : status})
- if db.pingBike(content['team_name'], content['latitude'], content['longitude']) == 4:
+ if db.pingBike(content['team_index'], content['latitude'], content['longitude']) == 4:
return jsonify({'status' : "ERROR 4"})
- result = db.visitClueTeam(content['team_name'], content['clue_name'])
+ result = db.visitClueTeam(content['team_index'], content['clue_name'])
if result != 0:
return jsonify({'status' : f"ERROR {result}"})
return jsonify({'status' : "OK"})
@@ -212,13 +216,29 @@ def requireClue():
def enableClueToggle():
content = request.get_json()
if not ('clue_name' in content):
- return jsonify({'status' : "ERROR 1"})
+ return jsonify({'status' : "ERROR: NO CLUE PROVIDED"})
result = db.toggleEnableClue(content['clue_name'])
if result != 0:
return jsonify({'status' : f"ERROR {result}"})
return jsonify({'status' : "OK"})
+@app.route("/setClueTeam", methods=['POST'])
+# Expected JSON
+# {"clue_name" : xxxx, str,
+# "bike_team" : 0-4, int}
+# Returns JSON
+# {"status" : "OK"/"ERROR XX" }
+def setClueTeam():
+ content = request.get_json()
+ print("setting clue team", content)
+ if not ('clue_name' in content and 'bike_team' in content):
+ return jsonify({'status' : "ERROR: NO CLUE AND/OR TEAM PROVIDED"})
+ result = db.assignClueToTeam(content['clue_name'], content['bike_team'])
+ if result != 0:
+ return jsonify({'status' : "ERROR: CLUE NOT FOUND"})
+ return jsonify({'status' : "OK"})
+
#
# WEB / DASHBOARD API
#
@@ -236,7 +256,8 @@ def getLatestInfo():
data = {'timestamp' : db.getTime(),
'clues_changed' : False,
'home_changed' : False,
- 'routes_changed' : False}
+ 'routes_changed' : False,
+ 'route_update_staged' : db.is_route_update_required() }
cl = db.getCluesJSON(last_timestamp)
if cl != False:
data['clues_changed'] = True
@@ -262,7 +283,7 @@ def addClueWeb():
print("adding clue:", content)
if not ('clue_name' in content and 'longitude' in content and 'latitude' in content and 'clue_info' in content):
return jsonify({'status' : "ERROR: INVALID CLUE JSON FORMAT"})
- res = db.addClue(content['clue_name'], content['clue_info'], content['latitude'], content['longitude'])
+ res = db.addClue(content['clue_name'], content['clue_info'], content['longitude'], content['latitude'])
if res == 0:
return jsonify({'status' : "OK",})
elif res == -1:
@@ -300,6 +321,14 @@ def siteControls():
if db.load("clean.csv") != 0:
return jsonify({"status" : "ERROR"})
return jsonify({"status" : "OK"})
+ elif cmd == "setHome":
+ if db.setHomeBase(request.form.get('latitude'), request.form.get('longitude')) != 0:
+ return jsonify({"status" : "ERROR"})
+ return jsonify({"status" : "OK"})
+ elif cmd == "generateRoutes":
+ if db.updateRoutes() != 0:
+ return jsonify({"status" : "ERROR"})
+ return jsonify({"status" : "OK"})
return render_template("controls.html")
diff --git a/dashboard_website/datastructs.py b/dashboard_website/datastructs.py
index 1b4b92b..4fe5038 100644
--- a/dashboard_website/datastructs.py
+++ b/dashboard_website/datastructs.py
@@ -1,10 +1,11 @@
import math
import time
+import datetime
# time since last ping before deactivating/deleting
BIKE_TIMEOUT = 60000 # 3 minutes
-BIKE_DELETE = 360000 # time before bike deletes itself
+endtime = datetime.datetime(2024, 11, 16, hour=18, minute=45)
# data structures
class Point:
@@ -71,6 +72,9 @@ class Clue(Point):
def toggle_required(self):
self.required = False if self.required else True
+
+ def set_team(self, team):
+ self.assigned_team = team
def toJSON(self):
json_dict = {'longitude' : self.longitude,
@@ -85,14 +89,17 @@ class Clue(Point):
class Bike(Point):
- def __init__(self, lat, long, name, status):
+ def __init__(self, lat, long, name):
self.longitude = long
self.latitude = lat
self.name = name
+ self.deadline = endtime
self.last_contact = time.time()
- self.target_name = "N/A"
+ self.target_clue = None
+ self.time_modifier = 1 # factor by which to modulate expected arrival time
self.cluster = [] # list of clues this bike team is responsible for
- self.status = status # ACTIVE | INACTIVE
+ self.status = "DISABLED" # DISABLED | ACTIVE | INACTIVE
+ self.clue_assignment_time = 0 # when clue was assigned, so that upon clue visitation we can determine speed
def setTarget(self, clue_name):
self.target_name = clue_name
@@ -101,14 +108,24 @@ class Bike(Point):
self.cluster = clue_cluster
self.updateTarget()
+ def setDeadline(self, new_deadline):
+ self.deadline = new_deadline
+
+ def timeTilDeadline(self):
+ return (self.deadline - datetime.datetime.now()).total_seconds()
+
def updateTarget(self):
+ self.clue_assignment_time = time.time()
if len(self.cluster) <= 0:
- self.target_name = "N/A"
+ self.target_clue = None
+ self.status = "INACTIVE"
else:
- self.target_name = self.cluster[0].name
+ self.status = "ACTIVE"
+ self.target_clue = self.cluster[0]
def visitTarget(self):
- self.cluster[0].visit()
+ print("clue visit time: ", time.time() - self.clue_assignment_time)
+ self.target_clue.visit()
self.cluster.pop(0)
self.updateTarget()
while len(self.cluster) > 0 and self.cluster[0].status == "VISITED":
@@ -120,25 +137,24 @@ class Bike(Point):
self.last_contact = time.time()
def disable(self):
+ self.status = "DISABLED"
+ self.target = None
+
+ def enable(self):
self.status = "INACTIVE"
- self.target = "N/A"
- self.last_contact = 0
-
- def checkStatus(self):
- if time.time() - self.last_contact > BIKE_TIMEOUT:
- self.status = "INACTIVE"
- self.target = "N/A"
- if time.time() - self.last_contact > BIKE_DELETE:
- return -1
- return 0
+ self.target = None
+
+ def tripTime(self):
+ return datetime.datetime.now() - self.deadline
def toJSON(self):
json_dict = {
"longitude": self.longitude,
"latitude": self.latitude,
"time_since_last_contact": time.time() - self.last_contact,
+ "team_deadline" : str(self.deadline),
"team_name": self.name,
"team_status": self.status,
- "target_clue": self.target_name,
+ "target_clue": self.target_clue.name if self.target_clue else "N/A",
}
return json_dict
diff --git a/dashboard_website/db.py b/dashboard_website/db.py
index 7bb5a18..eec587d 100644
--- a/dashboard_website/db.py
+++ b/dashboard_website/db.py
@@ -2,10 +2,12 @@
# also manages currently available bike teams
import csv
from threading import Thread
+import datetime
import router
from datastructs import *
+
# constants
CLUE_MIN_DISTANCE = 0.1 # minimum distance between clue and bike to be considered valid visitation
@@ -16,33 +18,63 @@ bikes = []
clusters = []
routes = {"clusters": [], "cluster_times": {},
"individual_routes": []} # geojson polylines, both between all the clusters
+preview_routes = {"clusters": [], "cluster_times": {}, # geojson polylines, both between all the clusters
+ "individual_routes": [], "clue_clusters" : []} # except clue clusters which is a list of a list of clues
assigned_clues = []
clues_last_changed = time.time()
home_last_changed = time.time()
routes_last_changed = time.time()
+route_update_required = False
currently_updating = False
+minimalRouting = False # if false, currently assigned clues
+
+_bikes_changed = False # In case bikes or clues are enabled/disabled
+_clues_changed = False # during route calculation
startup = False #
+def should_cancel_routing():
+ global _bikes_changed, _clues_changed
+ if _bikes_changed or _clues_changed:
+ _bikes_changed = True
+ _clues_changed = True
+ return True
+ return False
+
+def is_route_update_required():
+ return route_update_required
# called every time a node is added
# a bike is added/removed
# determines/assigns clusters, and assigns routes to bikes
def updateRoutes_background(): # run in thread due to long runtime
- global currently_updating, routes_last_changed, routes, clusters
+ global currently_updating, routes_last_changed, route_update_required, preview_routes, clusters, routes
print("Calculating clusters...")
- routes = {"clusters": [], "cluster_times": {}, "individual_routes": []} # reset
- clusters, paths, times = router.getClusters(bikes, clues, homeBase)
+ preview_routes = {"clusters": [], "cluster_times": {}, "individual_routes": [], "clue_clusters" : []} # reset
+ status, clusters, paths, times = router.getClusters(bikes, clues, homeBase, minimalRouting)
+ if status != 0:
+ print("Aborted routing.")
+
+ preview_routes['individual_routes'] = paths.copy()
+ preview_routes['cluster_times'] = times
+ preview_routes['clusters'] = paths
+ preview_routes['clue_clusters'] = clusters
+ for i in range(len(bikes)):
+ if bikes[i].status != "DISABLED":
+ preview_routes['individual_routes'][i] = router.getRouteHDPolyline(bikes[i], clusters[i][0])
+
+ # set routes - TESTING
routes['individual_routes'] = paths.copy()
routes['cluster_times'] = times
routes['clusters'] = paths
- for i in range(len(bikes)):
- if bikes[i].status == "ACTIVE":
- routes['individual_routes'][i] = router.getRouteHDPolyline(bikes[i], clusters[i][0])
+ for i, bike in enumerate(bikes):
+ if bike.status == "DISABLED": continue
+ bike.setCluster(clusters[i])
routes_last_changed = time.time()
print("Finished calculating clusters/routes.")
currently_updating = False
+ route_update_required = False
def updateRoutes(): # if necessary
@@ -52,6 +84,7 @@ def updateRoutes(): # if necessary
currently_updating = True
t = Thread(target=updateRoutes_background)
t.start()
+ return 0
# interface functions
@@ -73,21 +106,20 @@ def getHomeBaseJSON(timestamp):
def setHomeBase(latitude, longitude):
- homeBase.setCoords(latitude, longitude)
- home_last_changed = time.time()
+ global home_last_changed
+ try:
+ latitude = float(latitude)
+ longitude = float(longitude)
+ homeBase.setCoords(latitude, longitude)
+ home_last_changed = time.time()
+ return 0
+ except:
+ return -1
-def getBikeCluePair(team_name):
- b = None;
- c = None
- for bike in bikes:
- if bike.name == team_name:
- b = bike
- for clue in clues:
- if clue.name == b.target_name:
- c = clue
- break
- break
+def getBikeCluePair(team_index):
+ b = bikes[team_index-1]
+ c = b.target_clue
return b, c
@@ -97,41 +129,31 @@ def getRoutesJSON(timestamp):
return False
-def deleteBike(team_name):
- for bike in bikes:
- if bike.name == team_name: # already exists
- bike.disable()
- updateRoutes()
- return 0 # OK
- return 4 # bike does not exist
-
-
-def addBike(team_name, latitude, longitude):
- for bike in bikes:
- if bike.name == team_name: # already exists
- return 4 # already exists
- newBike = Bike(latitude, longitude, team_name, "ACTIVE")
- bikes.append(newBike)
- updateRoutes()
+def pingBike(team_index, latitude, longitude):
+ bike = bikes[team_index-1]
+ bike.ping()
+ bike.setCoords(latitude, longitude)
return 0
+def setBikeEnabled(team_index, enabled):
+ global route_update_required
+ bike = bikes[team_index-1]
+ route_update_required = True
+ if enabled:
+ bike.enable()
+ else:
+ bike.disable()
+ return 0
-def pingBike(team_name, latitude, longitude):
- for bike in bikes:
- if bike.name == team_name:
- bike.ping()
- bike.setCoords(latitude, longitude)
- return 0
- return 4 # team not found
-
+def setBikeDeadline(team_index, datetime_string):
+ global route_update_required
+ new_deadline = datetime.datetime.strptime(datetime_string, "%Y-%m-%dT%H:%M")
+ bikes[team_index-1].setDeadline(new_deadline)
+ route_update_required = True
+ return 0
def getBikesJSON():
- global bikes
- old_length = len(bikes)
- bikes = [x for x in bikes if x.checkStatus() >= 0]
- if old_length != len(bikes):
- updateRoutes()
- return [x.toJSON() for x in bikes]
+ return [ x.toJSON() for x in bikes ]
def getCluesJSON(timestamp):
@@ -141,27 +163,29 @@ def getCluesJSON(timestamp):
def addClue(clue_name, clue_info, longitude, latitude, status="UNVISITED"):
- global clues_last_changed
+ global clues_last_changed, route_update_required
for clue in clues:
if clue.name == clue_name:
return -1 # clue already exists
newClue = Clue(latitude, longitude, clue_name, clue_info, status)
clues.append(newClue)
clues_last_changed = time.time()
+ route_update_required = True
return 0
def deleteClue(clue_name):
+ global route_update_required
for i, clue in enumereate(clues):
if clue.name == clue_name:
clues.pop(i)
clues_last_changed = time.time()
- updateRoutes()
+ route_update_required = True
break
def visitClue(clue_name, unvisit=False):
- global clues_last_changed
+ global clues_last_changed, route_update_required
for clue in clues:
if clue.name == clue_name:
if clue.status == "VISITED":
@@ -172,41 +196,45 @@ def visitClue(clue_name, unvisit=False):
return 3 # already visited
clue.visit()
clues_last_changed = time.time()
- updateRoutes()
+ route_update_required
return 0 # OK
return 2 # no clue
def toggleEnableClue(clue_name):
- global clues_last_changed
+ global clues_last_changed, route_update_required
for clue in clues:
if clue.name == clue_name:
clue.toggle_enable()
clues_last_changed = time.time()
- updateRoutes()
+ route_update_required = True
return 0 # OK
return 2 # no clue
def toggleClueRequired(clue_name):
- global clues_last_changed
+ global clues_last_changed, route_update_required
for clue in clues:
if clue.name == clue_name:
clue.toggle_required()
clues_last_changed = time.time()
- updateRoutes()
+ route_update_required
+ return 0 # OK
+ return 2 # no clue
+
+def assignClueToTeam(clue_name, team_index):
+ global clues_last_changed, route_update_required
+ for clue in clues:
+ if clue.name == clue_name and clue.status != "ASSIGNED":
+ clue.set_team(team_index)
+ clues_last_changed = time.time()
+ route_update_required = True
return 0 # OK
return 2 # no clue
+def visitClueTeam(team_index, clue_name):
+ global clues_last_changed, route_update_required
+ b = bikes[team_index-1] # Team 1 -> index 0
-def visitClueTeam(team_name, clue_name):
- global clues_last_changed
- b = None
- for bike in bikes:
- if bike.name == team_name:
- b = bike
- break
- else:
- return 4 # team not found
c = None
for clue in clues:
if clue.name == clue_name:
@@ -219,9 +247,9 @@ def visitClueTeam(team_name, clue_name):
# if visited clue is the expected one (current target)
# no need to recalculate
- if clue_name == b.target_name:
- if c.distanceTo(b) < CLUE_MIN_DISTANCE:
- return 3 # too far away
+ if c == b.target_clue:
+ #if c.distanceTo(b) < CLUE_MIN_DISTANCE:
+ # return 3 # too far away
b.visitTarget()
clues_last_changed = time.time()
return 0
@@ -229,7 +257,7 @@ def visitClueTeam(team_name, clue_name):
# otherwise
c.visit()
clues_last_changed = time.time()
- updateRoutes() # must be updated due to unexpected visitation
+ route_update_required
def load(filename=None):
@@ -284,6 +312,7 @@ def loadDirty(filename=None):
if len(latlong) != 2: continue
latitude = float(latlong[0])
longitude = float(latlong[1])
+ print(longitude)
clues.append(Clue(latitude, longitude, name, info, "UNVISITED"))
except:
@@ -305,5 +334,8 @@ def save():
for clue in clues:
csvwriter.writerow([clue.name, clue.latitude, clue.longitude, clue.info, clue.status])
-
+bikes = [ Bike(homeBase.latitude, homeBase.longitude, "Team 1"),
+ Bike(homeBase.latitude, homeBase.longitude, "Team 2"),
+ Bike(homeBase.latitude, homeBase.longitude, "Team 3"),
+ Bike(homeBase.latitude, homeBase.longitude, "Team 4") ]
#load("all_clues.csv") \ No newline at end of file
diff --git a/dashboard_website/router.py b/dashboard_website/router.py
index e0e0720..4840c8d 100644
--- a/dashboard_website/router.py
+++ b/dashboard_website/router.py
@@ -5,10 +5,11 @@ import requests
from sklearn.cluster import KMeans
from datastructs import *
+import db
host = "http://acetyl.net:5000" # queries acetyl.net:5000, the OSRM engine
-endtime = datetime.datetime(2023, 11, 18, hour=18, minute=45) # 11/18/2023 6:35pm
+endtime = datetime.datetime(2024, 11, 16, hour=18, minute=45) # 11/18/2023 6:35pm
# external facing functions
@@ -42,38 +43,36 @@ def getRouteFastPolyline(bike, clue):
return p
-def getZSP(bike, home, clue_cluster):
- pass
-
-
# determines clusters based on current bikes and clues
-def getClusters(bikes, clues, endpoint):
+def getClusters(bikes, clues, endpoint, minimal=True):
+ status = 0
clusters = [[] for bike in bikes]
route_geos = [[] for bike in bikes]
times = {}
- active_indices = [i for i in range(len(bikes)) if bikes[i].status == "ACTIVE"]
- active_bikes = [bike for bike in bikes if bike.status == "ACTIVE"]
+ active_indices = [i for i in range(len(bikes)) if bikes[i].status != "DISABLED"]
+ active_bikes = [bike for bike in bikes if bike.status != "DISABLED"]
if len(active_bikes) == 0:
- return clusters, route_geos, times
- active_clues = [clue for clue in clues if clue.status == "UNVISITED"]
+ return status, clusters, route_geos, times
+ active_clues = [ clue for clue in clues if (clue.status != "VISITED" and clue.status != "DISABLED") ]
# select only active bikes
# select only unvisited clues
- clusters_t, route_geos_t, times_t = cluster_and_optimize(
- active_clues, active_bikes, endpoint
+ status_c, clusters_t, route_geos_t, times_t = cluster_and_optimize(
+ active_clues, bikes, endpoint, minimal
)
- for i in range(len(active_indices)):
- route_geos[active_indices[i]] = route_geos_t[i]
- clusters[active_indices[i]] = clusters_t[i]
- bikes[active_indices[i]].setCluster(clusters_t[i])
- times[bikes[active_indices[i]].name] = times_t[i]
+ if status_c != 0: # routing canceled
+ return -1, [], [], []
+ for i in range(len(bikes)):
+ route_geos[i] = route_geos_t[i]
+ clusters[i] = clusters_t[i]
+ times[i] = times_t[i]
# return list of clue clusters corresponding to bikes
- return clusters, route_geos, times
+ return status, clusters, route_geos, times
# utility functions (internal)
def cluster_and_optimize(
- clues: [Clue], bikes: [Bike], end: Point, time_diff=0.25, max_time=24, n=2
+ clues: [Clue], bikes: [Bike], end: Point, minimal : bool, time_diff=0.25, max_time=24
):
"""
Takes a dataframe of gps coordinates, a list of centroids, and an end point and returns a dataframe with a cluster
@@ -85,25 +84,43 @@ def cluster_and_optimize(
:param n: the number of routes to create
:return: a list of lists of clues (ordered by position on route), and a list of json route geojsons
"""
+ status = 0
# OVERRIDE MAX TIME
max_time = datetime.datetime.now() - endtime
max_time = max_time.seconds / 3600
- routes = [
- clues
- ] # one bike = one set of routes. only need to remove the faraway waypoints
- if len(bikes) > 1:
- # Create a new column with normalized gps coordinates and centroids
- normalized_points, norm_centroids = __normalize_points(clues, bikes)
- # Cluster the coordinates
- kmeans = KMeans(n_clusters=len(norm_centroids), init=norm_centroids)
- kmeans.fit(normalized_points)
-
- # Split the clues into clusters based on the cluster labels
- routes = [[] for i in range(len(norm_centroids))]
- for i, label in enumerate(kmeans.labels_):
- routes[label].append(clues[i])
-
- routes = __minimize_route_time_diff(routes, bikes, end, time_diff, n)
+
+ # Create a new column with normalized gps coordinates and centroids
+ active_indices = [i for i in range(len(bikes)) if bikes[i].status != "DISABLED"]
+ active_bikes = [bike for bike in bikes if bike.status != "DISABLED"]
+
+ normalized_points, norm_centroids = __normalize_points(clues, active_bikes)
+ # Cluster the coordinates
+ kmeans = KMeans(n_clusters=len(norm_centroids), init=norm_centroids)
+ kmeans.fit(normalized_points)
+
+ # assign pre-determined clues to bikes
+ already_assigned_clues = []
+ routes = [[] for i in bikes]
+ for clue in clues:
+ if clue.assigned_team != 0 and ((clue.assigned_team-1) in active_indices):
+ routes[clue.assigned_team - 1].append(clue)
+ already_assigned_clues.append(clue)
+
+ # if we are not doing a hard reset, keep the current target clues
+ if minimal:
+ for i, bike in enumerate(bikes):
+ if (bike.status != "DISABLED") and (bike.target_clue != None) and (bike.target_clue not in routes[i]):
+ routes[i].append(bike.target_clue)
+ already_assigned_clues.append(clue)
+
+ # Split the remaining clues into clusters based on the cluster labels
+ for i, label in enumerate(kmeans.labels_):
+ if clues[i].assigned_team == 0 and (clues[i] not in already_assigned_clues):
+ routes[active_indices[label]].append(clues[i])
+
+ routes = __minimize_route_time_diff(routes, bikes, end, minimal, time_diff)
+ if routes == -1:
+ return -1, [], [], []
# Remove waypoints from the longest route until the trip time is less than the max time
for i in range(len(routes)):
@@ -116,7 +133,7 @@ def cluster_and_optimize(
for i, route in enumerate(routes):
route_json = __get_json(
__clues_to_string(route),
- __clues_to_string([bikes[i]]),
+ __clues_to_string([bikes[i].target_clue if (minimal and (bikes[i].target_clue != None)) else bikes[i]]),
__clues_to_string([end])[:-1],
)
geometries.append(route_json["trips"][0]["geometry"]["coordinates"])
@@ -126,13 +143,15 @@ def cluster_and_optimize(
times.append(eta_str)
# Use the waypoint_index to reorder each route
+
for i, route in enumerate(routes):
route2 = ["" for x in route]
- for j, k in enumerate(route_waypoints[i][1:-1]):
+ start_index = 0 if (minimal and (bikes[i].target_clue != None)) else 1 # if starting route with first clue, first index of trip must be included
+ for j, k in enumerate(route_waypoints[i][start_index:-1]):
route2[k["waypoint_index"] - 1] = route[j]
routes[i] = route2
- return routes, geometries, times
+ return status, routes, geometries, times
def __clues_to_string(points: [Clue]):
@@ -199,7 +218,7 @@ def __get_trip_time(
def __minimize_route_time_diff(
- routes: [Clue], starts: [Point], end: Point, time_diff, n
+ routes: [Clue], bikes: [Bike], end: Point, minimal: bool, time_diff
):
"""
Takes a list of lists of coordinates, a list of start points, an end point, a time difference, and a number of routes
@@ -210,17 +229,25 @@ def __minimize_route_time_diff(
:param n: the number of routes
:return: a list of lists of coordinates
"""
- times = []
+
+ if db.should_cancel_routing(): return -1
+
+ active_indices = [i for i in range(len(bikes)) if bikes[i].status != "DISABLED"]
+ starts = [ bike.target_clue if (minimal and (bike.target_clue != None)) else bike for bike in bikes ] # all bikes regardless of enabled
+ max_times = [ bike.timeTilDeadline()/3600 for bike in bikes if bike.status != "DISABLED"]
+ times = [ ]
for i, route in enumerate(routes):
+ if bikes[i].status == "DISABLED": continue
times.append(
__get_trip_time(
__clues_to_string(route),
len(route),
__clues_to_string([starts[i]]),
__clues_to_string([end])[:-1],
- )
+ ) * bikes[i].time_modifier
)
+ if db.should_cancel_routing(): return -1
# Find the average trip time
average_time = np.mean(times)
@@ -237,18 +264,18 @@ def __minimize_route_time_diff(
closest_coordinate = __find_closest_coordinate(
routes[sorted_indices[-1]], __mean_center(routes[sorted_indices[0]])
)
- routes[sorted_indices[0]].append(closest_coordinate)
- routes[sorted_indices[-1]].remove(closest_coordinate)
+ routes[active_indices[sorted_indices[0]]].append(closest_coordinate)
+ routes[active_indices[sorted_indices[-1]]].remove(closest_coordinate)
# Recursively minimize the time difference between the routes
- return __minimize_route_time_diff(routes, starts, end, time_diff, n)
+ return __minimize_route_time_diff(routes, bikes, end, minimal, time_diff)
# If the difference of the longest trip time from average is less than the time difference, return the routes
return routes
def __remove_longest_waypoints(
- route_coordinates: [Clue], start: Bike, end: Point, max_time
+ route_coordinates: [Clue], start: Bike, end: Point, max_time, start_already_set=False
):
"""
Takes a list of coordinates, a start point, an end point, and a maximum time and returns a list of coordinates
@@ -258,6 +285,11 @@ def __remove_longest_waypoints(
:param max_time: the maximum time
:return: a list of coordinates
"""
+ if len(route_coordinates) < 1:
+ return []
+
+ if start.status != "DISABLED" and start.target_clue != None: # already assigned a clue
+ start = start.target_clue
# Find the trip time for the route
route_time = __get_trip_time(
__clues_to_string(route_coordinates),
@@ -269,10 +301,11 @@ def __remove_longest_waypoints(
# If the trip time is greater than the max time, remove the waypoint with the longest distance from the mean
if route_time > max_time:
route_mean = __mean_center(route_coordinates)
- furthest_coordinate = __find_farthest_coordinate(route_coordinates, route_mean)
- route_coordinates.remove(furthest_coordinate)
+ farthest_coordinates = __find_farthest_coordinates(route_coordinates, route_mean)
+ for farthest_coordinate in farthest_coordinates:
+ route_coordinates.remove(farthest_coordinate)
- return __remove_longest_waypoints(route_coordinates, start, end, max_time)
+ return __remove_longest_waypoints(route_coordinates, start, end, max_time, start_already_set=True)
return route_coordinates
@@ -348,22 +381,34 @@ def __find_closest_coordinate(clues: [Clue], centroid: Point):
return closest_coordinate
-def __find_farthest_coordinate(clues: [Clue], centroid: Point):
+def __find_farthest_coordinates(clues: [Clue], centroid: Point, n:int=1):
"""
Takes a list of coordinates and a centroid and returns the clue in the list that is farthest from the centroid
:param clues: the list of coordinates
:param centroid: the centroid
:return: the clue in the list that is farthest from the centroid
"""
- farthest_coordinate = clues[0]
- farthest_coordinate_distance = __distance(farthest_coordinate, centroid)
-
- for clue in clues:
- if __distance(clue, centroid) > farthest_coordinate_distance:
- farthest_coordinate = clue
- farthest_coordinate_distance = __distance(clue, centroid)
-
- return farthest_coordinate
+
+ farthest_coordinates = []
+
+ for _ in range(n):
+ farthest_coordinate = clues[0]
+ # remove only unrequired clues
+ i = 1
+ while farthest_coordinate.required or (farthest_coordinate in farthest_coordinates):
+ farthest_coordinate = clues[i]
+ i += 1
+
+ farthest_coordinate_distance = __distance(farthest_coordinate, centroid)
+
+ for clue in clues:
+ if __distance(clue, centroid) > farthest_coordinate_distance and (not clue.required) and (clue not in farthest_coordinates):
+ farthest_coordinate = clue
+ farthest_coordinate_distance = __distance(clue, centroid)
+
+ farthest_coordinates.append(farthest_coordinate)
+
+ return farthest_coordinates
def __mean_center(clues: [Clue]):
diff --git a/dashboard_website/savefile.csv b/dashboard_website/savefile.csv
index 84b6c30..314a385 100644
--- a/dashboard_website/savefile.csv
+++ b/dashboard_website/savefile.csv
@@ -1 +1,56 @@
name,latitude,longitude,info,status
+A1,42.3430402738135,-71.0675801105357,Peters Park,UNVISITED
+A3,42.340735169280684,-71.0822142949978,Wally's Cafe Jazz Club,UNVISITED
+A4,42.340965725249276,-71.07004200293923,Cathedral of the Holy Cross,UNVISITED
+A5,42.34236478201366,-71.07314991031272,Betances Mural (80%),UNVISITED
+A6,42.34355826498339,-71.0779098454636,Harriet Tubman Memorial,UNVISITED
+A7,42.343917124461065,-71.06620549483627,Myers+Chang,UNVISITED
+A8,42.33836232114676,-71.0667887997758,Hidden Kitchen,UNVISITED
+A10,42.363845469421676,-71.10129788828729,La Fabrica Central Restaurant,UNVISITED
+A11,42.33000580911709,-71.08345739566941,"Dudley Cafe , Dudley Square",UNVISITED
+A12,42.321503645221576,-71.0863561083245,Malcolm X House,UNVISITED
+A13,42.329719158213535,-71.08393017974034,"Nubian Station, Roxbury",UNVISITED
+A14,42.32995937830152,-71.08425706494715,soleil,UNVISITED
+A15,42.31163492909362,-71.0944883387719,old bear pens,UNVISITED
+A17,42.33240052352865,-71.10001875238213,Lily's Gourmet Pasta Express,UNVISITED
+A18,42.30940190779075,-71.11503165360074,Loring Greenough House,UNVISITED
+A19,42.31599303331773,-71.0660247,Strand Theatre,UNVISITED
+A20,42.31145648101414,-71.11454193944768,noodle barn (?),UNVISITED
+A21,42.30758360954152,-71.11986574437587,Harvard Arboretum,UNVISITED
+A22,42.31703238067602,-71.1172176327592,JAMACIA POND BENCH,UNVISITED
+A23,42.32234399461923,-71.10765946762163,Lucy Parsons Center (Jamaica Plain),UNVISITED
+A24,42.312828161837565,-71.1142521044243,fire station turned JP Licks,UNVISITED
+A26,42.35772690789592,-71.0636244852727,Massachusetts State House,UNVISITED
+A27,42.35296368437526,-71.13261242438084,Brighton Music Hall,UNVISITED
+A28,42.3632259246665,-71.12855843461686,SWISS BAKERY ALSTON,UNVISITED
+A29,42.35298420423782,-71.13089261430888,Bonchon Boston,UNVISITED
+A30,42.36898885010578,-71.12306457136677,"Anderson bridge: a small brass plaque, the size of one brick, that is located on the
+brick wall of the Eastern (Weld Boathouse) side of the bridge, just
+north of the middle of the bridge span, about eighteen inches from the
+ground in a small alcove. It reads:
+""QUENTIN COMPSON
+Drowned in the odour of honeysuckle.
+1891-1910""",UNVISITED
+A31,42.353802,-71.136116,Boston Fire Dept. Station 41,UNVISITED
+A32,42.34038115716941,-71.16265204351588,McMullen Museum of art,UNVISITED
+A33,42.35037729554579,-71.14555952509022,Hal Connolly Statue,UNVISITED
+A34,42.3434052435984,-71.14279038706111,Tasca Restaurant,UNVISITED
+A35,42.34965767795442,-71.14606621964128,Brighton High School,UNVISITED
+A36,42.36580905967968,-71.12374919164488,Harvard Class of 1959 Chapel,UNVISITED
+A38,42.34341395011007,-71.09317746727483,agassiz road duck house,UNVISITED
+A39,42.35036326253947,-71.10650807481497,MLK statue on BU campus,UNVISITED
+A40,42.348465223671944,-71.09776617622576,Boston Hotel Buckminster,UNVISITED
+A41,42.34453026994564,-71.09720444433195,The Viridian (Apartment Building that has Blaze Pizza),UNVISITED
+A42,42.34138871428725,-71.14657093175214,Land of Fire Pizzeria,UNVISITED
+A43,42.3485725720217,-71.0942113926108,India Quality Restaurant,UNVISITED
+A44,42.34605480640728,-71.0960390033173,MAYBE Ted Williams Statue ,UNVISITED
+A46,42.35116095548625,-71.10606134524427,BU Beach,UNVISITED
+A47,42.3518271762187,-71.11983863137432,Paradise Rock Club (Commonwealth),UNVISITED
+A48,42.34517137203154,-71.09676153399613,The Verb Hotel,UNVISITED
+A49,42.3315920224197,-71.12571559999999,Cypress Street Playground,UNVISITED
+A50,42.335982955313646,-71.11231902990166,The Dutch House,UNVISITED
+A51,42.32590914866024,-71.13228373433928,Frederick Law Olmstead National Historic Site,UNVISITED
+A52,42.342764551151554,-71.12168301640884,Brookline Booksmith,UNVISITED
+A53,42.31487124359676,-71.22693938547769,Echo Bridge,UNVISITED
+A54,42.32485823589608,-71.16189103410989,Longyear Museum,UNVISITED
+A55,42.3320010747946,-71.15563010527421,Waterworks Museum,UNVISITED
diff --git a/dashboard_website/static/css/dashboard.css b/dashboard_website/static/css/dashboard.css
index b4110a7..8afb839 100644
--- a/dashboard_website/static/css/dashboard.css
+++ b/dashboard_website/static/css/dashboard.css
@@ -11,3 +11,10 @@
font-family:HWYGOTH;
color: lightgray;
}
+
+.disabled-class > * > * {
+ color: darkgray;
+}
+.disabled-class > * {
+ color: darkgray;
+} \ No newline at end of file
diff --git a/dashboard_website/static/js/controls.js b/dashboard_website/static/js/controls.js
index 3d81032..3f8b8a5 100644
--- a/dashboard_website/static/js/controls.js
+++ b/dashboard_website/static/js/controls.js
@@ -15,6 +15,13 @@ function loadSave(){
call("loadSave", "clean");
}
+function setHome(){
+ call("setHome", "home");
+}
+function generateRoutes(){
+ call("generateRoutes");
+}
+
function call(command, formid=""){
var formData;
if (formid == "") {
diff --git a/dashboard_website/static/js/dashboard.js b/dashboard_website/static/js/dashboard.js
index 43c07f3..a816327 100644
--- a/dashboard_website/static/js/dashboard.js
+++ b/dashboard_website/static/js/dashboard.js
@@ -34,19 +34,20 @@ var homeIcon = new baseIcon({iconUrl: 'static/img/marker-home.png'}),
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-green.png'}),
+ new baseIcon({iconUrl: 'static/img/marker-icon-blue.png'}),
new baseIcon({iconUrl: 'static/img/marker-icon-yellow.png'}) ];
-var teamIconsReq = [ disabledIconReq,
+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-green-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}
+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());
@@ -57,14 +58,23 @@ function zoomToClue(clue_name){
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 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) {
//osrm lat/long are swapped
for (var i = 0; i < route_coords_osrm.length; i++){
@@ -75,6 +85,7 @@ function drawRoute(route_coords_osrm, team_color) {
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++){
@@ -98,20 +109,23 @@ function drawRoutes() {
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 = "<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 statuscell = document.createElement("td"); statuscell.innerHTML = "<a href='#' onclick=\"toggleBikeEnable('"+index.toString()+"')\" "+((bike['team_status'] == "ACTIVE")? "style=\"color:lightgreen\"" : "") +">" + bike['team_status'] + " ("+parseInt(bike['time_since_last_contact']).toString()+"s)</a>";
var targetcell = document.createElement("td"); targetcell.innerHTML = "<a href=\"#\" onclick=\"zoomToClue('"+bike['target_clue']+"')\">"+bike['target_clue']+"</a>";
- var etacell = document.createElement("td"); etacell.innerHTML = "ETA: "+routes['cluster_times'][key];
+ var etacell = document.createElement("td"); etacell.innerHTML = "<input type='datetime-local' id='deadline_"+index+"' value='"+bike['team_deadline']+"' onchange='onDeadlineChange(this)'"+(bike['team_status'] == "DISABLED" ? 'disabled' : "")+"> ";
row.appendChild(namecell);
row.appendChild(statuscell);
row.appendChild(targetcell);
row.appendChild(etacell);
table.appendChild(row);
+ index += 1;
}
}
@@ -154,8 +168,8 @@ function drawClues(){
popupdiv.innerHTML += "<button id=\"visitbutton_"+clues[i]['clue_name']+"\" style='margin-left:2px' onclick=\"toggle_visit_clue('"+clues[i]['clue_name']+"')\""+(clues[i]['clue_status'] == "DISABLED" ? "disabled" : "")+">"+toggleVisitText+"</button>";
popupdiv.innerHTML += "<button id=\"enablebutton_"+clues[i]['clue_name']+"\" style='margin-left:2px' onclick=\"toggle_enable_clue('"+clues[i]['clue_name']+"')\""+(clues[i]['clue_status'] == "VISITED" ? "disabled" : "")+">"+toggleDisableText+"</button>";
popupdiv.innerHTML += "<button id=\"requirebutton_"+clues[i]['clue_name']+"\" style='margin-left:2px' onclick=\"toggle_required_clue('"+clues[i]['clue_name']+"')\">"+(clues[i]['clue_required'] ? "Un-require" : "Require")+"</button></br>";
- popupdiv.innerHTML += "<div style='color:grey'>Assigned team: <select style='color:grey' id='assignedteam_"+clues[i]['clue_name']+"'><option value='0'>None</option> <option value='1'>Team 1</option> <option value='2'>Team 2</option> <option value='3'>Team 3</option> <option value='4'>Team 4</option></select></span>";
- var clueMarker = L.marker([clues[i]['longitude'], clues[i]['latitude']], {icon: tempIcon}).bindPopup(popupdiv);
+ popupdiv.innerHTML += "<div style='color:grey'>Assigned team: <select style='color:grey' id='assignedteam_"+clues[i]['clue_name']+"' onchange=\"assignedTeamChanged('"+clues[i]['clue_name']+"', this.value);\"><option value='0'>None</option> <option value='1'>Team 1</option> <option value='2'>Team 2</option> <option value='3'>Team 3</option></select></span>";
+ 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);
@@ -163,6 +177,18 @@ function drawClues(){
}
}
+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);
@@ -224,6 +250,25 @@ function toggle_enable_clue(clue_name){
.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;
@@ -273,13 +318,14 @@ function requestLatestInfo(){
}
// process bikes
if(true || json['bikes_changed']){ // always true since we need constant updates for bikes
- bikes_t = json['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']]);
+ 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;
}
@@ -288,6 +334,7 @@ function requestLatestInfo(){
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);
}
}
@@ -325,11 +372,32 @@ function requestLatestInfo(){
.then((response) => response.json())
.then((json) => handleLatestInfo(json));
}
+
+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 clockInterval = window.setInterval(function(){
var d = new Date();
document.getElementById("titletime").innerText = d.toLocaleString();
}, 1000);
diff --git a/dashboard_website/templates/controls.html b/dashboard_website/templates/controls.html
index 97b3b06..be5f659 100644
--- a/dashboard_website/templates/controls.html
+++ b/dashboard_website/templates/controls.html
@@ -10,8 +10,10 @@
<body>
<a href="..">Back</a> <br>
+<hr>
<button onclick="downloadCurrent()">Download Current Savefile (.CSV)</button>
<br>
+<br>
<form id="dirty" name="dirty">
<label for="dirtyfile">Choose dirty clue list to upload</label>
<input id="dirtyfile" name="dirtyfile" type="file" accept=".csv,.CSV"/>
@@ -23,6 +25,18 @@
<input id="cleanfile" name="cleanfile" type="file" accept=".csv,.CSV"/>
<button type="button" onclick="loadSave()">Upload</button>
</form>
+<br>
+<form id="home" name="home">
+ <label for="longitude">Longitude</label>
+ <input id="longitude" name="longitude" autocomplete="off"/>
+ <label for="latitude">Latitude</label>
+ <input id="latitude" name="latitude" autocomplete="off"/>
+ <button type="button" onclick="setHome()">Set Home Location</button>
+</form>
+<br>
+<form id="routes" name="routes">
+ <button type="button" onclick="generateRoutes()">Generate Routes</button>
+</form>
</body>
</html> \ No newline at end of file
diff --git a/dashboard_website/templates/index.html b/dashboard_website/templates/index.html
index fc61472..9c604e0 100644
--- a/dashboard_website/templates/index.html
+++ b/dashboard_website/templates/index.html
@@ -136,8 +136,11 @@
<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 style="color:white;margin: 0px;margin-left:15px">
+ CLUBHOUSE HQ |
+ <span id="titletime" style="color:lightgray; display:inline-block; width: 350px;"></span> |
+ <input id="searchfilter" style="vertical-align: middle;" placeholder="Filter clues" onchange="filterClues(this.value)" onkeyup="filterClues(this.value)" autocomplete=off/> |
+ <a style="font-size:14pt; vertical-align: middle;" href="/controls">Settings</a>
</h1>
</div>
<div class="map-frame">
@@ -174,11 +177,13 @@
<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"
+ autocomplete=off
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"
+ autocomplete=off
style="flex:1;"
placeholder="e.g., Savenor's Butcher"/>
</div>