# stores and manages clue DB # 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 # variables homeBase = Point(42.340226, -71.088395) # krentzman, can be changed on dashboard clues = [] bikes = [] clusters = [] routes = {"clusters": [], "cluster_times": {}, "individual_routes": [], "clue_clusters" : []} # 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 = True # if false, currently assigned clues routesToCommit = False # if there exist preview routes to commit _bikes_changed = False # In case bikes or clues are enabled/disabled _clues_changed = False # during route calculation _force_stop = False startup = False # def should_cancel_routing(): global _bikes_changed, _clues_changed, _force_stop if _bikes_changed or _clues_changed or _force_stop: _bikes_changed = False _clues_changed = False _force_stop = False return True return False def force_stop(): global _force_stop, currently_updating currently_updating = False _force_stop = True def is_route_update_required(): return route_update_required def toggleMinimalRouting(): global minimalRouting minimalRouting = not minimalRouting # 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, route_update_required, preview_routes, clusters, routesToCommit, _bikes_changed, _clues_changed print("Calculating clusters...") _bikes_changed = False _clues_changed = False _force_stop = False 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.") currently_updating = False return -1 preview_routes['individual_routes'] = [[] for bike in bikes] 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]) routes_last_changed = time.time() print("Finished calculating clusters/routes.") routesToCommit = True currently_updating = False route_update_required = False def updateRoutes(): # if necessary global currently_updating if not currently_updating: save() currently_updating = True t = Thread(target=updateRoutes_background) t.start() return 0 def applyPreviewRoutes(): global routes_last_changed, routesToCommit, preview_routes, routes # set routes routes['individual_routes'] = preview_routes['individual_routes'].copy() routes['cluster_times'] = preview_routes['cluster_times'].copy() routes['clusters'] = preview_routes['clusters'].copy() routes['clue_clusters'] = preview_routes['clue_clusters'].copy() for i, bike in enumerate(bikes): if bike.status == "DISABLED": continue bike.setCluster(routes['clue_clusters'][i]) preview_routes = {"clusters": [], "cluster_times": {}, "individual_routes": [], "clue_clusters" : []} # reset routesToCommit = False routes_last_changed = time.time() # interface functions def getTime(): return time.time() def reassignIndividualRoute(bike, route_json): global routes_last_changed for i, b in enumerate(bikes): if b.name == bike.name: routes['individual_routes'][i] = route_json['routes'][0]['geometry']['coordinates'] routes_last_changed = time.time() return def getHomeBaseJSON(timestamp): if timestamp < 0 or home_last_changed - timestamp > 0: return homeBase.toJSON() return False def setHomeBase(latitude, longitude): 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_index): b = bikes[team_index-1] c = b.target_clue return b, c def getRoutesJSON(timestamp): if timestamp < 0 or routes_last_changed - timestamp > 0: preview_routes_t = preview_routes.copy() routes_t = routes.copy() del preview_routes_t["clue_clusters"] del routes_t["clue_clusters"] return preview_routes_t, routes_t return False, False 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, _bikes_changed bike = bikes[team_index-1] route_update_required = True _bikes_changed = True if enabled: bike.enable() else: bike.disable() return 0 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(): return [ x.toJSON() for x in bikes ] def getCluesJSON(timestamp): if timestamp < 0 or clues_last_changed - timestamp > 0: return [x.toJSON() for x in clues] return False def addClue(clue_name, clue_info, longitude, latitude, status="UNVISITED", required=False): global clues_last_changed, route_update_required, _clues_changed for clue in clues: if clue.name == clue_name: return -1 # clue already exists newClue = Clue(latitude, longitude, clue_name, clue_info, status, required=required) clues.append(newClue) clues_last_changed = time.time() route_update_required = True _clues_changed = True return 0 def deleteClue(clue_name): global route_update_required, _clues_changed for i, clue in enumereate(clues): if clue.name == clue_name: clues.pop(i) clues_last_changed = time.time() route_update_required = True _clues_changed = True break def visitClue(clue_name, unvisit=False): global clues_last_changed, route_update_required, _clues_changed for clue in clues: if clue.name == clue_name: if clue.status == "VISITED": if unvisit: clue.unvisit() clues_last_changed = time.time() return 0 return 3 # already visited clue.visit() clues_last_changed = time.time() route_update_required = True _clues_changed = True return 0 # OK return 2 # no clue def toggleEnableClue(clue_name): global clues_last_changed, route_update_required for clue in clues: if clue.name == clue_name: clue.toggle_enable() clues_last_changed = time.time() route_update_required = True return 0 # OK return 2 # no clue def toggleClueRequired(clue_name): global clues_last_changed, route_update_required for clue in clues: if clue.name == clue_name: clue.toggle_required() clues_last_changed = time.time() 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 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) # no need to recalculate 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 # otherwise c.visit() clues_last_changed = time.time() route_update_required = True def load(filename=None): global route_update_required """ Loads all clues from a CSV file :param filename: name of CSV file :return: None """ # if there is no filename, wipe all clues if filename == None: clues.clear() filename = "savefile.csv" # otherwise, load from file with open(filename, newline='') as f: csvreader = csv.reader(f, delimiter=',', quotechar='"') # skip header row next(csvreader) for row in csvreader: try: name = row[0] latitude = float(row[1]) longitude = float(row[2]) info = row[3] status = row[4] reqd = True if (row[5] == "True") else False addClue(name, info, longitude, latitude, "UNVISITED", required=reqd) except: return 1 clues_last_changed = time.time() route_update_required = True return 0 def loadDirty(filename=None): global clues_last_changed, route_update_required if filename == None: return # otherwise, load from file with open(filename, newline='') as f: csvreader = csv.reader(f, delimiter=',', quotechar='"') # skip header row next(csvreader) for row in csvreader: try: name = row[0] info = row[2] latlong = row[3].split(",") reqd = True if (row[4] == "True") else False if len(latlong) != 2: continue latitude = float(latlong[0]) longitude = float(latlong[1]) addClue(name, info, longitude, latitude, "UNVISITED", required=reqd) except Exception as e: print(e) return 1 clues_last_changed = time.time() route_update_required = True return 0 def save(): """ Writes all clues to a csv file :return: a CSV file with all clues """ with open("savefile.csv", 'w', newline='') as f: csvwriter = csv.writer(f, delimiter=',', quotechar='"') # add a header row csvwriter.writerow(["name", "latitude", "longitude", "info", "status", "required"]) for clue in clues: csvwriter.writerow([clue.name, clue.latitude, clue.longitude, clue.info, clue.status, clue.required]) 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()