From a3db30e2a06685aa2976ac8062040b4657109523 Mon Sep 17 00:00:00 2001 From: Anson Bridges Date: Fri, 10 Nov 2023 16:46:09 -0500 Subject: ROUGH DRAFT --- .../__pycache__/datastructs.cpython-311.pyc | Bin 4422 -> 7348 bytes dashboard_website/__pycache__/db.cpython-311.pyc | Bin 6886 -> 8538 bytes .../__pycache__/router.cpython-311.pyc | Bin 17589 -> 18525 bytes dashboard_website/dashboard.py | 160 +++++++++++++++++---- dashboard_website/datastructs.py | 55 +++++-- dashboard_website/db.py | 113 ++++++++++++--- dashboard_website/router.py | 79 ++++++---- dashboard_website/static/js/dashboard.js | 38 +++-- dashboard_website/templates/index.html | 9 +- 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 Binary files a/dashboard_website/__pycache__/datastructs.cpython-311.pyc and b/dashboard_website/__pycache__/datastructs.cpython-311.pyc differ diff --git a/dashboard_website/__pycache__/db.cpython-311.pyc b/dashboard_website/__pycache__/db.cpython-311.pyc index b1e752a..e463978 100644 Binary files a/dashboard_website/__pycache__/db.cpython-311.pyc and b/dashboard_website/__pycache__/db.cpython-311.pyc differ diff --git a/dashboard_website/__pycache__/router.cpython-311.pyc b/dashboard_website/__pycache__/router.cpython-311.pyc index 57b79d2..7cbbd01 100644 Binary files a/dashboard_website/__pycache__/router.cpython-311.pyc and b/dashboard_website/__pycache__/router.cpython-311.pyc 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 = ""+name+""; var statuscell = document.createElement("td"); statuscell.innerHTML = "" + bike['team_status'] + " ("+parseInt(bike['time_since_last_contact']).toString()+"s)"; var targetcell = document.createElement("td"); targetcell.innerHTML = ""+bike['target_clue']+""; + var etacell = document.createElement("td"); etacell.innerHTML = "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 @@ - MMHC HQ + MMCH HQ
- ROUTE PARAMETERS + ROUTE INFO
+
-- cgit v1.2.3