summaryrefslogtreecommitdiff
path: root/dashboard_website
diff options
context:
space:
mode:
Diffstat (limited to 'dashboard_website')
-rw-r--r--dashboard_website/__pycache__/datastructs.cpython-311.pycbin4422 -> 7348 bytes
-rw-r--r--dashboard_website/__pycache__/db.cpython-311.pycbin6886 -> 8538 bytes
-rw-r--r--dashboard_website/__pycache__/router.cpython-311.pycbin17589 -> 18525 bytes
-rw-r--r--dashboard_website/dashboard.py160
-rw-r--r--dashboard_website/datastructs.py55
-rw-r--r--dashboard_website/db.py113
-rw-r--r--dashboard_website/router.py79
-rw-r--r--dashboard_website/static/js/dashboard.js38
-rw-r--r--dashboard_website/templates/index.html9
9 files changed, 351 insertions, 103 deletions
diff --git a/dashboard_website/__pycache__/datastructs.cpython-311.pyc b/dashboard_website/__pycache__/datastructs.cpython-311.pyc
index 62fbcab..6eb883a 100644
--- a/dashboard_website/__pycache__/datastructs.cpython-311.pyc
+++ b/dashboard_website/__pycache__/datastructs.cpython-311.pyc
Binary files differ
diff --git a/dashboard_website/__pycache__/db.cpython-311.pyc b/dashboard_website/__pycache__/db.cpython-311.pyc
index b1e752a..e463978 100644
--- a/dashboard_website/__pycache__/db.cpython-311.pyc
+++ b/dashboard_website/__pycache__/db.cpython-311.pyc
Binary files differ
diff --git a/dashboard_website/__pycache__/router.cpython-311.pyc b/dashboard_website/__pycache__/router.cpython-311.pyc
index 57b79d2..7cbbd01 100644
--- a/dashboard_website/__pycache__/router.cpython-311.pyc
+++ b/dashboard_website/__pycache__/router.cpython-311.pyc
Binary files differ
diff --git a/dashboard_website/dashboard.py b/dashboard_website/dashboard.py
index 59fa0df..aab38b0 100644
--- a/dashboard_website/dashboard.py
+++ b/dashboard_website/dashboard.py
@@ -21,30 +21,43 @@ app = Flask(__name__)
# "longitude" : xx.xxxxxx, float //current team location
# "latitude" : xx.xxxxxx, float }
# Returns JSON
-# {"team_name" : "XXXX", str
-# "status" : "OK"/"ERROR XX", str
-# "clue_name" : "XXXX", str
-# "clue_long" : xx.xxxxxx, float
-# "clue_lat" : xx.xxxxxx, float
-# "clue_info" : "Xxxx xxx xxx", str
-# "route" : {...}, JSON } 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 field types, 3 = invalid coordinates, 4 = team does not exist, 5 = could not find route, 10 = other/network error
+# {"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():
- pass
+ content = request.get_json()
+ if not ('team_name' in content and 'longitude' in content and 'latitude' 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})
+
+ return jsonify({'status' : "OK"})
# disable eligibility to receive routes, e.g. if team is done while another is not. can be re-enabled either in app or on dashboard
# responds with your next route
@app.route("/disableTeam", methods=['POST'])
# Expected JSON
-# {"team_name" : "XXXX", str
-# "longitude" : xx.xxxxxx, float //current team location
-# "latitude" : xx.xxxxxx, float }
+# {"team_name" : "XXXX", str }
# Returns JSON
-# {"team_name" : "XXXX", str
-# "status" : "OK"/"ERROR XX", str }
-# ERROR CODES: 1 = missing fields, 2 = invalid field types, 3 = invalid coordinates, 4 = team does not exist, 10 = other/network error
+# {"status" : "OK"/"ERROR XX", str }
+# ERROR CODES: 1 = missing fields, 4 = team does not exist,
def disableTeam():
- pass
+ content = request.get_json()
+ if not ('team_name' in content):
+ status = "ERROR 1"
+ return jsonify({'status' : status})
+
+ if db.deleteBike(content['team_name']) == 4:
+ status = "ERROR 4"
+ return jsonify({'status' : status})
+
+ return jsonify({'status' : "OK"})
# requests a route to the best clue given the team's current coordinates
@app.route("/requestRoute", methods=['POST'])
@@ -53,16 +66,43 @@ def disableTeam():
# "longitude" : xx.xxxxxx, float //current team location
# "latitude" : xx.xxxxxx, float }
# Returns JSON
-# {"team_name" : "XXXX", str
-# "status" : "OK"/"ERROR XX", str
+# {"status" : "OK"/"ERROR XX", str
# "clue_name" : "XXXX", str
# "clue_long" : xx.xxxxxx, float
# "clue_lat" : xx.xxxxxx, float
# "clue_info" : "Xxxx xxx xxx", str
# "route" : {...}, JSON } 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 field types, 3 = invalid coordinates, 5 = could not find route, 10 = other/network error
+# ERROR CODES: 1 = missing fields, 2 = invalid coordinates, 3 = cluster calculation in progress, 4 = team not found, 5 = no more clues, 6 = routing error
def requestRoute():
- pass
+ content = request.get_json()
+ # verify request
+ if not ('team_name' 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:
+ return jsonify({'status' : "ERROR 4"})
+
+ if db.currently_updating:
+ return jsonify({'status' : "ERROR 3"})
+
+ bike, clue = db.getBikeCluePair(content['team_name'])
+ if clue == None:
+ return jsonify({'status' : "ERROR 5"})
+
+ route = getRouteFullJSON(bike, clue)
+ if route['code'] != 'Ok': # or some other code indicating routing problem?
+ return jsonify({'status' : "ERROR 6"})
+
+ reply = {"status" : "OK",
+ "clue_name" : clue.name,
+ "clue_long" : clue.longitude,
+ "clue_lat" : clue.latitude,
+ "clue_info" : clue.info,
+ "route" : route}
+ return jsonify(reply)
# periodically called to update team location in the management dashboard
@app.route("/updateTeamLocation", methods=['POST'])
@@ -71,23 +111,83 @@ def requestRoute():
# "longitude" : xx.xxxxxx, float
# "latitude" : xx.xxxxxx, float }
# Returns JSON
-# {"team_name" : "XXXX", str
-# "status" : "OK"/"ERROR XX" }
-# ERROR CODES: 1 = missing fields, 2 = invalid field types, 3 = invalid coordinates, 10 = other/network error
+# {"status" : "OK"/"ERROR XX" }
+# ERROR CODES: 1 = missing fields, 2 = invalid coordinates, 4 = no active team found under given name,
def updateTeamLocation():
+ status = "OK"
content = request.get_json()
- db.pingBike(content['team_name'], content['latitude'], content['longitude'])
- return jsonify({'team_name' : content['team_name'], 'status' : "OK"})
+ if not ('team_name' in content and 'longitude' in content and 'latitude' 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.pingBike(content['team_name'], content['latitude'], content['longitude']) == 4:
+ status = "ERROR 4"
+ return jsonify({'status' : status})
+
+ return jsonify({'status' : "OK"})
+
+# mark clue as visited from app
+@app.route("/visitClueTeam", methods=['POST'])
+# Expected JSON
+# {"team_name" : xxxx, str
+# "clue_name" : xxxx, str
+# "longitude" : xx.xxxxxx, float
+# "latitude" : xx.xxxxxx, float }
+# Returns JSON
+# {"status" : "OK"/"ERROR XX" }
+# 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):
+ 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:
+ return jsonify({'status' : "ERROR 4"})
+
+ result = db.visitClueTeam(content['team_name'], content['clue_name'])
+ if result != 0:
+ return jsonify({'status' : f"ERROR {result}"})
+ return jsonify({'status' : "OK"})
+
+#
+# DISCORD BOT API
+#
+@app.route("/visitClueGeneric", methods=['POST'])
+# Expected JSON
+# {"clue_name" : xxxx, str}
+# Returns JSON
+# {"status" : "OK"/"ERROR XX" }
+# ERROR CODES: 1 = missing fields, 2 = clue doesn't exist, 3 = already marked as visited
+def visitGeneric():
+ content = request.get_json()
+ if not ('clue_name' in content):
+ return jsonify({'status' : "ERROR 1"})
+
+ result = db.visitClue(content['clue_name'])
+ if result != 0:
+ return jsonify({'status' : f"ERROR {result}"})
+ return jsonify({'status' : "OK"})
#
-# WEB PAGES + DASHBOARD API
+# WEB / DASHBOARD API
#
# send updated bike/clue/home info
# POST = request above
@app.route("/getLatestInfo", methods=['POST'])
def getLatestInfo():
- db.moveBike2Test()
+ # run first update
+ if db.startup == False:
+ db.startup = True
+ db.updateRoutes()
content = request.get_json()
last_timestamp = content['info_age']
data = {'timestamp' : db.getTime(),
@@ -106,8 +206,10 @@ def getLatestInfo():
if r != False:
data['routes_changed'] = True
data['routes'] = r
+ data['calculating_routes'] = db.currently_updating
data['bikes'] = db.getBikesJSON()
data['status'] = "OK"
+
return jsonify(data)
@@ -126,6 +228,6 @@ if __name__ == "__main__":
app.secret_key = 'hf8f3sd0zmqpmhss7dr3'
# local test
- app.run(host="127.0.0.1", port=5001, debug=True)
+ #app.run(host="127.0.0.1", port=5001, debug=True)
# production
- #app.run(host="96.126.106.128", port=5001, debug=True)
+ app.run(host="96.126.106.128", port=5001, debug=True)
diff --git a/dashboard_website/datastructs.py b/dashboard_website/datastructs.py
index 6870521..6966ca9 100644
--- a/dashboard_website/datastructs.py
+++ b/dashboard_website/datastructs.py
@@ -1,7 +1,7 @@
-import time
+import time, math
# time since last ping before deactivating/deleting
-BIKE_TIMEOUT = 60
-BIKE_DELETE = 1800 # time before bike deletes itself
+BIKE_TIMEOUT = 10 # 3 minutes
+BIKE_DELETE = 20 # time before bike deletes itself
# data structures
class Point:
@@ -24,6 +24,21 @@ class Point:
def __str__(self):
return f"{self.longitude},{self.latitude}"
+
+ def __repr__(self):
+ return f"{self.longitude},{self.latitude}"
+
+ def distanceTo(self, pt): # distance between points in miles
+ lat1 = self.latitude; lon1 = self.longitude;
+ lat2 = pt.latitude; lon2 = pt.longitude;
+ R = 3958.8 # Radius of the earth
+ lat_d = math.radians(lat2-lat1);
+ lon_d = math.radians(lon2-lon1);
+ a = math.sin(dLat/2) * math.sin(dLat/2) + math.cos(radians(lat1)) * math.cos(radians(lat2)) * math.sin(dLon/2) * math.sin(dLon/2)
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a));
+ d = R * c; # Distance in mi
+ return d
+
class Clue(Point):
def __init__(self, lat, long, name, info, status):
@@ -50,19 +65,41 @@ class Bike(Point):
self.latitude = lat
self.name = name
self.last_contact = time.time()
- self.target = "N/A"
- self.route_to_next = [] # list of coords if target isnt' N/A
+ self.target_name = "N/A"
+ self.cluster = [] # list of clues this bike team is responsible for
self.status = status # ACTIVE | INACTIVE
def setTarget(self, clue_name):
- self.target = clue_name
+ self.target_name = clue_name
+
+ def setCluster(self, clue_cluster):
+ self.cluster = clue_cluster
+ self.updateTarget()
+
+ def updateTarget(self):
+ if len(self.cluster) <= 0:
+ self.target_name = "N/A"
+ else:
+ self.target_name = self.cluster[0].name
+
+ def visitTarget(self):
+ self.cluster[0].visit()
+ self.cluster.pop(0)
+ self.updateTarget()
+ while len(self.cluster) > 0 and cluster[0].status == "VISITED":
+ self.cluster.pop(0) # skip next node if it has been somehow visited
+ self.updateTarget()
+
def ping(self):
- if self.status != "ACTIVE":
- updateRoutes()
self.status = "ACTIVE"
self.last_contact = time.time()
+ def disable(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"
@@ -77,5 +114,5 @@ class Bike(Point):
'time_since_last_contact' : time.time()-self.last_contact,
'team_name' : self.name,
'team_status' : self.status,
- 'target_clue' : self.target}
+ 'target_clue' : self.target_name}
return json_dict \ No newline at end of file
diff --git a/dashboard_website/db.py b/dashboard_website/db.py
index 892fa0b..1bc1f3c 100644
--- a/dashboard_website/db.py
+++ b/dashboard_website/db.py
@@ -3,25 +3,50 @@
from datastructs import *
import router
import csv, time
+from threading import Thread
+# constants
+CLUE_MIN_DISTANCE = 0.1 # minimum distance between clue and bike to be considered valid visitation
# variables
homeBase = Point(42.340226, -71.088395) # krentzman, can be changed on dashboard
clues = []
bikes = []
-routes = {"clusters" : [], "individual_routes" : []} #geojson polylines, both between all the clusters
+clusters = []
+routes = {"clusters" : [], "cluster_times" : {}, "individual_routes" : []} #geojson polylines, both between all the clusters
assigned_clues = []
clues_last_changed = time.time()
home_last_changed = time.time()
routes_last_changed = time.time()
+currently_updating = False
+
+startup = False #
# called every time a node is added
# a bike is added/removed
# determines/assigns clusters, and assigns routes to bikes
-def updateRoutes():
- clusters, paths = router.getClusters(bikes, clues, homeBase)
+def updateRoutes_background(): # run in thread due to long runtime
+ global currently_updating, routes_last_changed, routes
+ print("Calculating clusters...")
+ routes = {"clusters" : [], "cluster_times" : {}, "individual_routes" : []} # reset
+ clusters, paths, times = router.getClusters(bikes, clues, homeBase)
+ 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])
+
routes_last_changed = time.time()
+ print("Finished calculating clusters/routes.")
+ currently_updating = False
+
+def updateRoutes(): # if necessary
+ global currently_updating
+ if not currently_updating:
+ currently_updating = True
+ t = Thread(target=updateRoutes_background)
+ t.start()
# interface functions
def getTime():
@@ -36,6 +61,18 @@ def setHomeBase(latitude, longitude):
homeBase.setCoords(latitude, longitude)
home_last_changed = time.time()
+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:
+ c = clue
+ break
+ break
+ return b, c
+
def getRoutesJSON(timestamp):
if timestamp < 0 or routes_last_changed - timestamp > 0:
@@ -43,30 +80,37 @@ 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
- bike.ping()
- bike.setCoords(latitude, longitude)
- return # return code indicating already exists, although status still OK
+ return 4 # already exists
newBike = Bike(latitude, longitude, team_name, "ACTIVE")
bikes.append(newBike)
+ updateRoutes()
+ return 0
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)
- updateRoutes()
-
+ return 0
+ return 4 # team not found
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]
@@ -85,13 +129,48 @@ def deleteClue(clue_name):
if clue.name == clue_name:
clues.pop(i)
clues_last_changed = time.time()
+ updateRoutes()
break
def visitClue(clue_name):
for clue in clues:
if clue.name == clue_name:
+ if clue.status == "VISITED":
+ return 3 # already visited
clue.visit()
clues_last_changed = time.time()
+ #updateRoutes()
+ return 0 # OK
+ return 2 # no clue found
+
+def visitClueTeam(team_name, clue_name):
+ b = None
+ for bike in bikes:
+ if bike.name == team_name:
+ b = bike
+ else:
+ return 4 # team not found
+ c = None
+ for clue in clues:
+ if clue.name == clue_name:
+ c = clue
+ if c.status == "VISITED":
+ return 6 # already visited
+ break # continue
+ else:
+ return 5 # clue not found
+
+ # if visited clue is the expected one (current target)
+ if clue_name == b.target_name:
+ if c.distanceTo(b) < CLUE_MIN_DISTANCE:
+ return 3 # too far away
+ b.visitTarget()
+ clues_last_changed = time.time()
+ return 0
+
+ # otherwise
+ c.visit()
+ clues_last_changed = time.time()
# junk for testing
with open("all_clues.csv", newline='') as f:
@@ -101,14 +180,6 @@ with open("all_clues.csv", newline='') as f:
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")
+ newClue = Clue(coords[0], coords[1], f"Clue #{i}", row[0], "UNVISITED" if i < 100 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")
-bikes.append(bike1); bikes.append(bike2)
-updateRoutes()
-def moveBike2Test():
- bike1.move(0, -0.001); bike1.ping();
diff --git a/dashboard_website/router.py b/dashboard_website/router.py
index 5bafd1e..5d5a909 100644
--- a/dashboard_website/router.py
+++ b/dashboard_website/router.py
@@ -1,9 +1,12 @@
import numpy as np
import requests
from sklearn.cluster import KMeans
+import time
+from datetime import datetime
from datastructs import *
+
host = "http://acetyl.net:5000" # queries acetyl.net:5000, the OSRM engine
@@ -12,22 +15,26 @@ host = "http://acetyl.net:5000" # queries acetyl.net:5000, the OSRM engine
# gets single leg route between bike and clue
# should be HD and GeoJSON
def getRouteFullJSON(bike, clue):
+ bike = bike.toJSON(); clue = clue.toJSON()
url = f"{host}/route/v1/bike/{bike['longitude']},{bike['latitude']};{clue['longitude']},{clue['latitude']}?steps=true&overview=full&geometries=geojson"
- r = response.get(url)
+ r = requests.get(url)
return r.json()
def getRouteHDPolyline(bike, clue):
+ bike = bike.toJSON(); clue = clue.toJSON()
url = f"{host}/route/v1/bike/{bike['longitude']},{bike['latitude']};{clue['longitude']},{clue['latitude']}?overview=full&geometries=geojson"
- r = response.get(url)
- p = r.json()['trips'][0]['geometry']['coordinates']
+ r = requests.get(url)
+
+ p = r.json()['routes'][0]['geometry']['coordinates']
return p
def getRouteFastPolyline(bike, clue):
+ bike = bike.toJSON(); clue = clue.toJSON()
url = f"{host}/route/v1/bike/{bike['longitude']},{bike['latitude']};{clue['longitude']},{clue['latitude']}?geometries=geojson"
- r = response.get(url)
- p = r.json()['trips'][0]['geometry']['coordinates']
+ r = requests.get(url)
+ p = r.json()['routes'][0]['geometry']['coordinates']
return p
@@ -37,23 +44,25 @@ def getZSP(bike, home, clue_cluster):
# determines clusters based on current bikes and clues
def getClusters(bikes, clues, endpoint):
-
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"]
if len(active_bikes) == 0:
- return clusters, route_geos
+ return clusters, route_geos, times
active_clues = [clue for clue in clues if clue.status == "UNVISITED"]
# select only active bikes
# select only unvisited clues
- clusters_t, route_geos_t = cluster_and_optimize(active_clues, active_bikes, endpoint)
+ clusters_t, route_geos_t, times_t = cluster_and_optimize(active_clues, active_bikes, endpoint)
for i in range(len(active_indices)):
route_geos[active_indices[i]] = route_geos_t[i]
- clusters[active_indices[i]] = clusters_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]
# return list of clue clusters corresponding to bikes
- return clusters, route_geos
+ return clusters, route_geos, times
# utility functions (internal)
@@ -68,39 +77,46 @@ def cluster_and_optimize(clues: [Clue], bikes: [Bike], end: Point, time_diff=0.2
: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
"""
-
- # Create a new column with normalized gps coordinates and centroids
- normalized_points, norm_centroids = __normalize_points(clues, bikes)
- print(norm_centroids)
- # 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)
-
+ 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)
+
# Remove waypoints from the longest route until the trip time is less than the max time
for i in range(len(routes)):
routes[i] = __remove_longest_waypoints(routes[i], bikes[i], end, max_time)
+
# Get the json of the routes
route_waypoints = []
geometries = []
+ times = []
for i, route in enumerate(routes):
route_json = __get_json(__clues_to_string(route), __clues_to_string([bikes[i]]), __clues_to_string([end])[:-1])
geometries.append(route_json['trips'][0]['geometry']['coordinates'])
route_waypoints.append(route_json['waypoints'])
+ eta = time.time() + route_json['trips'][0]['duration'] + 90 * len(route)
+ eta_str = datetime.fromtimestamp(eta).strftime("%I:%M:%S%p")
+ times.append(eta_str)
# Use the waypoint_index to reorder each route
for i, route in enumerate(routes):
- route = [ route[ j['waypoint_index']-1 ] for j in route_waypoints[i] if route_waypoints[i].index(j) < (len(route_waypoints[i])-1) ]
- routes[i] = route
+ route2 = ["" for x in route]
+ for j,k in enumerate(route_waypoints[i][1:-1]):
+ route2[ k['waypoint_index']-1 ] = route[j]
+ routes[i] = route2
- return routes, geometries
+ return routes, geometries, times
def __clues_to_string(points: [Clue]):
@@ -125,13 +141,13 @@ def __get_json(coordinate_string, start, end):
:return: the json of the route
"""
coordinates = requests.get(
- 'http://acetyl.net:5000/trip/v1/bike/' + start + coordinate_string + end + '?roundtrip=false&source=first&destination=last&geometries=geojson')
+ 'http://acetyl.net:5000/trip/v1/bike/' + start + coordinate_string + end + '?roundtrip=false&source=first&destination=last&geometries=geojson&overview=full')
coordinates = coordinates.json()
return coordinates
-def __get_trip_time(coordinate_string, num_waypoints, start, end, time_per_waypoint=90):
+def __get_trip_time(coordinate_string, num_waypoints, start, end, time_per_waypoint=90, seconds=False):
"""
Takes a string of coordinates and returns the trip time in hours
:param coordinate_string: a string of coordinates
@@ -147,7 +163,8 @@ def __get_trip_time(coordinate_string, num_waypoints, start, end, time_per_waypo
travel_time_seconds = int(coordinates['trips'][0]['duration'])
waypoint_time_seconds = num_waypoints * time_per_waypoint
-
+ if seconds:
+ return (travel_time_seconds + waypoint_time_seconds)
total_time_hours = (travel_time_seconds + waypoint_time_seconds) / 3600
return total_time_hours
diff --git a/dashboard_website/static/js/dashboard.js b/dashboard_website/static/js/dashboard.js
index 0248844..a67f85f 100644
--- a/dashboard_website/static/js/dashboard.js
+++ b/dashboard_website/static/js/dashboard.js
@@ -57,12 +57,25 @@ function drawRoute(route_coords_osrm, team_color) {
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);
+ var route = new L.polyline(route_coords_osrm, {color: team_color});
+ routes_m.addLayer(route);
}
function drawRoutes() {
+ routes_m.clearLayers();
for (var i = 0; i < routes['clusters'].length; i++){
if(routes['clusters'][i].length > 0){
- drawRoute(routes['clusters'][i], i%2 == 0 ? 'red' : 'yellow');
+ var color;
+ var r = Math.floor(Math.random() * 155+100);
+ var g = 0; // no greens -- avoid yellow
+ var b = Math.floor(Math.random() * 155+100);
+ color= "rgb("+r+","+g+","+ b+")";
+ console.log(color);
+ drawRoute(routes['clusters'][i], color);
+ }
+ }
+ for (var i = 0; i < routes['individual_routes'].length; i++){
+ if(routes['individual_routes'][i].length > 0){
+ drawRoute(routes['individual_routes'][i], 'yellow');
}
}
}
@@ -77,10 +90,12 @@ function updateBikeStatus(){
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>";
+ var etacell = document.createElement("td"); etacell.innerHTML = "ETA: "+routes['cluster_times'][key];
row.appendChild(namecell);
row.appendChild(statuscell);
row.appendChild(targetcell);
+ row.appendChild(etacell);
table.appendChild(row);
}
}
@@ -138,9 +153,16 @@ function requestLatestInfo(){
if(homemarker)homemarker.setLatLng([homebase['latitude'], homebase['longitude']]);
else homemarker = L.marker([homebase['latitude'], homebase['longitude']], {icon: homeIcon}).addTo(map).bindPopup("Home is where the club is.");
}
+ // process routes
+ if(json['routes_changed']){
+ routes = json['routes'];
+ console.log(routes);
+ drawRoutes();
+ }
// process bikes
if(true || json['bikes_changed']){ // always true since we need constant updates for bikes
- var bikes_t = json['bikes'];
+ bikes_t = json['bikes'];
+
for (var i = 0; i < bikes_t.length; i++){
var name = bikes_t[i]['team_name'];
if(name in bikes) {
@@ -178,11 +200,8 @@ function requestLatestInfo(){
drawClues();
updateClueStats();
}
- // process routes
- if(json['routes_changed']){
- routes = json['routes'];
- drawRoutes();
- }
+
+ document.getElementById("routeinfo").innerText = json['calculating_routes'] ? "ROUTE INFO | (Calculating...)" : "ROUTE INFO";
}
fetch(host+'/getLatestInfo', {
method: "POST",
@@ -196,7 +215,7 @@ function requestLatestInfo(){
}
var intervalId = window.setInterval(function(){
requestLatestInfo();
- }, 5000);
+ }, 3000);
var clockINterval = window.setInterval(function(){
var d = new Date();
@@ -226,6 +245,7 @@ window.onload = function() {
map.addLayer(visited_clues_m);
map.addLayer(unvisited_clues_m);
map.addLayer(bikes_m);
+ map.addLayer(routes_m);
var layerControl = L.control.layers(null,null,{collapsed:false});
layerControl.addOverlay(unvisited_clues_m, "Unvisited Clues");
layerControl.addOverlay(visited_clues_m, "Visited Clues");
diff --git a/dashboard_website/templates/index.html b/dashboard_website/templates/index.html
index 1317145..73f9f22 100644
--- a/dashboard_website/templates/index.html
+++ b/dashboard_website/templates/index.html
@@ -2,7 +2,7 @@
<html style="height:100%">
<head>
- <title>MMHC HQ</title>
+ <title>MMCH 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') }}"
@@ -76,7 +76,7 @@
}
.route-controls {
/* flex:0.6 1 auto; */
- flex-grow:0.15;
+ height:15%;
display: flex;
flex-direction: column;
}
@@ -84,7 +84,7 @@
.right-column {
/* flex:0.25 1 auto; */
flex-grow:0;
- flex-basis:25em;
+ flex-basis:35em;
display: flex;
flex-direction: column;
gap: 5px;
@@ -142,8 +142,9 @@
<div id="map"></div>
</div>
<div class="route-controls">
- <span style="margin:-1px;margin-left:5px;margin-top:5px;">ROUTE PARAMETERS</span>
+ <span style="margin:-1px;margin-left:5px;margin-top:5px;" id="routeinfo">ROUTE INFO</span>
<hr style="width:100%;padding:0px;"/>
+ <div id="etas"></div>
</div>
</div>
<div class="right-column">